sa-token前后端分离解决跨域的正确姿势

1.前言

由于最近写了一个项目,前端使用vue,后端使用的是Java,集成国产sa-token(一个轻量级 java 权限认证框架,让鉴权变得简单、优雅!),采用前后端分离的方式开发和测试环境部署,在部署测试环境的时候前端静态资源编译打包放在阿里云的oss上,然后配置了一个域名加特定的首页路径跳转到访问到oss的前端静态资源登录首页,然后在点解登录的时候居然报跨域的了,于是乎,经过一番各种姿势的尝试,最终才解决了,http加域名加特定路径访问首页登录接口的跨域问题,后端的服务是一个公司内网服务地址,搭建了vpn环境连接vpn网络才是通的,阿里oss上放前端静态资源,使用https加域名加特定路劲访问,这种方式一直会有跨域的问题,这个就先不用管了,出现这个跨域的问题原因是由于前端项目部署不是那么正规,就为了方便省事,关于如何集成sa-token请参看下面这篇文章:
https://mp.weixin.qq.com/s/SREjXoyL9s1JfddQnU38yAhttps://blog.csdn.net/qq_34905631/article/details/137821489?spm=1001.2014.3001.5501
sa-token官网提供的解决跨域的方法如下;
https://juejin.cn/post/7247376558367981627https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso2-client/src/main/java/com/pj/h5/CorsFilter.java
spring5.2.15.RELEASE官方配置cors跨域的官网方法如下:
https://docs.spring.io/spring-framework/docs/5.2.15.RELEASE/spring-framework-reference/web.html#mvc-cors-intro
还有就是要设置chrome浏览器(新版chrome器)关闭跨域检查:
https://blog.csdn.net/qq_45301392/article/details/12870356
上面的方式都只可以参考,但是实际上实践下来都失效了,配置了没有用,项目中使用的springBoot版本是:2.3.12.RELEASE,spring版本是5.2.15.RELEASE,这个跨域真是一个蛋疼的问题,网上各种文章看然后各种姿势去尝试,没有一个是可以的,最后经过各种尝试,终于找到了一个可行的姿势。



2.正确姿势

2.1SaTokenConfigure配置如下

