有一段时间没有运行ttsx-backend这个项目了,打开的时候看了一下Sa-Token的相关配置,好像是使用的Sa-Token拦截器进行权限处理。
这个拦截器在使用的时候会有一点问题,在未登录状态下拦截请求时会抛出异常,提示Sa-Token上下文未初始化,于是又改成了通过过滤器进行认证处理。
后端代码
下面是配置Sa-Token过滤器的代码:都是根据官方文档进行配置的。
- 排除了匿名接口(使用了@SaIgnore注解的控制器接口)
- 查询非匿名接口信息,配置需要指定的权限才能访问
- Knife4j只有超管用户允许访问
- 自定义过滤器异常处理,根据异常类型分类处理
- 设置不同的HTTP响应状态码,并返回对应的提示
- 在过滤器中直接返回,阻止请求被继续处理
java
package cn.edu.sgu.www.ttsx.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import cn.dev33.satoken.filter.SaFilterAuthStrategy;
import cn.dev33.satoken.filter.SaFilterErrorStrategy;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.fun.SaFunction;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.edu.sgu.www.common.restful.JsonResult;
import cn.edu.sgu.www.common.restful.ResponseCode;
import cn.edu.sgu.www.pms.dubbo.PermissionDubboService;
import cn.edu.sgu.www.pms.entity.Permission;
import cn.edu.sgu.www.ttsx.config.property.CorsProperties;
import com.alibaba.fastjson2.JSON;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* Sa-Token配置类
* @author 沐雨橙风ιε
* @version 1.0
*/
@Configuration
public class SaTokenConfig {
/**
* Knife4j的资源
*/
private static final String[] knife4jUrls;
static {
knife4jUrls = new String[] {
"/doc.html",
"/v2/api-docs",
"/swagger-ui.html",
"/swagger-resources",
"/swagger-resources/configuration/ui",
"/swagger-resources/configuration/security"
};
}
@Value("${spring.application.name}")
private String service;
@DubboReference
private PermissionDubboService permissionDubboService;
/**
* 注册Sa-Token全局过滤器
*/
@Bean
public SaServletFilter saTokenFilter(CorsProperties corsProperties) {
// 查询匿名接口的url列表
List<String> list = permissionDubboService.selectAnonymityUrls(service);
// 不拦截knife4j的资源,否则Knife4j的页面将不可访问
list.addAll(Arrays.asList(knife4jUrls));
// 查询需要权限访问的资源
List<Permission> permissions = permissionDubboService.selectPermissions(service);
SaServletFilter saServletFilter = new SaServletFilter();
saServletFilter.addInclude("/**");
// 排除的路径:匿名访问接口
saServletFilter.addExclude(list.toArray(new String[] { }));
// 处理认证
saServletFilter.setAuth(new SaFilterAuthStrategy() {
@Override
public void run(Object obj) {
/*
* 需要权限访问的资源
*/
// 超管才能访问Knife4j的资源
SaRouter.match(knife4jUrls).check(new SaFunction() {
@Override
public void run() {
StpUtil.checkLogin();
// 已经登录才校验权限
if (StpUtil.isLogin()) {
// 超管才有权限查看接口文档
StpUtil.checkRole("ttsx:system-admin");
}
}
});
if (!permissions.isEmpty()) {
for (Permission permission : permissions) {
SaRouter.match(permission.getUrl(), new SaFunction() {
@Override
public void run() {
StpUtil.checkLogin();
StpUtil.checkPermission(permission.getValue());
}
});
}
}
}
});
// 处理认证异常
saServletFilter.setError(new SaFilterErrorStrategy() {
@Override
public Object run(Throwable throwable) {
// 响应状态码:403 Forbidden
ResponseCode responseCode = ResponseCode.FORBIDDEN;
// 构建响应数据
String message = null;
// 不同类型的异常区别处理
if (throwable instanceof NotLoginException) {
message = "请登录后访问!";
} else if (throwable instanceof NotRoleException || throwable instanceof NotPermissionException) {
message = "正在访问未授权的资源!";
// 设置响应状态码:401 Unauthorized
responseCode = ResponseCode.UNAUTHORIZED;
}
// 获取响应对象
SaResponse response = SaHolder.getResponse();
// 设置响应状态码
response.setStatus(responseCode.getCode());
JsonResult<Void> jsonResult = JsonResult.error(responseCode, message);
return JSON.toJSONString(jsonResult);
}
});
return saServletFilter;
}
}
但是,新的问题又出现了,原本在未登录状态下发起未授权的请求,会提示"请登陆后重试"的提示信息。而现实是操作无反应,打开浏览器控制台能看到错误信息。

