学习若依所需前置知识
后端相关: Java, SSM, SpringBoot, SpringSecurity, Maven, Redis
前端相关: HTML+CSS+JS, Vue, ElementUI
RuoYi-Vue 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、MyBatis、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、代码生成等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。
登录
登录功能整体流程
生成验证码
前端代码实现
基本思路
后端代码实现
基本思路
- 首先创建一个统一的返回结果(三个属性分别是,状态码,描述信息,数据)对象,然后读取判断验证码是否开启,并添加到返回结果中,如果没有开启则直接返回
1 2 3 4 5 6 7 8
| AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled(); ajax.put("captchaEnabled", captchaEnabled); if (!captchaEnabled) { return ajax; }
|
- 其次生成一个uuid,拼接形成redis的key值
1 2 3 4 5
| String uuid = IdUtils.simpleUUID(); String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null; BufferedImage image = null;
|
- 随后从配置文件中取出验证码的类型,根据验证码的类型进行响应的处理,这里以数字为例,生成一个表达式,例如:1+1=?@2,对生成的字符串从@进行截取分成题干和答案,并将题干处理成验证码图片,同时将拼接的key,答案,有效时间,单位存入Redis缓存中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| String captchaType = RuoYiConfig.getCaptchaType(); if ("math".equals(captchaType)) { String capText = captchaProducerMath.createText(); capStr = capText.substring(0, capText.lastIndexOf("@")); code = capText.substring(capText.lastIndexOf("@") + 1); image = captchaProducerMath.createImage(capStr); } else if ("char".equals(captchaType)) { capStr = code = captchaProducer.createText(); image = captchaProducer.createImage(capStr); }
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
|
- 最后把uuid,还有通过流来把图片存入返回结果对象,并将结果返回
1 2 3 4 5 6 7 8 9 10 11 12 13
| FastByteArrayOutputStream os = new FastByteArrayOutputStream(); try { ImageIO.write(image, "jpg", os); } catch (IOException e) { return AjaxResult.error(e.getMessage()); }
ajax.put("uuid", uuid); ajax.put("img", Base64.encode(os.toByteArray())); return ajax;
|
登录的实现
前端代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true; if (this.loginForm.rememberMe) { Cookies.set("username", this.loginForm.username, { expires: 30 }); Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); } else { Cookies.remove("username"); Cookies.remove("password"); Cookies.remove('rememberMe'); } this.$store.dispatch("Login", this.loginForm).then(() => { this.$router.push({ path: this.redirect || "/" }).catch(()=>{}); }).catch(() => { this.loading = false; if (this.captchaEnabled) { this.getCode(); } }); } }); }
|
后端代码实现
1、校验验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| boolean captchaEnabled = configService.selectCaptchaEnabled(); if (captchaEnabled) { String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); String captcha = redisCache.getCacheObject(verifyKey); redisCache.deleteObject(verifyKey); if (captcha == null) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); throw new CaptchaExpireException(); } if (!code.equalsIgnoreCase(captcha)) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); throw new CaptchaException(); } }
|
2、登录前置验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); throw new UserNotExistsException(); }
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); }
if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); }
String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); throw new BlackListException(); }
|
3、校验用户名和密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Authentication authentication = null; try { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); AuthenticationContextHolder.setContext(authenticationToken); authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e) { if (e instanceof BadCredentialsException) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } else { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e.getMessage()); } } finally { AuthenticationContextHolder.clearContext(); }
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId()); return tokenService.createToken(loginUser);
|
同时记录日志
1 2 3 4
| AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal(); recordLoginInfo(loginUser.getUserId());
|
4、生成Token
1 2 3 4 5 6 7 8 9 10 11 12 13
| String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token);. return createToken(claims);
|
使用异步任务管理器,结合线程池,实现了异步的操作日志记录,和业务逻辑实现异步解耦合.
获取用户权限
前端代码实现
1、登录的同时还发送了一个getInfo请求(访问每个页面都会发送,用路由前置守卫实现)
2、获取到返回的结果会把角色以及权限存储到Vuex当中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| GetInfo({ commit, state }) { return new Promise((resolve, reject) => { getInfo().then(res => { const user = res.user const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; if (res.roles && res.roles.length > 0) { commit('SET_ROLES', res.roles) commit('SET_PERMISSIONS', res.permissions) } else { commit('SET_ROLES', ['ROLE_DEFAULT']) } commit('SET_ID', user.userId) commit('SET_NAME', user.userName) commit('SET_AVATAR', avatar) resolve(res) }).catch(error => { reject(error) }) }) },
|
3、拿到权限后去发送getRouters请求获取动态路由,根据权限生成菜单
后端代码实现
1、查询账号权限
1 2 3 4 5 6 7 8 9 10
| SysUser user = SecurityUtils.getLoginUser().getUser();
Set<String> roles = permissionService.getRolePermission(user);
Set<String> permissions = permissionService.getMenuPermission(user); AjaxResult ajax = AjaxResult.success(); ajax.put("user", user); ajax.put("roles", roles); ajax.put("permissions", permissions); return ajax;
|
2、首先获取角色
1 2 3 4 5 6 7 8 9 10 11
| Set<String> roles = new HashSet<String>();
if (user.isAdmin()) { roles.add("admin"); } else { roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); } return roles;
|
3、根据角色赋予权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if (user.isAdmin()) { perms.add("*:*:*"); } else { List<SysRole> roles = user.getRoles(); if (!CollectionUtils.isEmpty(roles)) { for (SysRole role : roles) { Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); role.setPermissions(rolePerms); perms.addAll(rolePerms); } } else { perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); } } return perms;
|
4、前端发送getRouters请求,根据id来查询用户的动态路由,随后进行递归查询菜单
1 2 3 4 5 6 7 8 9 10 11
| List<SysMenu> childList = getChildList(list, t); t.setChildren(childList); for (SysMenu tChild : childList) { if (hasChild(list, tChild)) { recursionFn(list, tCh ild); } }
|