SaToken 配置的读取
SaToken是一个权限验证框架,结合springboot,可以实现前后台的权限管理以及页面的功能管理。
官方文档可以参考 SaToken官方文档
一 配置节的理解
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# jwt秘钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
token-prefix: Bearer
二 如何使用
1. 生成临时token:
AppUserLoginModel appUserLoginModel = AppUserLoginModel.builder()
.appId(appUserVo.getAppId())
.appUserId(Convert.toStr(appUserVo.getId()))
.platformAppId(appUserVo.getPlatformAppId())
.tenantId(appUserVo.getTenantId())
.build();
//注意临时token没有带 前缀的
String token = SaTempUtil.createToken(appUserLoginModel, 200);
注意生成的临时token的默认为JWT格式的token,例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcGVuSWQiOiJvS3pCMzY3S2hLbUZ4b3BLYUF0UnFKMTRZZDBBIiwidGltZXN0YW1wIjoiMTczMTQwMzkxNDI2MiJ9.b2sUrX4BkEpOcRqisG01kwtw0n4lBl4tvEOGa1wwRxk
直接使用这个返回是不行的,token-prefix: Bearer
因为这里配置过前缀,所以在下面的获取
获取临时token String token = StpUtil.getTokenValue();
就会获取不到值。
生成能用的token的代码: String token = String.join(" ", SaManager.getConfig().getTokenPrefix(), token)
2. 获取token的逻辑分析
public String getTokenValue(boolean noPrefixThrowException) {
String tokenValue = this.getTokenValueNotCut();
String tokenPrefix = this.getConfigOrGlobal().getTokenPrefix();
if (SaFoxUtil.isNotEmpty(tokenPrefix)) {
if (SaFoxUtil.isEmpty(tokenValue)) {
tokenValue = null;
} else if (!tokenValue.startsWith(tokenPrefix + " ")) {
if (noPrefixThrowException) {
throw NotLoginException.newInstance(this.loginType, "-7", "未按照指定前缀提交 token,prefix=" + tokenPrefix, (String)null).setCode(11017);
}
tokenValue = null;
} else {
tokenValue = tokenValue.substring(tokenPrefix.length() + " ".length());
}
}
return tokenValue;
}
tokenValue的值即为配置的:token-name: Authorization 的值:Authorization tokenPrefix的值即为配置的:token-prefix: Bearer 的值:Bearer
最终生成的token应该为:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcGVuSWQiOiJvS3pCMzY3S2hLbUZ4b3BLYUF0UnFKMTRZZDBBIiwidGltZXN0YW1wIjoiMTczMTQwMzkxNDI2MiJ9.b2sUrX4BkEpOcRqisG01kwtw0n4lBl4tvEOGa1wwRxk 注意:Bearer后是有一个空格作为获取后的分隔符的
3. 结合自定义的Handler来验证token
添加一个自定义的SaAppUserCheckLoginHandler类来拦截自定义的token逻辑
@Component
public class SaAppUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaAppUserCheckLogin>
{
@Autowired
private AppMchConfigMapper appMchConfigMapper;
/**
* 处理带有 SaAppCheckLogin 这类注解的方法拦截
* @return
*/
@Override
public Class<SaAppUserCheckLogin> getHandlerAnnotationClass() {
return SaAppUserCheckLogin.class;
}
/**
* 鉴权的 工具类使用 自定义的 StpAppUserUtil.class
* @param saAppCheckLogin
* @param method
*/
@Override
public void checkMethod(SaAppUserCheckLogin saAppCheckLogin, Method method) {
// 前端在传输时,仍然还需要加上 token的前缀 例如:Bearer eyJhbGciOiJIUzI1NiJ9.eyJ2YWx1ZV9yZWNvcmQiOiIxMDAxNCIsImVmZiI6MTcyNTYxNjMwOTI5NH0.VeKqydbAuUYDmiEj9C11PehXyyALY--rX2DAlpj6_OA
// 只需要进行临时token的处理 ,可以直接使用自定义的验证规则 获取前端请求提交的参数
String token = StpUtil.getTokenValue();
//获取token中的用户信息
if(StringUtils.isEmpty(token)){
throw new SaTokenException("授权信息有误,请重新登录后再试").setCode(101);
}
// SaAnnotationHandlerInterface 有几个默认实现
String value = SaTempUtil.parseToken(token, String.class);
//从token中获取的 信息与 参数中的进行比较
// String appId = SaHolder.getRequest().getParam("appId");
// String tenantId = SaHolder.getRequest().getParam("tenantId");
//
// //检查渠道信息是否正常
// AppMchConfig appMchConfig = appMchConfigMapper.selectOne(new LambdaQueryWrapper<AppMchConfig>()
// .eq(AppMchConfig::getWxOpenPlatformAppid, appId)
// .eq(AppMchConfig::getTenantId, tenantId)
// );
//
// if(Objects.isNull(appMchConfig)){
// throw new NotPermissionException("应用已下线").setCode(102);
// }
if(!checkSign())
{
throw new SaSignException("签名错误").setCode(100);
}
long timeout = SaTempUtil.getTimeout(token);
System.out.println(timeout);
//#endregion
}
//检查 app 参数的sign是否被更改过
private boolean checkSign()
{
return true;
}
}
4. 集成spring-security的权限处理
在一般情况下,如果我们项目中集成了spring-security,可以在配置中配置,需要使用SaAppUserCheckLoginHandler验证的路由进行放过。这样,自带的SaInterceptor就不会拦截过滤掉的路由进行后台登录的验证:StpUtil.checkLogin();
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(SecurityProperties.class)
@RequiredArgsConstructor
public class SecurityConfig 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
// 获取所有的 但是排除 openapi开头的,为临时教验类型
.match(allUrlHandler.getUrls().stream().filter(it -> !it.startsWith("/openapi")).collect(Collectors.toList()))
// 对未排除的路径进行检查
.check(() -> {
// 检查是否登录 是否有token
StpUtil.checkLogin();
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
// token 无效
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
// 有效率影响 用于临时测试
// if (log.isDebugEnabled()) {
// log.info("剩余有效时间: {}", StpUtil.getTokenTimeout());
// log.info("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
// }
});
SaRouter.match("/openapi/**")
.check(() -> {
//判断临时token即可
String token = SaHolder.getRequest().getHeader("Authorization", "");
System.out.println(token);
});
})).addPathPatterns("/**")
// 排除不需要拦截的路径
.excludePathPatterns(securityProperties.getExcludes());
}
}
在这个例子中,我们将SaRouter.match("/openapi/**")
系统中以openapi开头的路由全部放过,这时候我们的应用访问开放接口就不需要登录后台了。
5. 对/openapi访问的路由启用token验证
以上我们做一两件事,首先是创建了SaAppUserCheckLoginHandler的自定注解处理器。第二件事是将 /openapi放过权限验证。之后,要对/openapi的接口访问启用token验证。就非常简单,只需要在需要启用token验证的controller上面添加**@SaAppUserCheckLogin注解即可。当将注解处理器添加到controller上的时候,所有的内部方法都将验证token,如果有某一个不需要,只需要在方法上添加@@SaIgnore**即可
@RestController
@RequestMapping("/openapi/platform")
**@SaAppUserCheckLogin**
public class ApiPlatformController extends BaseController {
@Autowired
UploadConfig uploadConfig;
@Autowired
private AppUserMapper appUserMapper;
@Autowired
private ISysTenantService sysTenantService;
/**
* 获取是否打开广告配置
* 需要验证token
* @return
*/
@PostMapping("getAdvertInfo")
public R<Integer> getIsOpenAdvertInfo(@RequestBody GetAdvertOpenRequest request) {
return R.ok(0);
}
/**
* 获取用户在平台的注册信息
* SAAS平台登录信息
* 不需要验证token
* @return
*/
@SaIgnore
@PostMapping("/GetPlatformUserInfo")
public R<PlatformLoginResultBean> getPlatformLoginInfo(@RequestBody PlatformLoginReq req) {
AppUserVo appUserVo = appUserService.queryById(Convert.toLong(req.getAppUserId(), 0L));
if (appUserVo == null) {
return R.fail("登录失败");
}
....
}
三 总结
很多时候,都会涉及到两种用户的权限的验证,后台管理的用户和APP的用户请求接口时的token验证,或者其它角色的用户权限的时候,使用以上方法就可以完美解决,按路由的方式来区分是最高效而且最简单的方式。
1. 不建议多账户体系认证中使用多账户认证来实现
系统自带的StpUtil.checkLogin(),其实也是通过注解处理器 SaCheckLoginHandler 来实现的。通过配置,可以拦截所有的请求。
虽然可以按照多账户认证来实现,但其实会有影响,因为通过测试发现都会触发会触发全局的UserActionListener,而这里就又跳到 StpUtil.checkLogin()进行验证,就成了一个死循环。
2. 使用基于路由的验证规则来实现多体系的用户权限
在不同的IAuthStrategy中处理登录的逻辑即可
直接修改wycms-common-security中的 SecurityConfig 中的路由配置匹配规则,放过你想通过自定义的hander处理路由,然后自行处理
3. 临时token的使用
临时token在使用的时候,仍然是在sa-token的配置约束下进行的,所以在生成的时候需要注意带上前缀,在APP端使用时候,header中使用Authroziation作为键名进行请求即可。当然你也可以自定义token-name.