使用JWT进行权限检测

1 ,首先导入它的依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
</dependency>

2 ,实现StpInterface接口,并将当前登录用户具有的角色和权限赋予

实现了 StpInterface接口的 SaPermissionImpl类

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
package com.firerock.core.satoken;

import cn.dev33.satoken.stp.StpInterface;
import com.firerock.common.base.domain.model.LoginUser;
import com.firerock.common.helper.LoginHelper;

import java.util.ArrayList;
import java.util.List;


public class SaPermissionImpl implements StpInterface {

/**
* 获取菜单权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
return new ArrayList<>(loginUser.getMenuPermission());
}

/**
* 获取角色权限列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
return new ArrayList<>(loginUser.getMenuPermission());
}
}

2-1 ,loginHelper工具类 (此工具类需要在用户登录接口中设定login()或loginByDevice() )

loginHelper工具类

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.firerock.common.helper;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.firerock.common.base.domain.model.LoginUser;
import com.firerock.common.constant.UserConstants;
import com.firerock.common.enums.DeviceType;
import com.firerock.common.enums.UserType;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;


@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginHelper {

public static final String LOGIN_USER_KEY = "loginUser";
public static final String USER_KEY = "userId";

/**
* 登录系统
*
* @param loginUser 登录用户信息
*/
public static void login(LoginUser loginUser) {
loginByDevice(loginUser, null);
}

/**
* 登录系统 基于 设备类型
* 针对相同用户体系不同设备
*
* @param loginUser 登录用户信息
*/
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
SaStorage storage = SaHolder.getStorage();
storage.set(LOGIN_USER_KEY, loginUser);
storage.set(USER_KEY, loginUser.getUserId());
SaLoginModel model = new SaLoginModel();
if (ObjectUtil.isNotNull(deviceType)) {
model.setDevice(deviceType.getDevice());
}
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
/**
* 获取用户(多级缓存)
*/
public static LoginUser getLoginUser() {
LoginUser loginUser = (LoginUser) SaHolder.getStorage().get(LOGIN_USER_KEY);
if (loginUser != null) {
return loginUser;
}
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
return loginUser;
}

/**
* 获取用户基于token
*/
public static LoginUser getLoginUser(String token) {
return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
}

/**
* 获取用户id
*/
public static Long getUserId() {
Long userId;
try {
userId = Convert.toLong(SaHolder.getStorage().get(USER_KEY));
if (ObjectUtil.isNull(userId)) {
userId = Convert.toLong(StpUtil.getExtra(USER_KEY));
SaHolder.getStorage().set(USER_KEY, userId);
}
} catch (Exception e) {
return null;
}
return userId;
}

/**
* 获取部门ID
*/
public static Long getDeptId() {
return getLoginUser().getDeptId();
}

/**
* 获取用户账户
*/
public static String getUsername() {
return getLoginUser().getUsername();
}

public static String getNickName(){
return getLoginUser().getNickName();
}

/**
* 获取用户类型
*/
public static UserType getUserType() {
String loginId = StpUtil.getLoginIdAsString();
return UserType.getUserType(loginId);
}

/**
* 是否为管理员
*
* @param userId 用户ID
* @return 结果
*/
public static boolean isAdmin(Long userId) {
return UserConstants.ADMIN_ID.equals(userId);
}

public static boolean isAdmin() {
return isAdmin(getUserId());
}

}

3 ,用户登录接口处, 使用LoginHelper工具类,将用户登录信息提交给Stp

sysLoginController登录控制类

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid) {
HttpServletRequest request = ServletUtils.getRequest();
boolean captchaEnabled = configService.selectCaptchaEnabled();
// 此步是查询用户并返回存在的用户信息
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();
}

private SysUser loadUserByUsername(String username) {
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getUserName, SysUser::getStatus)
.eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return userMapper.selectUserByUserName(username);
}

/**
* 登录校验
*/
private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;

// 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
Integer errorNumber = RedisUtils.getCacheObject(errorKey);
// 锁定时间内登录 则踢出
if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}