很显然,CORS的OPTIONS预检请求失败了,因为还没有登录,会被过滤器拦截处理,返回了403状态码,所以浏览器认为CORS失败,也就会引发浏览器的CORS错误处理了。
由于同源策略的限制,浏览器不能读取响应的数据,也就无法对HTTP响应进行处理了。
前端代码
javascript
// 添加响应拦截器
instance.interceptors.response.use(function (resp) {
return resp.data;
}, function (err) {
console.log("invoke onRejected method of response interceptor.");
error(err); // 执行这行代码时浏览器报错了,CORS失败而无法读取响应数据
return Promise.reject(err);
});
原因分析
从上面浏览器控制台截图可以看到,浏览器发送的CORS预检请求返回了403状态码,浏览器就会认为CORS失败,也就无法读取服务端的HTTP响应了。
预检请求
CORS预检请求:请求方法为OPTIONS的请求,用于检查服务端是否允许接受跨域请求。
"预检"一词翻译于preflight,相关文档地址如下:
javascript
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#functional_overview

问题解决
为了防止预检请求失败,需要对Sa-Token的过滤器进行一些改进。
现在我们已知的是CORS预检请求的请求方法为OPTIONS,所以我们可以通过对OPTIONS请求放行来简单地避免这个CORS误判问题。
对此,需要重写过滤器的具体执行过滤的方法。
重写过滤器
在过滤器的doFilter()方法中放行,并通过return语句阻止请求被继续执行后面的处理认证的代码。
java
package cn.edu.sgu.www.ttsx.support;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.util.SaTokenConsts;
import cn.edu.sgu.www.ttsx.util.HttpUtils;
import org.springframework.core.annotation.Order;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 自定义Sa-Token过滤器
* @author 沐雨橙风ιε
* @version 1.0
*/
@Order(SaTokenConsts.ASSEMBLY_ORDER)
public class SaTokenFilter extends SaServletFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
/*
* CORS预检请求(OPTIONS)直接放行
* 如果预检请求因为执行认证时失败,会被误判为CORS失败
* 由于OPTIONS请求不会携带token请求头,通过Sa-Token过滤器处理认证时必然失败
*/
HttpServletRequest request = HttpUtils.getRequest();
if (isPreCheckRequest(request)) {
// 放行请求
chain.doFilter(req, resp);
return;
}
super.doFilter(req, resp, chain);
}
/**
* 是否CORS预检请求
* @param request HttpServletRequest对象
*/
private boolean isPreCheckRequest(HttpServletRequest request) {
String method = request.getMethod();
// CORS预检请求的请求方法为OPTIONS
return "OPTIONS".equalsIgnoreCase(method);
}
}
super.doFilter()就是调用原来SaServletFilter里的doFilter()方法。

也就是处理认证的代码,可以在后端代码的对应位置去查看。

最后,替换掉创建的Sa-Token过滤器。
java
SaServletFilter saServletFilter = new SaTokenFilter();

重启后端项目,然后重新操作一次,这次点击按钮还是没有反应。
查看控制台,CORS预检请求已经成功了,错误信息里少了一条CORS的错误提示。

但是,很多人对这个仅剩的1条跨域失败的亮眼的错误信息给整不会了,不是CORS成功了吗,为什么还会显示CORS错误。
其实提示里已经说明了原因,缺少Access-Control-Allow-Origin这个响应头。
添加CORS响应头
原因分析
为什么请求会缺少Access-Control-Allow-Origin这个响应头呢?
要弄明白这个问题,就需要查看我们Sa-Token过滤器处理认证错误的代码了。
java
// 处理认证异常
saServletFilter.setError(new SaFilterErrorStrategy() {
@Override
public Object run(Throwable throwable) {
// 响应状态码:403 Forbidden
ResponseCode responseCode = ResponseCode.FORBIDDEN;
// 构建响应数据
String message = null;
// 不同类型的异常区别处理
if (throwable instanceof NotLoginException) {
message = "请登录后访问!";
} else if (throwable instanceof NotRoleException || throwable instanceof NotPermissionException) {
message = "正在访问未授权的资源!";
// 设置响应状态码:401 Unauthorized
responseCode = ResponseCode.UNAUTHORIZED;
}
// 获取响应对象
SaResponse response = SaHolder.getResponse();
// 设置响应状态码
response.setStatus(responseCode.getCode());
JsonResult<Void> jsonResult = JsonResult.error(responseCode, message);
return JSON.toJSONString(jsonResult);
}
});
很显然,浏览器里显示的403状态码就是来自与这里的。
也就是说,在这个方法里已经给浏览器客户端发送响应了。
那问题就简单多了,这里有设置CORS响应头吗?答案显而易见,并没有!
既然没有设置CORS响应头,那加上设置的代码不就行了。
但是Access-Control-Allow-Origin响应头的值是什么呢?
别忘了,前面已经有了成功的OPTIONS请求,查看一下请求里的响应头就可以啦。

