0%

若依框架具体流程(前后端分离)

学习若依所需前置知识

后端相关: Java, SSM, SpringBoot, SpringSecurity, Maven, Redis

前端相关: HTML+CSS+JS, Vue, ElementUI

RuoYi-Vue 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、MyBatis、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、代码生成等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。

登录

登录功能整体流程

若依登录流程.drawio

生成验证码

前端代码实现

基本思路

  • 前端页面在created生命周期时去请求(通过封装的axios请求读取配置文件来访问后端路径url请求前端,进行代理映射到后端,解决跨域问题)一个验证码(http://localhost/dev-api/captchaImage)以及Cookie,后端则返回一个图片

    • 统一前缀

    image-20231009143350244

    image-20231009143229901

  • 后端生成一个表达式,例如:1+1=2,1+1=?@2(相当于1+1变成了题干,而2变成了答案),将生成的题干转换成流生成图片,发送给前端

  • 将答案存入Redis(key会传给前端),前端就通过key查询Redis的值进行校验是否正确

后端代码实现

基本思路

  • 首先创建一个统一的返回结果(三个属性分别是,状态码,描述信息,数据)对象,然后读取判断验证码是否开启,并添加到返回结果中,如果没有开启则直接返回
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
// 获取配置文件中验证码的开关,如果为false则直接略过
boolean captchaEnabled = configService.selectCaptchaEnabled();
if (captchaEnabled)
{
// 根据uuid拼接上常量,组装为key
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
// 从redis中根据key值取出value,存入captcha变量中
String captcha = redisCache.getCacheObject(verifyKey);
// 取出value值后,删除redis中的缓存
redisCache.deleteObject(verifyKey);
// 如果取出的值为空,抛出验证码已失效的异常
if (captcha == null)
{
// 异步写入日志(分离出了业务逻辑)
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
// 验证码与前端请求的code不相等,抛出验证码错误的异常
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();
}
// IP黑名单校验
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);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
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());
// 生成token
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
// 生成uuid作为token
String token = IdUtils.fastUUID();
// 给用户设置token
loginUser.setToken(token);
// 获取用户的操作浏览器和操作系统
setUserAgent(loginUser);
// 刷新token
refreshToken(loginUser);

// 通过jwt来生成token
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) { // 验证返回的roles是否是一个非空数组
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))
{
// 多角色设置permissions属性,以便数据权限匹配权限
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);
}
}