Spring boot 對於 axios 在 cors 與 csrf 的整合配置
Spring boot 在 單一網域 或是 跨網域 開發相關的文章整理如下:
- 單一網域
- 跨網域
- 在 Node JS 使用 vue-cli 3 快速開發網頁與佈署到 github-pages
- Spring Boot Vue Axios 實現前後端分離的跨域訪問(CORS)
- Spring Boot 與 Vue + Axios 跨域訪問(CORS)並使用 JWT 解決 302(OPTIONS) 問題
- vue.js 整合 axios 並設定 global interceptors 與 toast 訊息
最後對於 跨來源資源共用(Cross-Origin Resource Sharing (CORS))與 跨站請求偽造防護 ( Cross Site Request Forgery (CSRF) ) 的整合配置來作為總結。
利用 RestController 取得 JWT 取得 JSON Web Token ,以及 csrfToken ,並以 JSON 型式回傳。
@RestController public class AuthenticationRestController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${jwt.header}") private String tokenHeader; @Autowired private AuthenticationManager authenticationManager; @Autowired AppUserDetailsService appUserDetailsService; @Autowired JwtTokenUtil jwtTokenUtil; @Autowired HttpSession session; @Autowired HttpSessionCsrfTokenRepository sessionCsrfTokenRepo; @RequestMapping(value = "${jwt.route.authentication.path}") public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest, HttpServletRequest request, HttpServletResponse response) throws Exception { if ( authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword()) ) { // Reload password post-security so we can generate the token final UserDetails userDetails = appUserDetailsService.loadUserByUsername(authenticationRequest.getUsername()); final String jwtToken = jwtTokenUtil.generateToken(userDetails); CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); // get CsrfToken if (Objects.isNull(csrfToken)) { csrfToken = (CsrfToken) sessionCsrfTokenRepo.generateToken(request); sessionCsrfTokenRepo.saveToken(csrfToken, request, response); } Map<String, Object> data = new HashMap<String, Object>(); data.put("jwtToken", jwtToken); data.put("csrfToken", csrfToken.getToken()); // Return the token return ResponseEntity.ok(data); } else { return ResponseEntity.badRequest().build(); } } @ExceptionHandler({AuthenticationException.class}) public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); } /** * Authenticates the user. If something is wrong, an {@link AuthenticationException} will be thrown */ private boolean authenticate(String username, String password) { Objects.requireNonNull(username); Objects.requireNonNull(password); boolean result = false; try { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); result = true; } catch (DisabledException e) { throw new AuthenticationException("User is disabled!", e); } catch (BadCredentialsException e) { throw new AuthenticationException("Bad credentials!", e); } return result; } }
客制實作 CORS 的 Filter: CorsCustFilter.Class
,來對允許的來源作開放。稍後要將它放在 ConcurrentSessionFilter.class
之前。
public class CorsCustFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Value("${cors-enable}") private boolean corsEnable; @Value("${cors-allow-origin}") private String corsAllowOrigin; @Autowired JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}") private String tokenHeader; @Autowired AppUserDetailsService appUserDetailsService; @Autowired private UserDetailsService userDetailsService; @Autowired HttpSessionCsrfTokenRepository sessionCsrfTokenRepo; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { boolean isEnable = false; String allowOrigin = "http://localhost:8080"; isEnable = Objects.isNull(corsEnable) ? isEnable : corsEnable; allowOrigin = Strings.isEmpty(corsAllowOrigin) ? allowOrigin : corsAllowOrigin; String username = null; String authToken = null; if(isEnable) { HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletRequest request= (HttpServletRequest) servletRequest; // addAllowedOrigin 不能設定為* 因為與 allowCredential 衝突 response.setHeader("Access-Control-Allow-Origin", allowOrigin); response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "*"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Expose-Headers", "Authorization"); final String requestHeader = request.getHeader("Authorization"); if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod()) ) { response.setStatus(HttpServletResponse.SC_OK); } else { if (requestHeader != null && requestHeader.startsWith("Bearer ")) { authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (IllegalArgumentException e) { logger.error("an error occured during getting username from token", e); } catch (ExpiredJwtException e) { logger.warn("the token is expired and not valid anymore", e); } logger.debug("checking authentication for user '{}'", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { logger.debug("security context was null, so authorizating user"); UserDetails userDetails = appUserDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); logger.info("authorizated user '{}', setting security context", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(servletRequest, servletResponse); } } } }
客制實作 CSRF 的 Filter: CsrfCustFilter.class
,來接收來源端的 csrfToken,稍後要將它放在 CsrfFilter.class
之前。
public class CsrfCustFilter extends OncePerRequestFilter { private final Log logger = LogFactory.getLog(getClass()); private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; private String headerName = DEFAULT_CSRF_HEADER_NAME; private final CsrfTokenRepository tokenRepository; /** * @param tokenRepository */ public CsrfCustFilter(CsrfTokenRepository tokenRepository) { this.tokenRepository = tokenRepository; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // TODO Auto-generated method stub final String reqCsrfToken = request.getHeader("X-CSRF-TOKEN"); if (!Objects.isNull(reqCsrfToken) && !reqCsrfToken.isEmpty()) { CsrfToken csrfToken = new DefaultCsrfToken(this.headerName, this.parameterName, reqCsrfToken); tokenRepository.saveToken(csrfToken, request, response); } filterChain.doFilter(request, response); } }
最後在 WebSecurityConfigurerAdapter 作設定,這樣就可以達到在 CORS 時,又有 CSRF 防護的功能了。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired AppsAuthenticationSuccessHandler appsAuthenticationSuccessHandler; @Autowired AppsAuthenticationFailureHandler appsAuthenticationFailureHandler; @Autowired AppsLogoutSuccessHandler appsLogoutSuccessHandler; @Autowired AppUserDetailsService appUserDetailsService; @Autowired JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.route.authentication.path}") private String jwtAuthenticationPath; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(appUserDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeRequests() .antMatchers("/user/password/update") .hasAuthority("CHANGE_PASSWORD_PRIVILEGE") .antMatchers("/", "/home","/register","/login/failure" ,"/user/password/forgot","/user/password/change/**","/user/register/**").permitAll() //首頁不需認證 認證機制設置 .anyRequest().authenticated() // 除了以上的 URL 外, 都需要認證才可以訪問 .and() .formLogin() .loginPage("/login") // 認證頁面指向頁頁 .failureHandler(appsAuthenticationFailureHandler) .defaultSuccessUrl("/auth/home") .permitAll() .successHandler(appsAuthenticationSuccessHandler) .and() .logout() .logoutSuccessHandler(appsLogoutSuccessHandler) .deleteCookies("JSESSIONID") .and() .csrf().csrfTokenRepository(csrfTokenRepository()).and() .sessionManagement() .maximumSessions(1).sessionRegistry(sessionRegistry()); JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader); httpSecurity.addFilterBefore(jwtAuthorizationTokenFilter, SessionManagementFilter.class); //adds your custom CorsFilter: CORS ( Cross-Origin Resource Sharing 跨域請求) httpSecurity.addFilterBefore(corsCustFilter(), ConcurrentSessionFilter.class); //add your custom CsrfFilter: CSRF ( Cross-site request forgery 防止跨站請求偽造攻擊 ) CsrfCustFilter csrfCustFilter = new CsrfCustFilter(csrfTokenRepository()); httpSecurity.addFilterBefore(csrfCustFilter, CsrfFilter.class); } @Override public void configure(WebSecurity web) throws Exception { //allow anonymous resource requests web.ignoring().antMatchers("/fonts/**","/css/**","/js/**","/AdminLTE3/**","/webjars/**","/vueComponent/**","/user/password/reset"); // for vue CORS web.ignoring().antMatchers(jwtAuthenticationPath); // @formatter:off web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); } @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Bean public HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowSemicolon(true); firewall.setAllowUrlEncodedPercent(true); return firewall; } /*客制跨域請求 CORS-Cross-Origin Resource Sharing*/ @Bean CorsCustFilter corsCustFilter() { CorsCustFilter filter = new CorsCustFilter(); return filter; } /** * JWT 使用 for AuthenticationRestController.class */ @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public HttpSessionCsrfTokenRepository csrfTokenRepository() { return new HttpSessionCsrfTokenRepository(); } }
你必須 登入 才能發表評論。