package xxxx.config;import cn.dev33.satoken.SaManager;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.jwt.StpLogicJwtForSimple;import cn.dev33.satoken.router.SaHttpMethod;import cn.dev33.satoken.router.SaRouter;import cn.dev33.satoken.stp.StpLogic;import cn.dev33.satoken.stp.StpUtil;import xxxx.RestResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.servlet.http.HttpServletResponse;@Slf4j@Configurationpublic class SaTokenConfigure implements WebMvcConfigurer {    // Sa-Token 整合 jwt (Simple 简单模式)    @Bean    public StpLogic getStpLogicJwt() {        return new StpLogicJwtForSimple();    }    /**     * 注册 [Sa-Token 全局过滤器]     */    @Bean    public SaServletFilter getSaServletFilter() {        return new SaServletFilter()                // 指定 [拦截路由] 与 [放行路由]                .addInclude("/**")                // 登录认证 -- 拦截所有路由,并排除/user/login 用于开放登录                .addExclude("/admin/**")                .addExclude("/favicon.ico")                .addExclude("*.js")                .addExclude("*.css")                // 认证函数: 每次请求执行                .setAuth(obj -> {                    SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());                    // ...                    SaRouter.match("/**")    // 拦截的 path 列表,可以写多个 */                            // 排除掉的 path 列表,可以写多个                            .check(r -> StpUtil.checkLogin());// 要执行的校验动作,可以写完整的 lambda 表达式                    // 根据路由划分模块,不同模块不同鉴权                    //内部系统的登录权限                    //SaRouter.match("/inner/cam/**", r -> StpUtil.checkPermission("inner.cam"));                    //SaRouter.match("/inner/cds/**", r -> StpUtil.checkPermission("inner.cds"));                    //SaRouter.match("/inner/cim/**", r -> StpUtil.checkPermission("inner.cim"));                    //SaRouter.match("/inner/ads/**", r -> StpUtil.checkPermission("inner.ads"));                    //SaRouter.match("/inner/rs/**", r -> StpUtil.checkPermission("inner.rs"));                    //SaRouter.match("/inner/clw/**", r -> StpUtil.checkPermission("inner.clw"));                    //外部系统登录权限                    //SaRouter.match("/out/wb/**", r -> StpUtil.checkPermission("out.wb"));                    //SaRouter.match("/out/cm/**", r -> StpUtil.checkPermission("out.cm"));                    //SaRouter.match("/out/cds/**", r -> StpUtil.checkPermission("out.cds"));                    //SaRouter.match("/out/rs/**", r -> StpUtil.checkPermission("out.rs"));                    //SaRouter.match("/out/ads/**", r -> StpUtil.checkPermission("out.ads"));                    // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */                })                // 异常处理函数:每次认证函数发生异常时执行此函数                .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) {                            return RestResponse.fail("未能读取到有效Token");                        }                        if (e.getCode() == 
11002) {                            return RestResponse.fail("登录时的账号为空");                        }                        if (e.getCode() == 
11011) {                            return RestResponse.fail("未能读取到有效Token");                        }                        if (e.getCode() == 
11012) {                            return RestResponse.fail("Token无效");                        }                        if (e.getCode() == 
11013) {                            return RestResponse.fail("Token已过期");                        }                        if (e.getCode() == 
11014) {                            return RestResponse.fail("Token已被顶下线");                        }                        if (e.getCode() == 
11015) {                            return RestResponse.fail("Token已被踢下线");                        }                        if (e.getCode() == 
11016) {                            return RestResponse.fail("Token已被冻结");                        }                        if (e.getCode() == 
11017) {                            return RestResponse.fail("未按照指定前缀提交token");                        }                        if (e.getCode() == 
11041) {                            return RestResponse.fail("缺少指定的角色");                        }                        if (e.getCode() == 
11051) {                            return RestResponse.fail("缺少指定的权限");                        }                        if (e.getCode() == 
11061) {                            return RestResponse.fail("当前账号未通过服务封禁校验");                        }                        if (e.getCode() == 
11062) {                            return RestResponse.fail("提供要解禁的账号无效");                        }                        if (e.getCode() == 
12001) {                            return RestResponse.fail("请求中缺少指定的参数");                        }                        if (e.getCode() == 
12111) {                            return RestResponse.fail("密码md5加密异常");                        }                        if (e.getCode() == 
30201) {                            return RestResponse.fail("对jwt字符串解析失败");                        }                        if (e.getCode() == 
30202) {                            return RestResponse.fail("此jwt的签名无效");                        }                        if (e.getCode() == 
30203) {                            return RestResponse.fail("此jwt的loginType字段不符合预期");                        }                        if (e.getCode() == 
30204) {                            return RestResponse.fail("此jwt已超时");                        }                        if (e.getCode() == 
30205) {                            return RestResponse.fail("没有配置jwt秘钥");                        }                        if (e.getCode() == 
30206) {                            return RestResponse.fail("登录时提供的账号为空");                        }                        // 更多 code 码判断 ...                        // 默认的提示                        return RestResponse.fail("登录异常,请联系管理员处理...");                    }                    return RestResponse.fail(e1.getMessage());                })                // 前置函数:在每次认证函数之前执行                .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();                });    }}

2.2SimpleCORSFilter跨域配置如下

package com.dy.corporate.member.config;import org.springframework.stereotype.Component;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Componentpublic class SimpleCORSFilter implements Filter {    @Override    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {        HttpServletResponse response = (HttpServletResponse) res;        HttpServletRequest request = (HttpServletRequest) req;        response.setHeader("Access-Control-Allow-Credentials", 
"true");        response.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) req).getHeader("origin"));        response.setHeader("Access-Control-Allow-Methods", 
"POST, GET, OPTIONS, DELETE, HEAD,PUT");        response.setHeader("Access-Control-Max-Age", 
"3600");        response.setHeader("Access-Control-Allow-Headers", 
"access-control-allow-origin, authority, content-type, version-info, X-Requested-With,satoken");        if ("OPTIONS".equals(request.getMethod())) {            response.setStatus(HttpServletResponse.SC_OK);            return;        }        chain.doFilter(req, res);    }    @Override    public void init(FilterConfig filterConfig) {    }    @Override    public void destroy() {    }}
对之前的集成sa-token实现登录和RBAC权限控制文章中的配置做一些更改和新增配置上上面这个两个类即可解决跨域问题。

