前言
這篇主要記錄一個突然想做的做法,然後四處碰壁的結果,由來是原本我是按照單一前後台的方式做設計,於是原有的config 在同一個檔,包含登入登出跟頁面訪問跟exception 處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Bean public SecurityFilterChain webSecurityFilterChain( HttpSecurity http, DaoAuthenticationProvider authenticationProvider ) throws Exception {
http .authenticationProvider(authenticationProvider) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/attendance/**", "/user/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/css/**", "/js/**", "/images/**", "/assets/**").permitAll() .anyRequest().permitAll() ) .formLogin(form -> form .loginPage(LOGIN_URL) .loginProcessingUrl(LOGIN_URL) .successHandler(successHandler) .failureUrl(LOGIN_URL + "?error") .permitAll() ) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST")) .logoutSuccessHandler(logoutSuccessHandler) .permitAll() ) .exceptionHandling(exception -> exception .authenticationEntryPoint((request, response, authException) -> response.sendRedirect(LOGIN_URL)) .accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/403")) );
return http.build(); }
|
但是我突然想在同一個專案開api 😊😊
於是我就開始了我的痛苦之旅
config 共用
首先我照舊寫了另外一個api 專用的 config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/login", "/api/register").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) ;
return http.build(); }
|
然後就發現 全部request 都被擋掉 不管怎麼弄都一樣,看著看著發現,這條好像跟著走去web 的設定那段了,api 這段一般也不會加上csrf token, 還有url 被security 共用的問題,於是作了以下變動
優化: 增加序列
在兩段config 加上序列@Order
及加上securityMatcher
,讓spring security 知道誰要先執行,並指忽略api/** 的csrf token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Configuration @EnableWebSecurity @EnableMethodSecurity @Order(1) public class ApiSecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
public ApiSecurityConfig(JwtRequestFilter jwtRequestFilter) { this.jwtRequestFilter = jwtRequestFilter; }
@Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .authorizeHttpRequests(auth -> auth .requestMatchers("/api/login", "/api/register").permitAll() .anyRequest().authenticated() ) .csrf(csrf -> csrf .ignoringRequestMatchers("/api/**") ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) ;
return http.build(); }
}
|
然後發現程式正常跑了,但是又發現一個問題,就是跨來源資源共享(CORS)的問題
優化: 跨來源資源共享(CORS)
一開始使用了全局的config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.example.clockin.config;
import com.example.clockin.util.Constants; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class CrossOriginsConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins(Constants.LOCAL_FRONT_HOST) .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }
|
但發現不管我怎麼送都是403,後來發現是因為阿~~~好像這樣沒有設定在API 端阿,於是我就把config 改成這樣
以下是spring security 6 之後的寫法,因為不再支援.cors().disable(),所以要用這種方式設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Configuration @EnableMethodSecurity @Order(1) public class ApiSecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
public ApiSecurityConfig(JwtRequestFilter jwtRequestFilter) { this.jwtRequestFilter = jwtRequestFilter; }
@Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/login", "/api/register").permitAll() .anyRequest().authenticated() ) .csrf(csrf -> csrf .ignoringRequestMatchers("/api/**") ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) ;
return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of(Constants.LOCAL_FRONT_HOST)); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", configuration); return source; }
}
|
然後就可以送了-> 接著測試登入,發現又是403😒??
後來發現是jwt filter 那邊也要排除
優化: 排除jwt filter
// Skip JWT validation for specific endpoints 那段就是後來加上去的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| @Component public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService; private final JwtUtil jwtUtil; private static final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class); @Autowired public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) { this.userDetailsService = userDetailsService; this.jwtUtil = jwtUtil; }
@Override protected void doFilterInternal( @NotNull HttpServletRequest request, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain ) throws ServletException, IOException {
String path = request.getRequestURI();
if ("/api/login".equals(path) || "/api/register".equals(path)) { logger.info("Skipping JWT validation for path: {}", path); filterChain.doFilter(request, httpServletResponse); return; }
final String authorizationHeader = request.getHeader("Authorization");
String username = null; String jwt = null;
try { if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); logger.info("Validating JWT for user: {}", username); }
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (Boolean.TRUE.equals(jwtUtil.validateToken(jwt, userDetails.getUsername()))) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authToken); logger.info("JWT validation successful for user: {}", username); } else { logger.warn("JWT validation failed for user: {}", username); } } } catch (Exception e) { logger.error("JWT validation error: {}", e.getMessage()); } filterChain.doFilter(request, httpServletResponse); logger.info("Request processing completed for path: {}", path); }
}
|
然後就可以送了 可喜可樂,希望不要再有奇怪的想法出現,還是乖乖地拆成兩個小專案比較簡單