Springboot 中跨域问题的解决
等不及的小伙伴,直接跳到结论部分即可,谢谢!!!
1. 背景
Spring Security
Springboot
Vue.axios
Jwt
1.2 关键代码
Spring Security 实现了 JWT 验证
配置类相关代码
package xyz.yq56.sm.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import xyz.yq56.sm.filter.JwtSecurityFilter;
/**
* @author yi qiang
* @date 2021/9/25 9:16
*/
@ Configuration
@ EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@ Autowired
JwtSecurityFilter jwtSecurityFilter;
/**
* 配置接收检查的http请求
*
* @param http http
* @throws Exception 异常
*/
@ Override
protected void configure (HttpSecurity http ) throws Exception {
http. cors (AbstractHttpConfigurer :: disable)
. csrf (AbstractHttpConfigurer :: disable)
. sessionManagement (item -> item. sessionCreationPolicy (SessionCreationPolicy.STATELESS))
. authorizeRequests (req -> req
. antMatchers ( "/user/biz/login" ). permitAll ()
. anyRequest (). authenticated ())
. addFilterBefore (jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class)
. httpBasic (AbstractHttpConfigurer :: disable)
;
}
/**
* 配置忽略静态资源
*
* @param web web
* @throws Exception 异常
*/
@ Override
public void configure (WebSecurity web ) throws Exception {
web. ignoring (). mvcMatchers ( "/public/**" , "/static/**" );
}
/**
* 更改数据源实现
*
* @param auth 认证
* @throws Exception 异常
*/
@ Override
protected void configure (AuthenticationManagerBuilder auth ) throws Exception {
}
}
Filter 相关代码: 获取 JWT 认证头信息,然后进行认证
package xyz.yq56.sm.filter;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
import xyz.yq56.easytool.enums.JwtClaimKey;
import xyz.yq56.easytool.properties.EasyToolProperties;
import xyz.yq56.easytool.provider.jwt.JwtProvider;
import xyz.yq56.easytool.utils.json.JsonUtils;
import xyz.yq56.easytool.utils.nvll.NullUtil;
import xyz.yq56.easytool.utils.string.TextUtils;
import xyz.yq56.sm.common.context.RequestContextUtil;
/**
* @author yi qiang
* @date 2021/10/2 2:09
*/
@ Component
@ Slf4j
public class JwtSecurityFilter extends OncePerRequestFilter {
@ Autowired
EasyToolProperties easyToolProperties;
@ Autowired
JwtProvider jwtProvider;
@ Override
protected void doFilterInternal (
@ NonNull HttpServletRequest request ,
@ NonNull HttpServletResponse response ,
@ NonNull FilterChain filterChain ) throws ServletException, IOException {
if ( checkJwtToken ()) {
//do something
Map< String , Object > claims = jwtProvider. validateAccessToken ( extractToken ());
if ( ! NullUtil. isEmpty (claims)) {
List< String > list = TextUtils. strToList (String. valueOf (claims. get (JwtClaimKey.AUTHORITIES. getKey ())));
List< SimpleGrantedAuthority > authorities = list. stream (). map (SimpleGrantedAuthority ::new ). collect (Collectors. toList ());
//从token获取携带的信息,然后构建UsernamePasswordAuthenticationToken,并存入context
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (claims. get (JwtClaimKey.SUB. getKey ()), null , authorities);
SecurityContextHolder. getContext (). setAuthentication (authenticationToken);
log. info ( "Token认证成功,设置context | authenticationToken: {}" , JsonUtils. toJson (authenticationToken));
} else {
//拿不到信息就clear context
log. info ( "Token认证失败,清空context | claims: {}" , claims);
SecurityContextHolder. clearContext ();
}
} else {
log. info ( "Token认证失败,清空context | Token不存在或格式异常" );
SecurityContextHolder. clearContext ();
}
filterChain. doFilter (request, response);
}
/**
* 检查头是否存在
*
* @return 是否为jwt
*/
private boolean checkJwtToken () {
String jwtHeader = extractToken ();
log. info ( "检查Token格式 | token: {}" , jwtHeader);
return TextUtils. isNotEmpty (jwtHeader) && jwtHeader. startsWith (easyToolProperties. getJwt (). getPrefix ());
}
private String extractToken () {
return RequestContextUtil. getHeaderOrParam (easyToolProperties. getJwt (). getHeader ());
}
}
登录接口: 获取用户信息,然后颁发令牌
package xyz.yq56.sm.module.user.controller;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import xyz.yq56.easytool.provider.jwt.JwtProvider;
import xyz.yq56.easytool.utils.collection.CollectUtil;
import xyz.yq56.easytool.utils.collection.MapUtil;
import xyz.yq56.easytool.utils.log.LogUtil;
import xyz.yq56.sm.common.dto.Result;
import xyz.yq56.sm.common.enums.LogPrefix;
import xyz.yq56.sm.common.enums.ResponseCode;
import xyz.yq56.sm.common.util.ResultUtil;
import xyz.yq56.sm.module.user.model.User;
import xyz.yq56.sm.module.user.model.UserVo;
import xyz.yq56.sm.module.user.service.UserService;
/**
* @author yi qiang
* @date 2021/10/1 17:40
*/
@ RestController
@ RequestMapping ( "/user/biz/" )
public class UserBizController {
@ Autowired
JwtProvider jwtProvider;
@ Autowired
UserService userService;
@ PostMapping ( "login" )
public Result< UserVo > login (@ RequestBody User user ) {
LogUtil. info (LogPrefix.USER_BIZ. getPrefix (), MapUtil. builder ()
. put ( "user" , user). maps ());
List< User > userList = userService. list (Wrappers. < User > query (). eq ( "username" , user. getUsername ())
. eq ( "password" , user. getPassword ()));
if (CollectUtil. isEmpty (userList)) {
return ResultUtil. fail (ResponseCode.USER_NOT_EXIST);
}
if (userList. size () > 1 ) {
return ResultUtil. fail (ResponseCode.USER_EXIST_SAME);
}
return ResultUtil. success ( convertToVo (userList));
}
private UserVo convertToVo (List< User > userList ) {
User user = userList. get ( 0 );
UserVo userVo = new UserVo ();
BeanUtils. copyProperties (user, userVo);
userVo. setAccessToken (jwtProvider. generateAccessToken (JwtProvider.
buildClaims (userVo. getUid (), "ADMIN" , "ADMIN,USER" ), userVo. getUsername ()));
return userVo;
}
@ PostMapping ( "logout" )
public Result< UserVo > logout (String uid ) {
return ResultUtil. success ( null );
}
}
前端部分给 axios 配置了请求拦截器, 会在头部带上 JWT 认证头信息
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/element.js'
// 导入全局样式表
import './assets/css/global.css'
import './assets/font/iconfont.css'
import axios from 'axios'
// 配置请求根路径
axios.defaults.baseURL = 'http://localhost:8080/'
axios.interceptors.request. use ( config => {
if (config.url. indexOf ( 'login' ) === - 1 ) {
config.headers.Authorization = window.sessionStorage. getItem ( 'Authorization' )
}
return config
})
Vue . prototype .$http = axios
Vue.config.productionTip = false
new Vue ({
router,
render : h => h (App)
}). $mount ( '#app' )
除此之外,一般会事先进行一个基本的跨域配置,如下代码:
package xyz.yq56.sm.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @author yiqiang
*/
@ Configuration
public class CorsConfig {
@ Bean
public CorsFilter corsFilter () {
CorsConfiguration corsConfiguration = new CorsConfiguration ();
corsConfiguration. setAllowCredentials ( true );
corsConfiguration. addAllowedOrigin ( "*" );
corsConfiguration. addAllowedMethod ( "*" );
corsConfiguration. addAllowedHeader ( "*" );
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource ();
urlBasedCorsConfigurationSource. registerCorsConfiguration ( "/**" , corsConfiguration);
return new CorsFilter (urlBasedCorsConfigurationSource);
}
}
1.3 问题展示
以上代码登录接口都不会出现跨域问题, 但是当请求普通接口 (比如菜单接口) 时, 会出现跨域问题.
错误信息:
Access to XMLHttpRequest at xx from orgin xx has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
F12 截图
2 解决方案
其实通过报错信息也能大概明白, 是预检请求没有通过导致的报错, 一般就是 Option 请求出错
搜索了半天, 有人说加入如下配置即可解决
package xyz.yq56.sm.filter;
import java.io.IOException;
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 org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
/**
** 暂时没发现有什么用,先注释掉
* @author yiqiang
*/
@ Component
public class AccessCorsFilter implements Filter {
@ Override
public void doFilter (ServletRequest request , ServletResponse response , FilterChain chain )
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res. addHeader ( "Access-Control-Allow-Credentials" , "true" );
res. addHeader ( "Access-Control-Allow-Origin" , "*" );
res. addHeader ( "Access-Control-Allow-Methods" , "GET, POST, DELETE, PUT" );
res. addHeader ( "Access-Control-Allow-Headers" , "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN" );
if (((HttpServletRequest) request). getMethod (). equals (HttpMethod.OPTIONS. name ())) {
response. getWriter (). println ( "ok" );
return ;
}
chain. doFilter (request, response);
}
@ Override
public void destroy () {
}
@ Override
public void init (FilterConfig filterConfig ) {
}
}
先说结果, 我尝试加入了如上配置, 但是并没有什么卵用. 这个只是加了个过滤器,你这里没给人家退回去,不代表别人不会退回去.
经过推敲,我认为是 Spring Security 可能也有相关的限制,于是我去搜索了 Security 的跨域配置, 果然被我找到了requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
这个配置项. 于是我修改了 Security 配置, 如下:
@ Override
protected void configure (HttpSecurity http) throws Exception {
http. cors (AbstractHttpConfigurer :: disable)
. csrf (AbstractHttpConfigurer :: disable)
. sessionManagement (item -> item. sessionCreationPolicy (SessionCreationPolicy.STATELESS))
. authorizeRequests (req -> req
//非普通请求(比如请求新增了自定义头部信息,比如Jwt头),会发送预检Option请求,这里直接让他通过
. requestMatchers (CorsUtils :: isPreFlightRequest). permitAll ()
. antMatchers ( "/user/biz/login" ). permitAll ()
. anyRequest (). authenticated ())
. addFilterBefore (jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class)
. httpBasic (AbstractHttpConfigurer :: disable)
;
}
经验证, 请求正常,改动生效
3 结论
如果项目中符合如下几点, 可以尝试一下
项目中使用了 Spring Security
前端跨域请求中携带了自定义的 Header,比如 Jwt 等等
控制台报错: Access to XMLHttpRequest at xx from orgin xx has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
已配置常规跨域配置 CorsFilter
以上几点均满足的话,请立刻尝试新增 Spring Security 配置.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
@ Override
protected void configure (HttpSecurity http) throws Exception {
http. cors (AbstractHttpConfigurer :: disable)
. csrf (AbstractHttpConfigurer :: disable)
. sessionManagement (item -> item. sessionCreationPolicy (SessionCreationPolicy.STATELESS))
. authorizeRequests (req -> req
//非普通请求(比如请求新增了自定义头部信息,比如Jwt头),会发送预检Option请求,这里直接让他通过
. requestMatchers (CorsUtils :: isPreFlightRequest). permitAll ()
. antMatchers ( "/user/biz/login" ). permitAll ()
. anyRequest (). authenticated ())
. addFilterBefore (jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class)
. httpBasic (AbstractHttpConfigurer :: disable)
;
}
希望各位老大看到我这篇之后可以不用寻找下一篇博客. 能帮到你们的话,麻烦帮忙点个赞或者评论一下, 谢谢.