Bootstrap

认证鉴权框架之—sa-token

一、概述

Satoken 是一个 Java 实现的权限认证框架,它主要用于 Web 应用程序的权限控制。Satoken 提供了丰富的功能来简化权限管理的过程,使得开发者可以更加专注于业务逻辑的开发。

二、逻辑流程

1、登录认证
(1)、创建token
当用户输入用户名和密码请求登录,后端验证用户名和密码的正确性。验证成功时,调用sa-token提供的登录接口,创建token

StpUtil.login(saBaseLoginUser.getId(), new SaLoginModel().setDevice(device).setExtra("name", saBaseLoginUser.getName()));

此方法,会自动创建token,并将token和会话session相关联,并且返回给前端的响应头中自动设置token:如下:
Response Header中返回set-cookie:token=token值
在这里插入图片描述
浏览器接受response后,会自动设置token的请求头,之后请求该域的接口时,会自动带上该请求头
Request Header中会携带token
在这里插入图片描述
如上的创建token,到给前端设置token的过程,只需要后端调用sa-token的登录接口,其他无需我们做任何事情。

(2)、创建当前用户的缓存
当调用完成sa-token的登录接口后,我们可以构建当前登录用户的对象SaBaseLoginUser,该对象可以添加权限,角色,组织等信息。
设置到sa-token的缓存中,这样在之后的接口中,我们需要获取当前用户信息或当前用户信息的权限等信息时,就可以直接从sa-token缓存中获取,就无需从数据库中再次查询。

设置当前用户的缓存
StpUtil.getTokenSession().set("loginUser", saBaseLoginUser);
获取当前用户的缓存
Object loginUser = StpUtil.getTokenSession().get("loginUser");
if (ObjectUtil.isNull(loginUser)) throw new CommonException("用户未登录");
SaBaseLoginUser saBaseLoginUser = (SaBaseLoginUser) loginUser;

注意,这里我们最好整合redis使用,否则会占用系统内存,占用内存多了就会造成性能瓶颈问题。同时整合redis也可以适用分布式多节点的服务。

2、查询权限(菜单和接口)
(1)、菜单权限
关于菜单,针对每一个前端页面的路由创建不同的菜单数据保存到数据库就行,之后针对角色进行授权即可。方法比较常见不在赘述。

(2)、接口权限
关于接口权限,sa-token自带一些注解类去管理接口权限,如:在controller类上使用@SaCheckRole注解或接口上使用@SaCheckPermission注解,标识该类或者接口为待认证的接口。

在spring容器初始化时,会初始化和注入所有的接口到容器中被spring所管理,可以通过springApplication上下文对象获取所有的RequestMappingHandlerMapping.class,即所有的controller类。在分别requestMappingHandlerMapping.getHandlerMethods()方法,即查询了所有的接口。
示例代码:

SpringUtil.getApplicationContext().getBeansOfType(RequestMappingHandlerMapping.class).values()
                .forEach(requestMappingHandlerMapping -> requestMappingHandlerMapping.getHandlerMethods()
                        .forEach((key, value) -> {
                            SaCheckPermission saCheckPermission = value.getMethod().getAnnotation(SaCheckPermission.class);
                            if (ObjectUtil.isNotEmpty(saCheckPermission)) {
                                PatternsRequestCondition patternsCondition = key.getPatternsCondition();
                                if (patternsCondition != null) {
                                    String apiName = "未定义接口名称";
                                    ApiOperation apiOperation = value.getMethod().getAnnotation(ApiOperation.class);
                                    if (ObjectUtil.isNotEmpty(apiOperation)) {
                                        String annotationValue = apiOperation.value();
                                        if (ObjectUtil.isNotEmpty(annotationValue)) {
                                            apiName = annotationValue;
                                        }
                                    }
permissionResult.add(patternsCondition.getPatterns().iterator().next() + StrUtil.BRACKET_START + apiName + StrUtil.BRACKET_END);
                                }
                            }
                        }));

