Solving Cross-domain Problems in Springboot
Can't wait, just jump to the conclusion, thank you!!!
1.background
1.1 Use the technology stack
- Spring Security
- Springboot
- Vue.axios
- Jwt
1.2 Key Codes
Spring Security implements JWT validation
Configuration class related code
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; /** * Configure http requests to receive checks * * @param http http * @throws Exception abnormal */ @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) ; } /** * Configuration ignores static resources * * @param web web * @throws Exception abnormal */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().mvcMatchers("/public/**", "/static/**"); } /** * Change Data Source Implementation * * @param auth Authentication * @throws Exception abnormal */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { } }
Filter related code: Get JWT authentication header information, and then authenticate
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()); //Get the information you carry from token, build UsernamePasswordAuthenticationToken, and save it in context UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(claims.get(JwtClaimKey.SUB.getKey()), null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); log.info("Token Authentication Successful,Set up context | authenticationToken: {}", JsonUtils.toJson(authenticationToken)); } else { //clear context without information log.info("Token Authentication Failure,empty context | claims: {}", claims); SecurityContextHolder.clearContext(); } } else { log.info("Token Authentication Failure,empty context | Token No existence or formatting exception"); SecurityContextHolder.clearContext(); } filterChain.doFilter(request, response); } /** * Check Header Existence * * @return Is it jwt */ private boolean checkJwtToken() { String jwtHeader = extractToken(); log.info("inspect Token format | token: {}", jwtHeader); return TextUtils.isNotEmpty(jwtHeader) && jwtHeader.startsWith(easyToolProperties.getJwt().getPrefix()); } private String extractToken() { return RequestContextUtil.getHeaderOrParam(easyToolProperties.getJwt().getHeader()); } }
Logon interface: Get user information and issue a token
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); } }
The front part configures a request interceptor for axios with JWT authentication header information on the header
import Vue from 'vue' import App from './App.vue' import router from './router' import './plugins/element.js' // Import Global Style Sheet import './assets/css/global.css' import './assets/font/iconfont.css' import axios from 'axios' // Configure Request Root Path 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')
In addition, a basic cross-domain configuration is typically performed beforehand, as follows:
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 Question Show
None of the above code login interfaces have cross-domain issues, but when you request a common interface, such as a menu interface, cross-domain issues occur.
Error message:
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 Screenshot
2 Solutions
In fact, you can also get a general understanding through the error information, which is caused by the failure of the pre-check request, generally the Option request error
After searching for half a day, someone said that adding the following configuration would solve the problem
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; /** ** I don't see any use for this moment. Please comment it out first * @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) { } }
First, as a result, I tried to join the configuration as above, but there were no eggs. This just added a filter, and you didn't give people back here, which doesn't mean they won't.
After some speculation, I thought Spring Security might have limitations, so I searched for Security's cross-domain configuration and found the request Matchers (CorsUtils:: isPreFlightRequest). permitAll() configuration item. So I modified the Security configuration as follows:
@Override protected void configure(HttpSecurity http) throws Exception { http.cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(item -> item.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeRequests(req -> req //Non-trivial requests, such as requests with new custom headers, such as Jwt headers, send a pre-check Option request, where it is directly passed .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .antMatchers("/user/biz/login").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class) .httpBasic(AbstractHttpConfigurer::disable) ; }
Verify that the request is correct and the changes take effect
3 Conclusion
If your project meets the following points, you can try it
- Spring Security is used in the project
- Front-end cross-domain requests carry custom headers, such as Jwt, and so on
- Console error: Access to XMLHttpRequest at XX from orgin XX has been blocked by CORS policy: Response to preflight request does't pass access control check: It does not have HTTP OK status.
- Configured general cross-domain configuration CorsFilter
If all of the above are satisfied, try adding the Spring Security configuration.requestMatchers(CorsUtils::isPreFlightRequest).permitAll() now.
@Override protected void configure(HttpSecurity http) throws Exception { http.cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(item -> item.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeRequests(req -> req //Non-trivial requests, such as requests with new custom headers, such as Jwt headers, send a pre-check Option request, where it is directly passed .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .antMatchers("/user/biz/login").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class) .httpBasic(AbstractHttpConfigurer::disable) ; }
I hope you guys don't have to look for the next blog after you see this one. If you can help, please give us a compliment or comment. Thank you.