✅作者:TuNan ✨个人主页:图南的个人主页 欢迎关注点赞收藏⭐留言
本文章使用的版本: SpringBoot:2.6.13 JDK:1.8 SpringCloud Alibaba:2021.0.5.0 Satoken:1.37.0 微服务架构下的鉴权一般分为两种:
每个服务各自鉴权网关统一鉴权 方案一和传统单体鉴权差别不大,不再过多赘述,本篇介绍方案二的整合步骤(以及在下游微服务、服务与服务之间传递用户信息):
1.流程梳理
大致流程与下图相似只是将jwt替换为了satoken
2.项目架构
3.在网关中引入如下依赖和配置 依赖:
配置:
package team.weyoung.gateway.config;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* [Sa-Token 权限认证] 配置类
*
* @author Tunan
*/
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
// todo 这里的路由需要根据实际情况修改
//SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}
这里还需要一个过滤器将参数转发到下游微服务
package team.weyoung.gateway.config;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 全局过滤器,为请求添加 satoken 参数
*
* @author kong
*
*/
@Component
public class SaIdTokenFilter implements GlobalFilter, Ordered {
@Override
public Mono
ServerHttpRequest newRequest = exchange
.getRequest()
.mutate()
// 为请求追加 satoken 参数
.header("satoken",StpUtil.getTokenValue())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
}
@Override
public int getOrder() {
return 0;
}
}
4.在common模块引入以下依赖和配置(其他服务依赖于common模块) 依赖:
配置(注册全局拦截器)
package team.weyoung.ojmodel.satoken;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
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;
/**
* Sa-Token 代码方式进行配置
*
* @author kong
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册 Sa-Token 的拦截器,打开注解式鉴权功能
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
/**
* 注册 Sa-Token全局过滤器,解决跨域问题
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 拦截与排除 path
.addInclude("/**").addExclude("/favicon.ico")
// 全局认证函数
.setAuth(obj -> {
// 校验 Id-Token 身份凭证 —— 以下两句代码可简化为:SaIdUtil.checkCurrentRequestToken();
String token = SaHolder.getRequest().getHeader("satoken");
StpUtil.getTokenSessionByToken(token);
})
.setBeforeAuth(obj -> {
})
;
}
}
5.用户服务 引入依赖:
yml文件:
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
# 是否从cookie中读取token
is-read-cookie: false
最后在我们的用户服务就可以使用StpUtil获取登录信息了,这里我使用的是无cookie模式将tokenvalue塞到返回值里面给到前端,前端可以使用pinia或者localstorage存储token并在每次请求前在请求头加上satoken即可完成鉴权
@Override
public LoginUserVO userLogin(String userAccount, String userPassword) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "账号错误");
}
if (userPassword.length() < 8) {
throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "密码错误");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 查询用户是否存在
QueryWrapper queryWrapper = QueryWrapper.create()
.from(USER)
.where(USER.USER_ACCOUNT.eq(userAccount))
.where(USER.USER_PASSWORD.eq(encryptPassword));
User user = userMapper.selectOneByQuery(queryWrapper);
// 用户不存在
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "用户不存在或密码错误");
}
// 3. 记录用户的登录态
long userId = user.getId();
StpUtil.login(userId);
StpUtil.getSession().set(SaSession.USER, user);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println(tokenInfo);
LoginUserVO loginUserVO = this.getLoginUserVO(user);
loginUserVO.setToken(tokenInfo);
return loginUserVO;
}
参考文章
发表评论