3、授权
(1)、菜单授权
从数据库中查询所有的菜单数据,直接授权给角色即可。
(2)、接口授权
通过如上的方法,查询所有的接口权限(可以在容器初始化时保存到缓存中,减少查询次数)。之后做成表单,授权给角色或者指定账号即可。
在这里插入图片描述
4、鉴权
sa-token拦截器(SaInterceptor),我们只需要将拦截器注册到spring容器中,拦截所有的请求进行校验。

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关,只是说明哪些接口不需要被拦截器拦截,此处都拦截)
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

sa-token会开放查询用户权限和角色的接口给我们重写,我们只需要覆写(自定义类实现StpInteface接口,覆写里面的方法)获取用户权限和角色的接口即可。
在这里插入图片描述
5、总结
如上,我们描述了一个完整示例。从创建token,前端在指定域内请求携带token。在到获取权限,授权和鉴权的全过程。授权信息一般可以保存到数据库中,通过sa-token官方的示例去覆写接口即可。其他只需要我们在合适的类上或者接口上添加注解就行。

三、代码示例
1、引入依赖
如果服务只有一个节点,仅配置第一个sa-token-spring-boot-starter就够用了,后两个依赖是将redis和sa-token整合,这样认证信息保存到redis中,跨节点权限认证时也不会出现问题。

<!--版本-->
<properties>
    <sa.token.version>1.31.0</sa.token.version>
</properties>

<!-- sa-token -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>${sa.token.version}</version>
</dependency>

<!-- sa-token 整合 redis (使用jackson序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis-jackson</artifactId>
    <version>${sa.token.version}</version>
</dependency>

<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-alone-redis</artifactId>
    <version>${sa.token.version}</version>
</dependency>

2、配置文件

#########################################
# redis configuration
#########################################
spring:
  redis:
    database: 0
    host: 172.xx.xxx.xx
    port: 6379
    password:
    timeout: 10s
    lettuce:
      pool:
        max-active: 200       // 连接池最大连接数
        max-wait: -1ms       // 连接池最大阻塞等待时间(负数表示不限制)
        max-idle: 10          // 连接池中最大空闲连接
        min-idle: 0           // 连接池中最小空闲连接
#########################################
# sa-token configuration
#########################################
sa-token:
  token-name: token     // token名称,即设置cookie的key值
  timeout: 2592000      // token有效期(秒),默认30天,-1代表永久有效
  activity-timeout: -1     // 最低活跃频率(秒),即超过这个时间没有访问系统会冻结该token,默认-1代表永不冻结
  is-concurrent: true    // 是否允许同一账号多地登录
  is-share: false      // 是否同一账号多地登录时共用同一个token
  max-login-count: -1
  token-style: random-32
  is-log: false     // 是否输出操作日志
  is-print: false

# sa-token alone-redis configuration
  alone-redis:
    database: 2    // 指定sa-token使用redis的哪一个库
    host: ${spring.redis.host}
    port: ${spring.redis.port}
    password: ${spring.redis.password}
    timeout: ${spring.redis.timeout}
    lettuce:
      pool:
        max-active: ${spring.redis.lettuce.pool.max-active}
        max-wait: ${spring.redis.lettuce.pool.max-wait}
        max-idle: ${spring.redis.lettuce.pool.max-idle}
        min-idle: ${spring.redis.lettuce.pool.min-idle}

如上配置的sa-token整合redis后,sa-token在创建token信息时,会自动将token信息保存到配置的redis中,无需我们在操作redis进行设置。
在这里插入图片描述
3、全局管理类
这里我们注册sa-token拦截器,注入StpLogic管理类(一般多种用户类型时需要),重写注解合并方法(父类有的注解也会被作用到子类处理),覆写sa-token的自定义接口查询用户角色和权限。

import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.strategy.SaStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.nivic.common.auth.enums.SaClientTypeEnum;
import cn.nivic.common.auth.util.StpClientLoginUserUtil;
import cn.nivic.common.auth.util.StpLoginUserUtil;
import java.util.List;

/**
 * SaToken鉴权配置
 **/
@Configuration
public class AuthConfigure implements WebMvcConfigurer {

    /**
     * 注册Sa-Token的注解拦截器,打开注解式鉴权功能
     *
     * 注解的方式有以下几中,注解既可以加在接口方法上,也可加在Controller类上:
     * 1.@SaCheckLogin: 登录认证 —— 只有登录之后才能进入该方法(常用)
     * 2.@SaCheckRole("admin"): 角色认证 —— 必须具有指定角色标识才能进入该方法(常用)
     * 3.@SaCheckPermission("user:add"): 权限认证 —— 必须具有指定权限才能进入该方法(常用)
     * 4.@SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法
     * 5.@SaCheckBasic: HttpBasic认证 —— 只有通过 Basic 认证后才能进入该方法
     *
     * 在Controller中创建一个接口,默认不需要登录也不需要任何权限都可以访问的,只有加了上述注解才会校验
     **/
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关,只是说明哪些接口不需要被拦截器拦截,此处都拦截)
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

    @Bean("stpLogic")
    public StpLogic getStpLogic() {
        // 注入Sa-Token的StpLogic,客户端类型为B,处理B端用户的权限管理
        return new StpLogic(SaClientTypeEnum.B.getValue());
    }

    @Bean("stpClientLogic")
    public StpLogic getStpClientLogic() {
        // 注入Sa-Token的StpLogic,客户端类型为C,处理C端用户的权限管理        		return new StpLogic(SaClientTypeEnum.C.getValue());
    }

    @Bean
    public void rewriteSaStrategy() {
        // 重写Sa-Token的注解处理器,增加注解合并功能
// 这里的注解合并,即在继承体系中,也能获取到父类的注解实例。
        SaStrategy.me.getAnnotation = AnnotatedElementUtils::getMergedAnnotation;
    }

    /**
     * 权限认证接口实现类,集成权限认证功能
* 重写sa-token获取权限的接口,这里用户更具业务编码从数据库或者mongoDb中获取权限
     **/
    @Component
    public static class StpInterfaceImpl implements StpInterface {

        /**
         * 返回一个账号所拥有的权限码集合
         */
        @Override
        public List<String> getPermissionList(Object loginId, String loginType) {
            if (SaClientTypeEnum.B.getValue().equals(loginType)) {
                return StpLoginUserUtil.getLoginUser().getPermissionCodeList();
            } else {
                return StpClientLoginUserUtil.getClientLoginUser().getPermissionCodeList();
            }
        }

        /**
         * 返回一个账号所拥有的角色标识集合
         */
        @Override
        public List<String> getRoleList(Object loginId, String loginType) {
            if (SaClientTypeEnum.B.getValue().equals(loginType)) {
                return StpLoginUserUtil.getLoginUser().getRoleCodeList();
            } else {
                return StpClientLoginUserUtil.getClientLoginUser().getRoleCodeList();
            }
        }
    }

}