博主一眼就看到了4个和Access-Control-Allow-Origin名字很像的响应头。
而Access-Control-Allow-Origin响应头的值是前端项目的地址(IP+端口号)。
前端项目的地址可以通过这两个请求头获取。

只需要获取并设置到Access-Control-Allow-Origin响应头的值就可以了。
除了Access-Control-Allow-Origin响应头以外,其他几个的作用根据它们的名字和值也能猜出来。
- Access-Control-Allow-Methods:允许的CORS请求方法
- Access-Control-Allow-Headers:CORS请求中允许携带的请求头
- Access-Control-Max-Age:这个响应头的值是博主自己设置的
- Access-Control-Allow-Credentials:这个响应头的值是boolean类型的,直接设置成一样
代码展示
根据上面的分析,对处理认证错误的代码进行修改,设置上面提到的5个CORS相关的响应头。
SaTokenConfig.java
java
// 处理认证异常
saServletFilter.setError(new SaFilterErrorStrategy() {
@Override
public Object run(Throwable throwable) {
// 响应状态码:403 Forbidden
ResponseCode responseCode = ResponseCode.FORBIDDEN;
// 构建响应数据
String message = null;
// 不同类型的异常区别处理
if (throwable instanceof NotLoginException) {
message = "请登录后访问!";
} else if (throwable instanceof NotRoleException || throwable instanceof NotPermissionException) {
message = "正在访问未授权的资源!";
// 设置响应状态码:401 Unauthorized
responseCode = ResponseCode.UNAUTHORIZED;
}
// 获取响应对象
SaResponse response = SaHolder.getResponse();
// 设置响应状态码
response.setStatus(responseCode.getCode());
/*
* 设置CORS响应头
*/
// 获取请求对象
HttpServletRequest request = HttpUtils.getRequest();
// 获取Origin请求头
String origin = request.getHeader("Origin");
// 获取CORS配置
String maxAge = corsProperties.getMaxAge().toString();
String allowedHeaders = String.join(",", corsProperties.getAllowedHeaders());
String allowedMethods = String.join(",", corsProperties.getAllowedMethods());
response.setHeader("Access-Control-Max-Age", maxAge);
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Methods", allowedMethods);
response.setHeader("Access-Control-Allow-Headers", allowedHeaders);
response.setHeader("Access-Control-Allow-Credentials", "true");
JsonResult<Void> jsonResult = JsonResult.error(responseCode, message);
return JSON.toJSONString(jsonResult);
}
});
CorsProperties .java
CorsProperties 就是一个配置文件的读取类。
java
package cn.edu.sgu.www.ttsx.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* 跨域资源配置
* @author 沐雨橙风ιε
* @version 1.0
*/
@Data
@Component
@ConfigurationProperties("cors")
public class CorsProperties implements Serializable {
private static final long serialVersionUID = 18L;
/**
*
*/
private Long maxAge;
/**
* 允许的请求头
*/
private String[] allowedHeaders;
/**
* 允许的跨域请求方式
*/
private String[] allowedMethods;
/**
* 允许的跨域网站地址
*/
private String[] allowedOrigins;
}
application.yml
配置文件中的配置内容如下,其实项目中用到的自定义请求头只有一个。
java
cors:
max-age: 1800
allowed-headers: # 允许的跨域请求头
- '*'
allowed-methods: # 允许的跨域请求方式
- GET
- POST
- OPTIONS
allowed-origins: # 允许的跨域网站地址
- http://localhost:8087
重新操作
完成上面的修改之后,重新启动后端项目,再次点击操作按钮。
久违的错误提示已经出来了,这里用的是Element-UI的消息提示框。

浏览器的控制台也没有CORS的错误提示了。

文章结语
至此,问题已经被完美解决~
写这篇文章也是想分享一下自己遇到的问题,也许会对别人有所帮助。
好了,文章分享到这里了,如果觉得文章对你有所帮助(或者觉得博主写的好),不要吝啬你的一键三连哦~