// 根据login()中,调用checkLogin()时的 () -> !BCrypt.checkpw(password, user.getPassword()) 判断用户密码是否正确, 添加用户时需要使用Bcrypt进行加密 user.setPassword(BCrypt.hashpw(user.getPassword())), 如果密码错误就进入
if (supplier.get()) {
// 是否第一次
errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
// 达到规定错误次数 则锁定登录
if (errorNumber.equals(maxRetryCount)) {
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
// 未达到规定错误次数 则递增
RedisUtils.setCacheObject(errorKey, errorNumber);
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
}
}

// 登录成功 清空错误次数
RedisUtils.deleteObject(errorKey);
}

/**
* 记录登录信息
*
* @param username 用户名
* @param status 状态
* @param message 消息内容
* @return
*/
private void recordLogininfor(String username, String status, String message) {
LoginInEvent loginInEvent = new LoginInEvent();
loginInEvent.setUsername(username);
loginInEvent.setStatus(status);
loginInEvent.setMessage(message);
loginInEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(loginInEvent);
}
/**
* 记录登录信息
*
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId, String username) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(ServletUtils.getClientIP());
sysUser.setLoginDate(DateUtils.getNowDate());
sysUser.setUpdateBy(username);
userMapper.updateById(sysUser);
}

4 ,sa-token的全局拦截器

- 若要使用@SaCheckPermission(“system:role:query”)或@SaCheckRole(“admin”),则需要注册sa-token的全局拦截器

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.firerock.core.config;

import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import com.firerock.common.utils.SpringUtils;
import com.firerock.core.config.properties.SecurityProperties;
import com.firerock.core.handler.AllUrlHandler;
import com.firerock.core.satoken.PlusSaTokenDao;
import com.firerock.core.satoken.SaPermissionImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@RequiredArgsConstructor
@Slf4j
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

private final SecurityProperties securityProperties;

/**
* 注册sa-token的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaInterceptor(handler -> {
AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
// 登录验证 -- 排除多个路径
SaRouter
// 获取所有的
.match(allUrlHandler.getUrls())
// 对未排除的路径进行检查
.check(() -> {
// 检查是否登录 是否有token
StpUtil.checkLogin();
});
})).addPathPatterns("/**")
// 排除不需要拦截的路径
.excludePathPatterns(securityProperties.getExcludes());
}

@Bean
public StpLogic getStpLogicJwt() {
// Sa-Token 整合 jwt (简单模式)
return new StpLogicJwtForSimple();
}

/**
* 权限接口实现(使用bean注入方便用户替换)
*/
@Bean
public StpInterface stpInterface() {
return new SaPermissionImpl();
}

/**
* 自定义dao层存储
*/
@Bean
public SaTokenDao saTokenDao() {
return new PlusSaTokenDao();
}

}

5 ,使用权限注解进行校验

@SaCheckPermission

1
2
3
4
5
6
7
   // @SaCheckPermission("member:list:add") 注解表明校验当前用户的PermissionList中是否有 "member:list:add这个权限字段
@SaCheckPermission("member:list:add")
@Log(title = "新增人员", businessType = BusinessType.INSERT)
@PostMapping("/add")
public Result<Void> addMember(@RequestBody SysUser user) {
return toAjax(result);
}

@SaCheckRole

1
2
3
4
5
6
7
   // @RoleList("admin") 注解表明校验当前用户的RoleList中是否有 "admin"这个角色字段
@SaCheckRole("admin")
@Log(title = "新增人员", businessType = BusinessType.INSERT)
@PostMapping("/add")
public Result<Void> addMember(@RequestBody SysUser user) {
return toAjax(result);
}

@SaCheckLogin

1
2
3
4
5
6
7
   // @SaCheckLogin 注解表明校验当前用户是否登录,由用户登录接口处调用LoginHelper.loginByDevice(loginUser, DeviceType.PC);中的StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));进行用户登录注册
@SaCheckLogin
@Log(title = "新增人员", businessType = BusinessType.INSERT)
@PostMapping("/add")
public Result<Void> addMember(@RequestBody SysUser user) {
return toAjax(result);
}

至此使用JWT做权限校验的总过程进行完毕