4、注解和使用
(1)、@SaCheckLogin
放在controller的接口方法上,在调用到这个方法的时候,会校验用户是否登录,如果没有登录会直接报错,保障安全。一般放在关键接口上,一般接口无需如此。

import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.stp.StpUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;

@Api(tags = "B端登录控制器")
@RestController
@Validated
public class AuthController {

    /**
     * B端退出
     **/
    @ApiOperationSupport(order = 5)
    @ApiOperation("B端退出")
    @SaCheckLogin
    @GetMapping("/auth/b/doLogout")
    public CommonResult<String> doLogout() {
        StpUtil.logout();
        return CommonResult.ok();
    }

    /**
     * B端获取用户信息
     **/
    @ApiOperationSupport(order = 6)
    @ApiOperation("B端获取用户信息")
    @SaCheckLogin
    @GetMapping("/auth/b/getLoginUser")
    public CommonResult<SaBaseLoginUser> getLoginUser() {
        return CommonResult.data(authService.getLoginUser());
    }
}

(2)、@SaCheckPermission
放在controller的接口方法上,用户请求到这个方法时,会校验用户是否有这个接口的权限,没有权限会直接报错。常用在数据增删改查接口上。
至于sa-token获取接口权限:则是通过全局配置类中实现StpInterfaceImpl接口,获取当前用户的全部权限。

 @ApiOperationSupport(order = 1)
    @ApiOperation("获取机构分页")
    @SaCheckPermission("/biz/org/page")
    @GetMapping("/biz/org/page")
    public CommonResult<Page<BizOrg>> page(BizOrgPageParam bizOrgPageParam) {
        return CommonResult.data(bizOrgService.page(bizOrgPageParam));
    }

