2020 年 9 月 16 日

IT Skills 波林

Polin WEI – 資訊工作者的技術手札

Spring boot 對於 axios 在 cors 與 csrf 的整合配置(總結)

4 min read
cors-post

Spring boot 對於 axios 在 cors 與 csrf 的整合配置

cors-options

cors-post

 

Spring boot 在 單一網域 或是 跨網域 開發相關的文章整理如下:

 

  • 單一網域
  1. Spring Boot 的 CSRF_TOKEN 在 Vue 與 axios 整合
  2. spring boot 對於 axios 執行 ajax 遇到 response.status:302 的處理方式
  • 跨網域
  1. 在 Node JS 使用 vue-cli 3 快速開發網頁與佈署到 github-pages
  2. Spring Boot Vue Axios 實現前後端分離的跨域訪問(CORS)
  3. Spring Boot 與 Vue + Axios 跨域訪問(CORS)並使用 JWT 解決 302(OPTIONS) 問題
  4. 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();
 }
}

 

Copyright © All rights reserved. | Newsphere by AF themes.