一、SaInterceptor 注解鉴权和路由拦截鉴权
拦截器:SaInterceptor
实现类位置: cn.dev33.satoken.interceptor.SaInterceptor
功能:Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力
/** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 * @param auth 认证函数,每次请求执行 */ public SaInterceptor(SaParamFunction<Object> auth) { this.auth = auth; }
SaInterceptor 有参构造的参数,SaParamFunction:
单形参、无返回值的函数式接口,方便开发者进行 lambda 表达式风格调用,如下@FunctionalInterface 标注
@FunctionalInterface public interface SaParamFunction<T> { /** * 执行的方法 * @param r 传入的参数 */ void run(T r); }
/**
* Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力
*
* @author click33
* @since 1.31.0
*/
public class SaInterceptor implements HandlerInterceptor {
/**
* 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权
*/
public boolean isAnnotation = true;
/**
* 认证函数:每次请求执行
* <p> 参数:路由处理函数指针
*/
public SaParamFunction<Object> auth = handler -> {};
/**
* 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力
*/
public SaInterceptor() {
}
/**
* 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力
* @param auth 认证函数,每次请求执行
*/
public SaInterceptor(SaParamFunction<Object> auth) {
this.auth = auth;
}
/**
* 设置是否打开注解鉴权:配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权
* @param isAnnotation /
* @return 对象自身
*/
public SaInterceptor isAnnotation(boolean isAnnotation) {
this.isAnnotation = isAnnotation;
return this;
}
/**
* 写入 [ 认证函数 ]: 每次请求执行
* @param auth /
* @return 对象自身
*/
public SaInterceptor setAuth(SaParamFunction<Object> auth) {
this.auth = auth;
return this;
}
// ----------------- 验证方法 -----------------
/**
* 每次请求之前触发的方法
*/
@Override
@SuppressWarnings("all")
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
try {
// 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权
if(isAnnotation && handler instanceof HandlerMethod) {
// 获取此请求对应的 Method 处理函数
Method method = ((HandlerMethod) handler).getMethod();
// 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权
if(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) {
// 注意这里直接就退出整个鉴权了,最底部的 auth.run() 路由拦截鉴权也被跳出了
return true;
}
// 执行注解鉴权
SaStrategy.instance.checkMethodAnnotation.accept(method);
}
// Auth 路由拦截鉴权校验
auth.run(handler);
} catch (StopMatchException e) {
// StopMatchException 异常代表:停止匹配,进入Controller
} catch (BackResultException e) {
// BackResultException 异常代表:停止匹配,向前端输出结果
// 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json
// 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
if(response.getContentType() == null) {
response.setContentType("text/plain; charset=utf-8");
}
response.getWriter().print(e.getMessage());
return false;
}
// 通过验证
return true;
}
}
一、@SaIgnore
忽略认证:表示被修饰的方法或类无需进行注解认证和路由拦截认证。
请注意:此注解的忽略效果只针对 SaInterceptor拦截器 和 AOP注解鉴权 生效,对自定义拦截器与过滤器不生效。拦截器前置处理逻辑:(对上面SaInterceptor 拦截器解读)
- 1- preHandle 前置方法中对SaIgnore注解进行了判断,如果标注就忽略鉴权。
// 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权 if(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) { // 注意这里直接就退出整个鉴权了,最底部的 auth.run() 路由拦截鉴权也被跳出了 return true; }
- 2- 执行注解鉴权,校验其他注解,
// 拦截器执行注解鉴权代码,主要是调用策略类SaStrategy (是Sa-Token 策略对象),checkMethodAnnotation 方法SaStrategy.instance.checkMethodAnnotation.accept(method);
checkMethodAnnotation:
/** * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) */ public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { // 先校验 Method 所属 Class 上的注解 instance.checkElementAnnotation.accept(method.getDeclaringClass()); // 再校验 Method 上的注解 instance.checkElementAnnotation.accept(method); };
checkElementAnnotation:
· /** * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) */ public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { // 校验 @SaCheckLogin 注解 SaCheckLogin checkLogin = (SaCheckLogin) SaStrategy.instance.getAnnotation.apply(element, SaCheckLogin.class); if(checkLogin != null) { SaManager.getStpLogic(checkLogin.type(), false).checkByAnnotation(checkLogin); } // 校验 @SaCheckRole 注解 。。。。。省略其他
3- 执行SoToken拦截器
Auth 路由拦截鉴权校验: auth.run(handler); 实际执行是:SaTokenConfig 配置类中的 handler 处理,即如下public void addInterceptors(InterceptorRegistry registry) { // 注册路由拦截器,自定义验证规则 registry.addInterceptor(new SaInterceptor(new SaParamFunction<Object>() { @Override public void run(Object handler) { AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class); // 登录验证 -- 排除多个路径 SaRouter // 获取所有的 .match(allUrlHandler.getUrls()) // 对未排除的路径进行检查 .check(() -> { // 检查是否登录 是否有token StpUtil.checkLogin(); }); } })).addPathPatterns("/**") // 排除不需要拦截的路径 .excludePathPatterns(securityProperties.getExcludes()); }
拦截器的实现类:
二、@SaCheckLogin
上面 preHandle 方法中,
// 执行注解鉴权 SaStrategy.instance.checkMethodAnnotation.accept(method);
点击进去 checkMethodAnnotation:
/** * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现) */ public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> { // 先校验 Method 所属 Class 上的注解 instance.checkElementAnnotation.accept(method.getDeclaringClass()); // 再校验 Method 上的注解 instance.checkElementAnnotation.accept(method); };
调用 checkElementAnnotation 进行 SaCheckLogin注解的校验:
1-先从元素上获取注解
2- SaManager.getStpLogic(checkLogin.type(), false) 获取到StpLogic,进行 SaCheckLogin 注解的登录鉴权
Sa-Token :权限认证,逻辑实现类(所有SoToken注解认证都在这边实现)
/** * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现) */ public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> { // 校验 @SaCheckLogin 注解 SaCheckLogin checkLogin = (SaCheckLogin) SaStrategy.instance.getAnnotation.apply(element, SaCheckLogin.class); if(checkLogin != null) { SaManager.getStpLogic(checkLogin.type(), false).checkByAnnotation(checkLogin); } 。。。。。省略下文
三、@SaCheckRole @SaCheckPermission
功能:
- SaCheckRole :校验角色注解
- SaCheckPermission :权限校验注解
下面用 SaCheckRole 来进行解读:
策略类中 SaManager.getStpLogic(checkLogin.type(), false) 返回 StpLogic 对象
调用了checkByAnnotation 方法,进行匹配角色:如下
SaManager.getStpLogic(checkLogin.type(), false).checkByAnnotation(checkLogin);
/**
* 根据注解 ( @SaCheckRole ) 鉴权
*
* @param at 注解对象
*/
public void checkByAnnotation(SaCheckRole at) {
String[] roleArray = at.value();
//1-满足所有角色才能校验通过
if(at.mode() == SaMode.AND) {
this.checkRoleAnd(roleArray);
} else {
//2- 满足任意一个角色就能通过
this.checkRoleOr(roleArray);
}
}
在 上面代码 checkRoleAnd 和 checkRoleOr 中都调用了
getRoleList 方法,该方法是用于查询角色权限其查询权限的接口有2个实现类
其中 SaPermissionImpl 实现类,是我这在SaToken配置类中配置
/**
* 权限接口实现(使用bean注入方便用户替换)
*/
@Bean
public StpInterface stpInterface() {
return new SaPermissionImpl();
}
实现了:
菜单权限查询方法getPermissionList
角色权限查询方法getRoleList
public class SaPermissionImpl implements StpInterface {
/**
* 获取菜单权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
UserType userType = UserType.getUserType(loginUser.getUserType());
if (userType == UserType.SYS_USER) {
return new ArrayList<>(loginUser.getMenuPermission());
} else if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
return new ArrayList<>();
}
/**
* 获取角色权限列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
LoginUser loginUser = LoginHelper.getLoginUser();
UserType userType = UserType.getUserType(loginUser.getUserType());
if (userType == UserType.SYS_USER) {
return new ArrayList<>(loginUser.getRolePermission());
} else if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
return new ArrayList<>();
}
}
四、@SaCheckSafe
作用:二级认证(基于redis实现)
使用场景:
比如代码托管平台的仓库删除操作,尽管我们已经登录了账号,当我们点击 [删除] 按钮时,还是需要再次输入一遍密码,这么做主要为了两点:
- 保证操作者是当前账号本人。
- 增加操作步骤,防止误删除重要数据。
SaToken 关于二级认证的文档如下:(很详细)二级认证 (sa-token.cc)https://sa-token.cc/doc.html#/up/safe-auth
简单使用方法:
- 先调用StpUtil.openSafe("add", 600),在redis 缓存中保存数据。
- 之后在方法上面标注@RequestMapping("add") 注解即可开启
其代码实现逻辑如下注释:
/**
* 1-校验:检查当前会话是否已通过指定业务的二级认证,如未通过则抛出异常
*
* @param service 业务标识
*/
public void checkSafe(String service) {
//获取token
String tokenValue = getTokenValue();
//判断token是否有效,否则抛出异常
if ( ! isSafe(tokenValue, service)) {
throw new NotSafeException(loginType, tokenValue, service).setCode(SaErrorCode.CODE_11071);
}
}
/**
* 2-判断:指定 token 是否处于二级认证时间内
*
* @param tokenValue Token 值
* @param service 业务标识
* @return true=二级认证已通过, false=尚未进行二级认证或认证已超时
*/
public boolean isSafe(String tokenValue, String service) {
// 1、如果提供的 Token 为空,则直接视为未认证
if(SaFoxUtil.isEmpty(tokenValue)) {
return false;
}
// 2、如果缓存中可以查询出指定的键值,则代表已认证,否则视为未认证
//splicingKeySafe(tokenValue, service) 返回如下:
// 格式:<Token名称>:<账号类型>:<safe>:<业务标识>:<Token值>
// 形如:satoken:login:safe:important:gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__
String value = getSaTokenDao().get(splicingKeySafe(tokenValue, service));
return !(SaFoxUtil.isEmpty(value));
}
五、@SaCheckSafe
功能: 账号封禁(基于redis实现)
场景:
- 账号封禁
- 分类封禁 (封禁部分功能)
- 阶梯封禁 (比如按照时间长短封禁、按照等级 1234封禁)
- 使用注解封禁,如下:
// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法 @SaCheckDisable("comment") // 校验当前账号是否被封禁 comment、place-order、open-shop 等服务,指定多个值,只要有一个已被封禁,就无法进入方法 @SaCheckDisable({"comment", "place-order", "open-shop"}) // 阶梯封禁,校验当前账号封禁等级是否达到5级,如果达到则抛出异常 @SaCheckDisable(level = 5) // 分类封禁 + 阶梯封禁 校验:校验当前账号的 comment 服务,封禁等级是否达到5级,如果达到则抛出异常 @SaCheckDisable(value = "comment", level = 5)
API:账号封禁 (sa-token.cc)https://sa-token.cc/doc.html#/up/disable
代码逻辑:
/**
* 校验:指定账号的指定服务,是否已被封禁到指定等级(如果已经达到,则抛出异常)
*
* @param loginId 指定账号id
* @param service 指定封禁服务
* @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常)
*/
public void checkDisableLevel(Object loginId, String service, int level) {
// 1、先前置检查一下这个账号是否被封禁了
String value = getSaTokenDao().get(splicingKeyDisable(loginId, service));
if(SaFoxUtil.isEmpty(value)) {
return;
}
// 2、再判断被封禁的等级是否达到了指定级别
Integer disableLevel = SaFoxUtil.getValueByType(value, int.class);
if(disableLevel >= level) {
throw new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service))
.setCode(SaErrorCode.CODE_11061);
}
}