sa-token之@SaIgnore注解失效的真正原因及正确姿势


1.前言

由于之前在项目中使用了sa-token,在使用的过程中发现@SaIgnore注解失效,之前以为是aop切面失效了,不知道失效的真正原因,那本文分分享其失效的真正原因及真确姿势,之前分享文章导读:

https://mp.weixin.qq.com/s/SREjXoyL9s1JfddQnU38yA
https://blog.csdn.net/qq_34905631/article/details/137821489?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522391dd644a9de17d7bb113aeb20931c55%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=391dd644a9de17d7bb113aeb20931c55&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-3-137821489-null-null.nonecase&utm_term=sa-token&spm=1018.2226.3001.4450
https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg
https://blog.csdn.net/qq_34905631/article/details/140233780?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522391dd644a9de17d7bb113aeb20931c55%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=391dd644a9de17d7bb113aeb20931c55&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-140233780-null-null.nonecase&utm_term=sa-token&spm=1018.2226.3001.4450
https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg
https://blog.csdn.net/qq_34905631/article/details/140277487?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522391dd644a9de17d7bb113aeb20931c55%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=391dd644a9de17d7bb113aeb20931c55&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-2-140277487-null-null.nonecase&utm_term=sa-token&spm=1018.2226.3001.4450

2.原因

2.1原理

首先需要了解Filter、Interceptor、AOP及MVC的原理及调用先后顺序,看上面的几张图可以说是一下子豁然开朗了。

2.2sa-token相关源码

2.2.1 SaServletFilter

该类在:sa-token-core包中,此包只要引入了sa-token的相关包,会自动引入

/* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache LicenseVersion 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package cn.dev33.satoken.filter;
import cn.dev33.satoken.error.SaSpringBootErrorCode;import cn.dev33.satoken.exception.BackResultException;import cn.dev33.satoken.exception.SaTokenException;import cn.dev33.satoken.exception.StopMatchException;import cn.dev33.satoken.router.SaRouter;import cn.dev33.satoken.util.SaTokenConsts;import org.springframework.core.annotation.Order;
import javax.servlet.*;import java.io.IOException;import java.util.ArrayList;import java.util.Arrays;import java.util.List;
/** * Servlet 全局鉴权过滤器 *

 *     默认优先级为 -100,尽量保证在其它过滤器之前执行 * </p> * * @author click33 * @since 1.19.0 */@Order(SaTokenConsts.ASSEMBLY_ORDER)public class SaServletFilter implements SaFilterFilter {
// ------------------------ 设置此过滤器 拦截 & 放行 的路由 
/** * 拦截路由  */ public List includeList = new ArrayList<>();
/** * 放行路由  */ public List excludeList = new ArrayList<>();
@Override public SaServletFilter addInclude(String... paths) { includeList.addAll(Arrays.asList(paths)); return this; }
@Override public SaServletFilter addExclude(String... paths) { excludeList.addAll(Arrays.asList(paths)); return this; }
@Override public SaServletFilter setIncludeList(List pathList) { includeList = pathList; return this; }
@Override public SaServletFilter setExcludeList(List pathList) { excludeList = pathList; return this; }

// ------------------------ 钩子函数
/** * 认证函数:每次请求执行  */ public SaFilterAuthStrategy auth = r -> {};
/** * 异常处理函数:每次[认证函数]发生异常时执行此函数 */ public SaFilterErrorStrategy error = e -> { throw new SaTokenException(e).setCode(SaSpringBootErrorCode.CODE_20105); };
/** * 前置函数:在每次[认证函数]之前执行 *      注意点:前置认证函数将不受 includeList 与 excludeList 的限制,所有路由的请求都会进入 beforeAuth</b> */ public SaFilterAuthStrategy beforeAuth = r -> {};
@Override public SaServletFilter setAuth(SaFilterAuthStrategy auth) { this.auth = auth; return this; }
@Override public SaServletFilter setError(SaFilterErrorStrategy error) { this.error = error; return this; }
@Override public SaServletFilter setBeforeAuth(SaFilterAuthStrategy beforeAuth) { this.beforeAuth = beforeAuth; return this; }

// ------------------------ doFilter
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try { // 执行全局过滤器 beforeAuth.run(null); SaRouter.match(includeList).notMatch(excludeList).check(r -> { auth.run(null); });
} catch (StopMatchException e) { // StopMatchException 异常代表:停止匹配,进入Controller
} catch (Throwable e) { // 1. 获取异常处理策略结果  String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));
// 2. 写入输出流 // 请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json // 例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8"); if(response.getContentType() == null) { response.setContentType(SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); } response.getWriter().print(result); return; }
// 执行  chain.doFilter(request, response); }
@Override public void init(FilterConfig filterConfig) { }
@Override public void destroy() { }


}


2.2.2 SaInterceptor

该类在sa-tonekn-spring-boot-starter依赖中

/* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package cn.dev33.satoken.interceptor;
import cn.dev33.satoken.annotation.SaIgnore;import cn.dev33.satoken.exception.BackResultException;import cn.dev33.satoken.exception.StopMatchException;import cn.dev33.satoken.fun.SaParamFunction;import cn.dev33.satoken.strategy.SaStrategy;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.lang.reflect.Method;
/** * Sa-Token 综合拦截器,提供注解鉴权和路由拦截鉴权能力  *  * @author click33 * @since 1.31.0 */public class SaInterceptor implements HandlerInterceptor {
/** * 是否打开注解鉴权,配置为 true 时注解鉴权才会生效,配置为 false 时,即使写了注解也不会进行鉴权 */ public boolean isAnnotation = true;
/** * 认证函数:每次请求执行  *

参数:路由处理函数指针  */ public SaParamFunction auth = handler -> {};
/** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力  */ public SaInterceptor() { }
/** * 创建一个 Sa-Token 综合拦截器,默认带有注解鉴权能力 @param auth 认证函数,每次请求执行  */ public SaInterceptor(SaParamFunction 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 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; }
}

2.2.3SaCheckAspect

该类是在sa-token-spring-aop包中,此包需要单独引入

/* * Copyright 2020-2099 sa-token.cc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package cn.dev33.satoken.aop;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;
import cn.dev33.satoken.annotation.SaIgnore;import cn.dev33.satoken.strategy.SaStrategy;import cn.dev33.satoken.util.SaTokenConsts;
/** * Sa-Token 基于 Spring Aop 的注解鉴权 * *

 *     注意:在打开 注解鉴权 时,AOP 模式与拦截器模式不可同时使用,否则会出现在 Controller 层重复鉴权两次的问题 *

 *  * @author click33 * @since 1.19.0 */@Aspect@Component@Order(SaTokenConsts.ASSEMBLY_ORDER)public class SaCheckAspect {
/** * 构建 */ public SaCheckAspect() { }
/** * 定义AOP签名 (切入所有使用 Sa-Token 鉴权注解的方法) */ public static final String POINTCUT_SIGN =  "@within(cn.dev33.satoken.annotation.SaCheckLogin) || @annotation(cn.dev33.satoken.annotation.SaCheckLogin) || ""@within(cn.dev33.satoken.annotation.SaCheckRole) || @annotation(cn.dev33.satoken.annotation.SaCheckRole) || ""@within(cn.dev33.satoken.annotation.SaCheckPermission) || @annotation(cn.dev33.satoken.annotation.SaCheckPermission) || ""@within(cn.dev33.satoken.annotation.SaCheckSafe) || @annotation(cn.dev33.satoken.annotation.SaCheckSafe) || ""@within(cn.dev33.satoken.annotation.SaCheckDisable) || @annotation(cn.dev33.satoken.annotation.SaCheckDisable) || ""@within(cn.dev33.satoken.annotation.SaCheckBasic) || @annotation(cn.dev33.satoken.annotation.SaCheckBasic)";
/** * 声明AOP签名 */ @Pointcut(POINTCUT_SIGN) public void pointcut() { }
/** * 环绕切入@param joinPoint 切面对象@return 底层方法执行后的返回值@throws Throwable 底层方法抛出的异常 */ @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取对应的 Method 处理函数  MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod();
// 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权  if(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) { // ...  } else { // 注解鉴权  SaStrategy.instance.checkMethodAnnotation.accept(method); }
// 执行原有逻辑 return joinPoint.proceed(); }
}

2.2.4 官方文档地址

https://sa-token.cc/v/v1.36.0/doc.html#/plugin/aop-athttps://sa-token.cc/v/v1.36.0/doc.html#/up/global-filterhttps://sa-token.cc/v/v1.36.0/doc.html#/use/route-check

2.2.5小结

由此可见,Filter是责任链,Interceptor是java中的jdk接口动态代理,而sa-token中的@SaIgnore注解使用的是AspectJ的切面动态代理,执行顺序是Filter-->Interceptor(controller前、中、后)-->AspectJ切面代理-->controller-->Interceptor-->Filter。


3.正确姿势

正确姿势中是所有请求都要校验是否登录,可以在SaServletFilter或SaInterceptor排除登录校验,或者使用sa-token的@SaIgnore注解.

正确姿势有三种:

3.1.只使用SaServletFilter需要额外写代码实现

3.2使用SaServletFilter和SaInterceptor

3.3使用sa-token的注解

package xxxx.xxxx.xxxx.config;
import cn.dev33.satoken.SaManager;import cn.dev33.satoken.annotation.SaIgnore;import cn.dev33.satoken.context.SaHolder;import cn.dev33.satoken.context.model.SaRequest;import cn.dev33.satoken.exception.SaTokenException;import cn.dev33.satoken.filter.SaServletFilter;import cn.dev33.satoken.interceptor.SaInterceptor;import cn.dev33.satoken.jwt.StpLogicJwtForSimple;import cn.dev33.satoken.router.SaHttpMethod;import cn.dev33.satoken.router.SaRouter;import cn.dev33.satoken.router.SaRouterStaff;import cn.dev33.satoken.stp.StpLogic;import cn.dev33.satoken.stp.StpUtil;import com.alibaba.fastjson.JSON;import xxxx.xxx.xxx.dto.CustomError;import xxx.xxxx.xxx.i.response.RestResponse;import lombok.extern.slf4j.Slf4j;import org.apache.commons.collections4.CollectionUtils;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletResponse;import java.util.HashSet;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.Set;import java.util.stream.Collectors;
@Slf4j@Configurationpublic class SaTokenConfigure implements WebMvcConfigurerApplicationContextAware {
    private ApplicationContext applicationContext;
    private static Set saIgnoreUrls = new HashSet<>();

    // Sa-Token 整合 jwt (Simple 简单模式)    @Bean    public StpLogic getStpLogicJwt() {        return new StpLogicJwtForSimple();    }
    // 注册拦截器    @Override    public void addInterceptors(InterceptorRegistry registry) {        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。        //这里有登录校验,在controller上的登录方法就不要使用@SaCheckLogin注解,否则会有两次权限校验的        //这个官方文档也有说明的:https://sa-token.cc/v/v1.36.0/doc.html#/plugin/aop-at        /**         * 注意点:         * 使用拦截器模式,只能把注解写在Controller层,使用AOP模式,可以将注解写在任意层级         * 拦截器模式和AOP模式不可同时集成,否则会在Controller层发生一个注解校验两次的bug         */        //方式二:在方式一里面排除的路径需要在这里在写一遍,或者删除方式一的排除路径,直接写在方式二这里        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()) //方式三需要注释这里,然后controller的方法上使用sa-token的注解                               )                .addPathPatterns("/**")                //排除路径                .excludePathPatterns("/admin/**").excludePathPatterns("/payRefund/**");    }
    /**     * 注册 [Sa-Token 全局过滤器]     */    @Bean    public SaServletFilter getSaServletFilter() {        //方式一(方式二这里需要注释) //方式三需要注释这里,然后controller的方法上使用sa-token的注解        this.saIgnoreUrls();        return new SaServletFilter()                // 指定 [拦截路由] 与 [放行路由]                .addInclude("/**")                // 登录认证 -- 拦截所有路由,并排除/user/login 用于开放登录                .addExclude("/admin/**")                .addExclude("/actuator/**")                .addExclude("/favicon.ico")                .addExclude("*.js")                .addExclude("*.css")                // 认证函数: 每次请求执行                .setAuth(obj -> {                    log.info("getSaServletFilter.setAuth.saIgnoreUrls:{}", JSON.toJSONString(saIgnoreUrls));                    SaRouterStaff saRouterStaff = SaRouter.match("/**");  // 拦截的 path 列表,可以写多个 */                    //方式一(方式二这里需要注释) //方式三需要注释这里,然后controller的方法上使用sa-token的注解                    if (CollectionUtils.isNotEmpty(saIgnoreUrls)) {                        saRouterStaff.notMatch(saIgnoreUrls.stream().collect(Collectors.toList()));                    }                    SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());                    // ...                    // 排除掉的 path 列表,可以写多个                    //方式一(方式二这里需要注释) //方式三需要注释这里,然后controller的方法上使用sa-token的注解                    saRouterStaff.check(r -> StpUtil.checkLogin());// 要执行的校验动作,可以写完整的 lambda 表达式
                    // 根据路由划分模块,不同模块不同鉴权                    // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */                })
                // 异常处理函数:每次认证函数发生异常时执行此函数                .setError(e1 -> {                    log.error("sa-token异常:{}", e1.getMessage());                    // 设置响应头                    SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");                    /**                     * sa-token登录相关异常处理                     * https://sa-token.cc/v/v1.36.0/doc.html#/fun/exception-code                     */                    if (e1 instanceof SaTokenException) {                        SaTokenException e = (SaTokenException) e1;                        // 根据不同异常细分状态码返回不同的提示                        if (e.getCode() == 11001 || e.getCode() == 11011) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "未能读取到有效Token");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11002) {                            return JSON.toJSONString(RestResponse.fail("登录时的账号为空"));                        }                        if (e.getCode() == 11012) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "Token无效");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11013) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "Token已过期");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11014) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "Token已被顶下线");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11015) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "Token已被踢下线");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11016) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "Token已被冻结");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11017) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "未按照指定前缀提交token");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 11041) {                            return JSON.toJSONString(RestResponse.fail("缺少指定的角色"));                        }                        if (e.getCode() == 11051) {                            return JSON.toJSONString(RestResponse.fail("缺少指定的权限"));                        }                        if (e.getCode() == 11061) {                            return JSON.toJSONString(RestResponse.fail("当前账号未通过服务封禁校验"));                        }                        if (e.getCode() == 11062) {                            return JSON.toJSONString(RestResponse.fail("提供要解禁的账号无效"));                        }                        if (e.getCode() == 12001) {                            return JSON.toJSONString(RestResponse.fail("请求中缺少指定的参数"));                        }                        if (e.getCode() == 12111) {                            return JSON.toJSONString(RestResponse.fail("密码md5加密异常"));                        }                        if (e.getCode() == 30201) {                            return JSON.toJSONString(RestResponse.fail("对jwt字符串解析失败"));                        }                        if (e.getCode() == 30202) {                            return JSON.toJSONString(RestResponse.fail("此jwt的签名无效"));                        }                        if (e.getCode() == 30203) {                            return JSON.toJSONString(RestResponse.fail("此jwt的loginType字段不符合预期"));                        }                        if (e.getCode() == 30204) {                            CustomError customError = new CustomError(String.valueOf(HttpServletResponse.SC_UNAUTHORIZED), "此jwt已超时");                            return JSON.toJSONString(RestResponse.fail(customError));                        }                        if (e.getCode() == 30205) {                            return JSON.toJSONString(RestResponse.fail("没有配置jwt秘钥"));                        }                        if (e.getCode() == 30206) {                            return JSON.toJSONString(RestResponse.fail("登录时提供的账号为空"));                        }                        // 更多 code 码判断 ...                        // 默认的提示                        return JSON.toJSONString(RestResponse.fail("sa-token异常,请联系管理员处理..."));                    }                    String message = e1.getMessage();                    if (StringUtils.isNotEmpty(message)) {                        return JSON.toJSONString(RestResponse.fail(message));                    }                    return JSON.toJSONString(RestResponse.fail("系统异常,请联系管理员处理..."));                })
                // 前置函数:在每次认证函数之前执行                .setBeforeAuth(obj -> {                    // 获得客户端domain                    SaRequest request = SaHolder.getRequest();                    String origin = request.getHeader("Origin");                    if (origin == null) {                        origin = request.getHeader("Referer");                    }                    // ---------- 设置一些安全响应头 ----------                    SaHolder.getResponse()                            // 服务器名称                            //.setServer("sa-server")                            // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以                            .setHeader("X-Frame-Options", "SAMEORIGIN")                            // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面                            .setHeader("X-XSS-Protection", "1; mode=block")                            // 禁用浏览器内容嗅探                            .setHeader("X-Content-Type-Options", "nosniff")                            // ---------- 设置跨域响应头 ----------                            // 允许指定域访问跨域资源                            .setHeader("Access-Control-Allow-Origin", origin)                            // 允许所有请求方式                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEAD,PUT")                            // 允许的header参数                            .setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With,satoken")                            .setHeader("Access-Control-Allow-Credentials", "true")                            // 有效时间                            .setHeader("Access-Control-Max-Age", "3600");
                    // 如果是预检请求,则立即返回到前端                    SaRouter.match(SaHttpMethod.OPTIONS)                            .free(r -> {                                log.info("--------OPTIONS预检请求,不做处理,直接返回响应状态码为200");                                SaHolder.getResponse().setStatus(HttpServletResponse.SC_OK);                            })                            .back();                });    }
    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        this.applicationContext = applicationContext;    }
    public void saIgnoreUrls() {        RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);        Map handlerMethods = requestMappingHandlerMapping.getHandlerMethods();        for (Map.Entry entry : handlerMethods.entrySet()) {            HandlerMethod handlerMethod = entry.getValue();            SaIgnore classSaIgnore = handlerMethod.getBeanType().getAnnotation(SaIgnore.class);            SaIgnore methodSaIgnore = handlerMethod.getMethodAnnotation(SaIgnore.class);            Set patterns = entry.getKey().getPatternsCondition().getPatterns();            if (CollectionUtils.isNotEmpty(patterns)) {                if (Objects.nonNull(classSaIgnore)) {                    List classMethodUrls = patterns.stream().collect(Collectors.toList());                    log.info("saIgnoreUrls.classMethodUrls:{}", JSON.toJSONString(classMethodUrls));                    if (CollectionUtils.isNotEmpty(classMethodUrls)) {                        saIgnoreUrls.addAll(classMethodUrls);                    }                } else if (Objects.nonNull(methodSaIgnore)) {                    String methodUrl = patterns.stream().findFirst().orElse(null);                    log.info("saIgnoreUrls.methodUrl:{}", methodUrl);                    if (StringUtils.isNotEmpty(methodUrl)) {                        saIgnoreUrls.add(methodUrl);                    }                }            }        }    }
}

