一、概述
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指定策略,如注解合并等。
学海无涯苦作舟!!!