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

获取临时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.