进学阁

业精于勤荒于嬉,行成于思毁于随

0%

基于Sa-Token的微服务权限验证

Sa-Token 是一个轻量级 Java 权限认证框架主,要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。Sa-Token 旨在以简单、优雅的方式完成系统的权限认证部分。

官网地址:https://sa-token.cc/

实现思路:

功能

  • 多种登陆模式,账号密码,手机号验证码等
  • 网关验证登陆是否过期,黑白名单等处理
  • 可以支持角色和权限字符串鉴权验证
  • 实现登出功能
  • 减少数据库访问,登陆后数据详情放入redis中

在pomelo中权限的验证,采用了token格式的数据,对与用户信息进行缓存的形式进行操作,并没有使用jwt,主要考虑有两种:jwt的数据保存在token字符串中,增加了前端到后段的通讯负担,敏感数据的解析一样需要从服务端中获取,所以在pomelo中,我们直接使用token来进行交互,并把用户的数据保存在redis缓存中,虽然需要承担redis缓存宕机带来的用户验证异常,但是也拥有很高的灵活性

网关统一认证处理

网关可以统一的来实现权限的认证,但是不够灵活,所以在网关中只对消息中是否存在token来进行判断,token消息是否过期进行处理,当消息正常时新增Header对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
75
76
77
78
79
80
81
82
@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {

@Autowired
private DistributedCache distributedCache;
// 排除过滤的 uri 地址,nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private AuthProperties authProperties;
/**
* 连接 Token 前缀和 Token 值的字符
*/
public static final String TOKEN_CONNECTOR_CHAT = " ";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String path = exchange.getRequest().getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(path, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
String token = getToken(request);
if (StrUtil.isEmpty(token)) {
return unauthorizedResponse(exchange, ErrorCode.TOKEN_EMPTY_ERROR);
}
// 验证token是否过期
if (!distributedCache.hasKey(authProperties.getIdentifier() + ":login:token:" + token)) {
return unauthorizedResponse(exchange, ErrorCode.TOKEN_EXPIRATION_ERROR);
}
String prefixToken =String.format("%s%s%s", authProperties.getPrefix(),TOKEN_CONNECTOR_CHAT, token) ;
addHeader(mutate, authProperties.getIdentifier(), prefixToken,true);
String loginUserKey = String.format("%S%S", SecurityConstants.USER_KEY, token);
if (distributedCache.hasKey(loginUserKey)) {
LoginUser loginUser= distributedCache.get(loginUserKey, LoginUser.class);
if (loginUser != null){
addHeader(mutate, SecurityConstants.USER_ID_HEADER, loginUser.getUserId(),false);
addHeader(mutate, SecurityConstants.USER_NAME_HEADER, loginUser.getUserName(),false);
}
}
return chain.filter(exchange.mutate().request(mutate.build()).build());
}

private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value,Boolean isEncode) {
if (value == null) {
return;
}
String valueStr = value.toString();
if (isEncode){
mutate.header(name, valueStr);
return;
}
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}

private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, ErrorCode errorCode) {
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), errorCode.getMessage(), errorCode.getCode());
}

/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_HEADER);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StrUtil.isNotEmpty(token) && token.startsWith(authProperties.getPrefix())) {
token = token.replaceFirst(authProperties.getPrefix(), "");
}
return token;
}

@Override
public int getOrder() {
return 0;
}
}

登陆授权

登陆需要支持两种模式一种是短信登陆,以及账号密码登陆。短信登陆时如果账号未注册,直接注册新用户,账号密码需要提前注册后才能登陆。当用户身份验证通过后,需要将token放入redis用于处理token超时的问题,2将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
@Override
public LoginRspVo login(LoginDTO loginDTO) {
// 预认证 验证参数
preAuthenticationCheck(loginDTO);
// 认证
LoginUser loginUser = authenticate(loginDTO);
CacheUtil.addLoginUser(loginUser);
// 角色获取
List<String> roles = getRoles(loginUser.getUserId());
if (CollectionUtil.isNotEmpty(roles)){
CacheUtil.updateRoleCache(loginUser.getUserId(), roles);

// 权限获取
for (String role : roles) {
List<String> permissions = getPermissions(role);
CacheUtil.updatePermissionCache(role, permissions);
}
}
LoginRspVo loginRspVo = new LoginRspVo();
loginRspVo.setAccess_token(loginUser.getToken());
loginRspVo.setUserId(loginUser.getUserId());
loginRspVo.setUserName(loginUser.getUserName());
loginRspVo.setLoginTime(loginUser.getLoginTime());
return loginRspVo;

}

实现思路:当系统用户登录完成后,将用户数据放入缓存,以及将用户的角色和权限信息放入到缓存。以供网关以及后续鉴权提供数据支持。

服务鉴权

在sa-token中提供了StpInterface借口,来对权限以及角色进行处理,处理思路:定义PomeloInterfaceImpl对用户的权限以及角色获取

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
@Component
public class PomeloInterfaceImpl implements StpInterface {

@Override
@SuppressWarnings("unchecked")
public List<String> getPermissionList(Object loginId, String loginType) {
// 1. 声明权限码集合
List<String> list = new ArrayList<>();
// 2. 遍历角色列表,查询拥有的权限码
for (String roleId : getRoleList(loginId, loginType)) {
List<String> permissionList = (List<String>) SaManager.getSaTokenDao()
.getObject(SaManager.getConfig().getTokenName()+":role-find-permission:" + roleId);
if (permissionList == null) {
continue;
}
list.addAll(permissionList);
}
return list;
}

@Override
@SuppressWarnings("unchecked")
public List<String> getRoleList(Object loginId, String loginType) {
List<String> roleList = (List<String>) SaManager.getSaTokenDao()
.getObject(SaManager.getConfig().getTokenName()+":loginId-find-role:" + loginId);
return roleList;
}
}