(3)、@SaCheckRole
@SaCheckRole是sa-token校验角色权限的注解,这里自定义注解SaClientCheckRole,在注解类上添加@SaCheckRole注解,从而达到sa-token校验角色的效果之后,还可以继续扩展注解功能。
前提:我们在全部配置类中调整了注解处理器为注解合并方式,这样sa-token在解析注解SaClientCheckRole时也会读取父类的SaCheckRole注解,从而达到校验的效果。
至于sa-token获取角色:则是通过全局配置类中实现StpInterfaceImpl接口,获取当前用户的全部角色。

@SaCheckRole(type = StpClientUtil.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaClientCheckRole {

    /**
     * 需要校验的角色标识
     * @return 需要校验的角色标识
     */
    @AliasFor(annotation = SaCheckRole.class)
    String [] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     * @return 验证模式
     */
    @AliasFor(annotation = SaCheckRole.class)
    SaMode mode() default SaMode.AND;

}

四、sa-token基本组件
1、StpUtil
sa-token官方提供的工具类,提供了许多静态方法来帮助开发者快速地完成登录认证、权限校验等工作。例如,当用户登录时,可以通过 StpUtil.login(10001) 方法来标记当前会话已登录,并将账号ID写入会话中。设置当前用户信息缓存,可在之后的请求中获取登录用户信息等。具体实现还是依靠StpLogic完成的。

2、StpLogic
是 Sa-Token 框架的核心逻辑处理类,它封装了与认证和授权相关的具体实现。例如,当调用 StpUtil.logout() 方法时,实际上是调用了 StpLogic 的 logout 方法来处理会话注销的操作。此外,StpLogic 也用于实现登录、会话管理等核心逻辑。

3、StpInterface
是一个接口,它用于自定义权限认证的逻辑。开发者可以通过实现这个接口来提供自己的业务逻辑,比如实现 getPermissionList 方法来获取一个账号的所有权限列表,或者实现 getRoleList 方法来获取一个账号的角色列表。当 Sa-Token 需要校验权限时,它会调用这个接口的方法来获取相应的信息。

4、SaManager
一个管理类,它负责管理 Sa-Token 框架的各种全局组件,如全局配置 SaTokenConfig、持久化处理 SaTokenDao、权限认证 StpInterface、框架行为 SaTokenAction、上下文处理器 SaTokenContext 与 SaTokenSecondContext、认证活动监听 SaTokenListener、临时令牌验证 SaTempInterface、认证处理逻辑 StpLogic 等等。通过 SaManager,开发者可以方便地获取和管理这些组件。

5、SaStrategy
是一个策略类,它提供了一组策略方法,允许开发者自定义框架内部的一些行为。例如,你可以重写createToken方法来改变创建Token的策略,或者重写checkMethodAnnotation方法来改变如何校验一个Method 对象上的注解。

6、关联性
这些组件之间通过 SaManager 和 StpUtil 等工具类相互关联起来。例如,StpUtil 中的很多方法实际上都是调用了 StpLogic 的相应方法来实现其功能。而 StpLogic 在执行一些核心逻辑时,可能会调用 SaManager 中的组件,比如 SaTokenDao 来进行数据的持久化操作,或者调用 StpInterface 来获取权限信息。SaStrategy 则提供了一种机制,允许开发者在不改变 Sa-Token 框架核心逻辑的情况下调整某些行为。

7、流程图示例
StpUtil完成创建token,登录,登出等操作,具体通过StpLogic完成;
SaManager负责管理所有的Sa-token组件(StpLogic,StpInteface等…)
StpLogic负责实现所有的业务,会通过SaManager获取其他组件协调完成。
StpInterface给用户实现,提供具体的获取权限,角色等信息
SaStrategy指定策略,如注解合并等。
在这里插入图片描述

学海无涯苦作舟!!!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;