Resolution of Cross-domain Problems in Springboot (Response to preflight request does't pass access control check)

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

  1. Spring Security is used in the project
  2. Front-end cross-domain requests carry custom headers, such as Jwt, and so on
  3. 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.
  4. 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.

Keywords: Java Spring Spring Boot

Added by rtconner on Tue, 05 Oct 2021 20:18:15 +0300