ruoyi-vue-plus-基础功能
# 接口相关
# 登录接口 /login
大致步骤
- 登录请求 /login
- 验证码验证
- 查用户
- 验证密码 , 明文加密比较
- 生成Token
- 记录登录日志
- 响应token
整体登录业务步骤 SysLoginService#login()
public String login(String username, String password, String code, String uuid) {
boolean captchaEnabled = configService.selectCaptchaEnabled();
// 验证码开关
if (captchaEnabled) {
validateCaptcha(username, code, uuid);
}
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUser user = loadUserByUsername(username);
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
recordLoginInfo(user.getUserId(), username);
return StpUtil.getTokenValue();
}
密码验证 SysLoginService#checkLogin()
private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出
if (errorNumber >= maxRetryCount) {
// 日志记录
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}
if (supplier.get()) {
// 错误次数递增
errorNumber++;
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录
if (errorNumber >= maxRetryCount) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
// 未达到规定错误次数
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
}
}
// 登录成功 清空错误次数
RedisUtils.deleteObject(errorKey);
}
生成token
通过sa-token实现
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
SaStorage storage = SaHolder.getStorage();
// 缓存 用户信息
storage.set(LOGIN_USER_KEY, loginUser);
// 缓存 用户ID
storage.set(USER_KEY, loginUser.getUserId());
SaLoginModel model = new SaLoginModel();
if (ObjectUtil.isNotNull(deviceType)) {
model.setDevice(deviceType.getDevice());
}
// 登录 , 并拓展记录配置信息 登录类型 , 用户ID
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
# 退登接口 /logout
大致步骤 :
- 退出登录请求 /logout
- sa-token方法退出 , StpUtil#logout()
- 记录退出登录日志
- 前端清空token缓存
# 个人信息 /getInfo
登录成功后会在路由守卫调用此 /getInfo接口 获取用户自身的基本信息 , 并存到 vuex 中...
响应结构
permissions
(数组) : 权限文本标识roles
(数组) : 身份文本标识user
(SysUser对象) : 个人用户对象
{
"code": 200,
"msg": "操作成功",
"data": {
"permissions": [
"system:oss:download",
"system:oss:upload",
"demo:tree:query"
],
"roles": [
"teacher"
],
"user": {...}
}
}
# 路由接口 /getRouters
登录成功后会获取路由路径 , 它也是在路由守卫和 /getInfo接口 一同调用
后端主要结构体
库表结构 : SysMenu
响应结构 : RouterVo
后端主要方法
核心封装方法
- 树型封装 : SysMenuServiceImpl#getChildPerms()
- 封装转化 : SysMenuServiceImpl#buildMenus()
开放应用接口 SysLoginController#getRouters()
@GetMapping("getRouters")
public R<List<RouterVo>> getRouters() {
Long userId = LoginHelper.getUserId();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return R.ok(menuService.buildMenus(menus));
}
获取库菜单主要业务方法 SysMenuServiceImpl#selectMenuTreeByUserId()
@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
List<SysMenu> menus = null;
if (LoginHelper.isAdmin(userId)) {
menus = baseMapper.selectMenuTreeAll();
} else {
menus = baseMapper.selectMenuTreeByUserId(userId);
}
return getChildPerms(menus, 0);
}
getChildPerms()方法 , 将列表的所有节点以树型结构封装 , 其结构自行和思路查阅
前端动态实现
通过 /getRouters
接口的 数据的大致结构
点击代码展开
{
"code": 200,
"msg": "操作成功",
"data": [
{
"name": "System",
"path": "/system",
"hidden": false,
"redirect": "noRedirect",
"component": "Layout",
"alwaysShow": true,
"meta": {
"title": "系统管理",
"icon": "system",
"noCache": false,
"link": null
},
"children": [
{
"name": "User",
"path": "user",
"hidden": false,
"component": "system/user/index",
"meta": {
"title": "用户管理",
"icon": "user",
"noCache": false,
"link": null
}
},
{
"name": "Role",
"path": "role",
"hidden": false,
"component": "system/role/index",
"meta": {
"title": "角色管理",
"icon": "peoples",
"noCache": false,
"link": null
}
},
{
"name": "Menu",
"path": "menu",
"hidden": false,
"component": "system/menu/index",
"meta": {
"title": "菜单管理",
"icon": "tree-table",
"noCache": false,
"link": null
}
},
{
"name": "Dept",
"path": "dept",
"hidden": false,
"component": "system/dept/index",
"meta": {
"title": "部门管理",
"icon": "tree",
"noCache": false,
"link": null
}
},
{
"name": "Post",
"path": "post",
"hidden": false,
"component": "system/post/index",
"meta": {
"title": "岗位管理",
"icon": "post",
"noCache": false,
"link": null
}
},
{
"name": "Dict",
"path": "dict",
"hidden": false,
"component": "system/dict/index",
"meta": {
"title": "字典管理",
"icon": "dict",
"noCache": false,
"link": null
}
},
{
"name": "Config",
"path": "config",
"hidden": false,
"component": "system/config/index",
"meta": {
"title": "参数设置",
"icon": "edit",
"noCache": false,
"link": null
}
},
{
"name": "Notice",
"path": "notice",
"hidden": false,
"component": "system/notice/index",
"meta": {
"title": "通知公告",
"icon": "message",
"noCache": false,
"link": null
}
},
{
"name": "Log",
"path": "log",
"hidden": false,
"redirect": "noRedirect",
"component": "ParentView",
"alwaysShow": true,
"meta": {
"title": "日志管理",
"icon": "log",
"noCache": false,
"link": null
},
"children": [
{
"name": "Operlog",
"path": "operlog",
"hidden": false,
"component": "monitor/operlog/index",
"meta": {
"title": "操作日志",
"icon": "form",
"noCache": false,
"link": null
}
},
{
"name": "Logininfor",
"path": "logininfor",
"hidden": false,
"component": "monitor/logininfor/index",
"meta": {
"title": "登录日志",
"icon": "logininfor",
"noCache": false,
"link": null
}
}
]
},
{
"name": "Oss",
"path": "oss",
"hidden": false,
"component": "system/oss/index",
"meta": {
"title": "文件管理",
"icon": "upload",
"noCache": false,
"link": null
}
}
]
},
{
"name": "Tool",
"path": "/tool",
"hidden": false,
"redirect": "noRedirect",
"component": "Layout",
"alwaysShow": true,
"meta": {
"title": "系统工具",
"icon": "tool",
"noCache": false,
"link": null
},
"children": [
{
"name": "Build",
"path": "build",
"hidden": false,
"component": "tool/build/index",
"meta": {
"title": "表单构建",
"icon": "build",
"noCache": false,
"link": null
}
},
{
"name": "Gen",
"path": "gen",
"hidden": false,
"component": "tool/gen/index",
"meta": {
"title": "代码生成",
"icon": "code",
"noCache": false,
"link": null
}
}
]
}
]
}
调用此接口在于 vuex 中的 permission 模块
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
// 深拷贝
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
// 侧边栏路由
const sidebarRoutes = filterAsyncRouter(sdata)
// 重写路由
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
// 过滤路由 , 根据 管理权的权限标识 控制过滤
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
})
}
GenerateRoutes() 调用在于 路由守卫的前置拦截 src/permission.js
官网文档 : https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html (opens new window)
在路由守卫中 动态添加 路由信息
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
# 导出接口 /export
采用 用户信息导出导入作为示例应用
大致流程
- 数据源拿到集合
- 将集合转化对象 , 转化为Vo (Vo中一般有字段映射标识)
- 调用工具Excel实现导出下载
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@SaCheckPermission("system:user:export")
@PostMapping("/export")
public void export(SysUser user, HttpServletResponse response) {
List<SysUser> list = userService.selectUserList(user);
List<SysUserExportVo> listVo = BeanUtil.copyToList(list, SysUserExportVo.class);
for (int i = 0; i < list.size(); i++) {
SysDept dept = list.get(i).getDept();
SysUserExportVo vo = listVo.get(i);
if (ObjectUtil.isNotEmpty(dept)) {
vo.setDeptName(dept.getDeptName());
vo.setLeader(dept.getLeader());
}
}
ExcelUtil.exportExcel(listVo, "用户数据", SysUserExportVo.class, response);
}
# 导入接口 /import
采用 用户信息导出导入作为示例应用
大致流程
# 拦截器相关
拦截器实现通过 ResourcesConfig
配置类
Sa-Token拦截器 SaTokenConfig
类 点击跳转了解相关
时间统计拦截器 PlusWebInvokeTimeInterceptor
类 (全局拦截)
前置拦截
打印请求日志以及参数信息
后置拦截
计算请求过程时长
PlusWebInvokeTimeInterceptor
计时拦截器类
点击展开
@Slf4j
public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
private final String prodProfile = "prod";
private final TransmittableThreadLocal<StopWatch> invokeTimeTL = new TransmittableThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
String url = request.getMethod() + " " + request.getRequestURI();
// 打印请求参数
if (isJsonRequest(request)) {
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader);
}
log.debug("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JsonUtils.toJsonString(parameterMap);
log.debug("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.debug("[PLUS]开始请求 => URL[{}],无参数", url);
}
}
StopWatch stopWatch = new StopWatch();
invokeTimeTL.set(stopWatch);
stopWatch.start();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
StopWatch stopWatch = invokeTimeTL.get();
stopWatch.stop();
log.debug("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime());
invokeTimeTL.remove();
}
}
/**
* 判断本次请求的数据类型是否为json
*
* @param request request
* @return boolean
*/
private boolean isJsonRequest(HttpServletRequest request) {
String contentType = request.getContentType();
if (contentType != null) {
return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
}
return false;
}
}
# 异常相关
GlobalExceptionHandler
全局异常处理器类
异常种类
异常类 | 说明 |
---|---|
NotPermissionException | 权限异常 |
NotRoleException | 角色异常 |
NotLoginException | 认证异常 |
HttpRequestMethodNotSupportedException | 请求异常 |
DuplicateKeyException | SQL 主键/唯一 异常 |
MyBatisSystemException | MyBatis异常 |
ServiceException | 业务异常 |
MissingPathVariableException | 请求参数异常 |
MethodArgumentTypeMismatchException | 请求参数类型不匹配 |
BindException | 自定义验证异常 (绑定的参数) |
ConstraintViolationException | 自定义验证异常 (约束转化的参数) |
MethodArgumentNotValidException | 自定义验证异常 (对批注的参数) |