3.4说明

方式一:在controller上或者controller的方法上使用@SaIgnore或把controller类上的所有方法url解析排除或者标记的某些controller类的方法url解析排除

方式二:在没有关闭注解校验注解校验是开启的

此时在controller的方法上写了@SaIgnore注解,SaCheckAspect切面也不会进,原因是在SaInterceptor的preHandle方法这种已经让@SaIgnore注解标记调用的方法忽略了校验的:

       /**	 * 每次请求之前触发的方法 	 */	@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; }

方式三:

方式一不校验,可以在使用了方式二的校验在controller上加上

@SaIgnore@SaCheckLogin

这种或略逻辑虽然会走两次也不影响,优先走的是@SaIgnore的或略逻辑,其它权限逻辑校验是会走两次的,这种可能会有影响。

要想sa-token的注解权限生效必须关闭SaInterceptor的注解校验,SaCheckAspect切面才会进。

关闭SaInterceptor的注解校验:

@Overridepublic void addInterceptors(InterceptorRegistry registry) {    registry.addInterceptor(        new SaInterceptor(handle -> {            SaRouter.match("/**").check(r -> StpUtil.checkLogin());        }).isAnnotation(false)  // 指定关闭掉注解鉴权能力,这样框架就只会做路由拦截校验了     ).addPathPatterns("/**");}

4.总结

由于我在项目中自定义注解使用aspectJ切面实现接口调用日志记录(这个实现思路后面会写一篇文章分享,好用到飞起,纵享丝滑)的时候发现切面居然没有生效。然后等我处理好了之后,想起了之前搞的sa-token的@SaIgnore注解失效,于是我就在次去搞了一下@SaIgnore的源码,解决之后写了这篇文章,我会把这篇文章投稿给sa-token官方,挂在sa-token的官网,避坑指南,希望我的分享对你有所启发和帮助,请一键三连,么么么哒!