## Spring Security:JwtAuthenticationConverter 與 @PreAuthorize 實戰筆記 - `JwtAuthenticationConverter`:決定如何把 JWT 的 claim 轉成 Spring Security 的 `Authentication`(特別是 authorities/roles)。 - `@PreAuthorize`:在方法層用 SpEL 表達式做授權判斷。 ### JwtAuthenticationConverter 是什麼? - 負責把 `Jwt` 物件轉成 `JwtAuthenticationToken`,同時決定 principal 與 authorities 的來源及格式。 - 預設會讀 `scope`/`scp` 並加上 `SCOPE_` 前綴;若你的 token 用 `roles`、`permissions` 等自訂 claim,就必須自訂 converter。 - 常見客製: - 改 authorities 的 claim 名稱(如 `roles`)。 - 設定 authority 前綴(如 `ROLE_`;若要用 `hasAuthority('orders.read')` 可改為空字串)。 - 指定 principal 來源(如 `sub` 或 `preferred_username`)。 ### 自訂 JwtAuthenticationConverter 範例 ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http, Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthConverter) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)) ); return http.build(); } @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); authoritiesConverter.setAuthoritiesClaimName("roles"); // JWT 權限的 claim 名稱 authoritiesConverter.setAuthorityPrefix("ROLE_"); // 讓 hasRole 判斷得通 JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); converter.setPrincipalClaimName("sub"); // principal 來源,也可用 preferred_username return converter; } } ``` 說明: - `roles` 內的值如 `"ADMIN"` 會被轉成 `ROLE_ADMIN`。 - 若想用細粒度權限(如 `orders.read`),可把 `setAuthorityPrefix("")`,改用 `hasAuthority('orders.read')`。 ### @PreAuthorize 是什麼? - 在方法執行前,用 SpEL 表達式檢查授權;常放在 Controller 或 Service。 - 先啟用方法安全:Spring Security 6+ 用 `@EnableMethodSecurity`,舊版可用 `@EnableGlobalMethodSecurity(prePostEnabled = true)`。 - 常用表達式: - `hasRole('ADMIN')`、`hasAnyRole('ADMIN','MANAGER')` - `hasAuthority('orders.read')`、`hasAnyAuthority(...)` - `authentication.name == #userId`(比對當事人) - `@beanName.check(authentication, #arg)`(呼叫自訂 Bean) ### @PreAuthorize 範例 ```java @RestController @RequestMapping("/orders") @EnableMethodSecurity public class OrderController { // 需要 ROLE_ADMIN 或 ROLE_MANAGER @PreAuthorize("hasAnyRole('ADMIN','MANAGER')") @GetMapping public List<Order> list() { ... } // 細粒度權限;若 converter 前綴為空可直接用 hasAuthority @PreAuthorize("hasAuthority('orders.read')") @GetMapping("/{id}") public Order get(@PathVariable Long id) { ... } // 只能讀自己的資料;principal 來自 JWT 的 sub @PreAuthorize("#userId == authentication.name") @GetMapping("/users/{userId}") public UserProfile findByUser(@PathVariable String userId) { ... } } ``` ### 整合與除錯心法 - 先決定 JWT 權限欄位格式,再設定 converter 的 claim 名稱與前綴;`@PreAuthorize` 要選 `hasRole` 還是 `hasAuthority` 取決於這個前綴。 - 若判斷不到權限,檢查: 1) `jwtAuthenticationConverter` 是否已掛上, 2) JWT claim 名稱是否正確, 3) 權限值是否有預期的前綴(如 ROLE_)。 - 路由授權與方法授權可並用:路由控制粗粒度(需登入),方法標註處理細粒度(角色/權限/資料擁有者)。