3.总结

之前使用sa-token官网提供的跨域解决方案,两种都尝试了,后面很大一部分原因是因为没有仔细看两个方案有存在什么差异,Access-Control-Allow-Origin在chrome上浏览器报错不能设置为星号,还有就是携带satoken等请求头信息需要将Access-Control-Allow-Credentials设置为true,Access-Control-Allow-Methods这个允许的方法尽量写具体,不要写星号  Access-Control-Allow-Headers头要把satoken放进去,sa-token的token的名字默认是satoken,前缀是Bearer,请求头sa-token的格式是Bearer加一个空格加上一个xxxxxxxxxx(登录后生成给前端的token值),sa-token官方文档有详细的说明,可以在yaml中配置token名字等信息,具体的可以去官网看文档说明,或者是把sa-token项目的源码拉下来扒一扒你就可以找到答案,言归真转,我们继续说跨域问题的解决,还有一个就是OPTIONS预检测请求的响应需要响应200状态码,问题的关键就是那些写了星号的地方,没有写具体的值,这个的根本原因没有去探究为啥写了星号就失效了,这个要去看这个相关的源码才有答案,还有就是预检测请求响应需要返回200状态的code码,解决跨域的思路其实也很简单,利用的是Filter过滤器的原理,配置一个SimpleCORSFilter过滤器,还有一个sa-token的过滤器SaServletFilter,SaServletFilter的order是-100,SimpleCORSFilter的order是默认的没有设置,Filter的order值越小就越在前面执行,所以SimpleCORSFilter的order默认值比SaServletFilter的order值要小,所以SaServletFilter的order执行先于SaServletFilter执行,只要将这两个Filter的response的tHeader的相关参数两边都设置成一样的即可,下面是SaServletFilter的doFilter的源码,SaServletFilter的.setBeforeAuth()方法会在doFilter方法里面最先执行,所以只要将两边的response的Header头的参数设置保持一致,预检测请求响应也设置200保持一直就可以了。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {        try {            this.beforeAuth.run((Object)null);            SaRouter.match(this.includeList).notMatch(this.excludeList).check((r) -> {                this.auth.run((Object)null);            });        } 
catch (StopMatchException var6) {        } 
catch (Throwable var7) {            String result = var7 
instanceof BackResultException ? var7.getMessage() : 
String.valueOf(this.error.run(var7));            if (response.getContentType() == 
null) {                response.setContentType("text/plain; charset=utf-8");            }            response.getWriter().print(result);            return;        }        chain.doFilter(request, response);    }

之前参看spring5.2.15的mvc的官网的cors的配置三种方式都配置失效了,这个也只能是看mvc的源码去找答案了,上面那两个配置的写法是我在我们测试环境部署的时候产生了跨域问题,经过各种尝试之后,亲测是有效的正确的姿势,先解决了http加域名加特定路径在首页登录请求后端服务ip加端口产生的跨域问题,后面上生产后端是采用k8s部署服务,需要将后端部署的k8s服务暴露到外网,通过一个华为云的公网弹性ip,将请求转发到k8s内部的服务上,先在测试解决了跨域,先把测试搞完,至于后面上生产会有啥问题,到时候有问题在去解决相应的问题,如果后续上生产部署访问有啥问题,我会更新到文章的评论上,一般会发csdn的文章评论上; 还有就是sa-token的@SaIgnore注解失效了,之前我集成进去用的时候是可以的,估计是那个切面失效了,我后面自己也搞了一个组件,使用了aop的切面,也是注解的方式,我那个就可以,然后估计就导致sa-token的权限切面注解失效了,我估计是这个问题的,在我项目中@SaIgnore注解失效也不影响使用,@SaIgnore注解失效解决办法可以参看我之前分享的文章:“集成sa-token实现登录和RBAC权限控制”,上面有文章链接本次分享到此结束,希望我的分享对你有所启发和帮助,请一键三连,么么么哒!

请使用浏览器的分享功能分享到微信等