接續上一篇我們已經成功產生 JWT 回傳,所以後續使用者需要攜帶 JWT 至 Header 內然後發送請求到我們後端,我們需要驗證 JWT 是否有效然後決定使用者是否可以進入瀏覽或是使用功能,而這相對應的驗證流程,其實 Spring Security 有一套 FilterChain 的機制可以協助我們,這其中包含我們前面實作的帳號密碼驗證等等都是包含在裡面,那我們需要做的就是把 JWT 的驗證放入 FilterChain 裡面。
解析 Token 取出 Claims
把之前拿來產生 Token 的 密鑰( getKey() 產生的) 拿來解析 Token,extractAllClaims() 帶入 token 可以取出其中的 Claims 也就是 Payload 的部分。
因為 Payload 裡面有很多屬性,下面另外抽出各別取 username 使用者名稱, expiration 到期時間等等的方法,方便後續驗證時可以引用
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
| @Service @Slf4j public class JwtService {
public Claims extractAllClaims(String token) throws JwtException { return Jwts.parser() .setSigningKey(getKey()) .build().parseClaimsJws(token).getBody(); }
public Claims extractAllClaims(String token) throws JwtException { return Jwts.parser() .setSigningKey(getKey()) .build().parseClaimsJws(token).getBody(); }
private <T> T extractClaim(String token, Function<Claims, T> claimResolver) { final Claims claims = extractAllClaims(token); return claimResolver.apply(claims); }
public String extractUserName(String token) { return extractClaim(token, Claims::getSubject); }
public boolean validateToken(String token, UserDetails userDetails) { final String userName = extractUserName(token); return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token)); }
private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); }
private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } }
|
先寫個 Controller 確認一下解析內容 ,JWT 會放在 Header 下面 Authorization 的屬性內,@RequestHeader 可以幫忙取得 Header 的內容,然後需要切割掉前面的部分,他會是呈現這樣的格式 Bearer {JWT}
所以需要切掉前面 Bearer 和一個空格,就是 7 個字元,切好之後就是 token 部分直接帶入給我們前面的方法
1 2 3 4 5 6 7 8 9
| @GetMapping("/extractJwt") public Map<String, Object> extractJwt(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) { String token = authorization.substring(7); try { return jwtService.extractAllClaims(token); } catch (JwtException e) { throw new BadCredentialsException(e.getMessage(), e); } }
|
實際用 postman 操作,用先前資料 sean 登入取得 JWT,然後用 postman 選擇 Authoriztion ⇒ Auth Type 選 Bearer Token 然後帶入 JWT 就可以看到回傳結果正確。
1
| !https://ithelp.ithome.com.tw/upload/images/20240913/20150977UVI1FHxUjC.png
|
!https://ithelp.ithome.com.tw/upload/images/20240913/20150977UVI1FHxUjC.png
完成 JwtFilter
確定我們解析 token 沒問題就實作 JwtFilter 然後設置到 config 內。
這邊需要繼承 OncePerRequestFilter
這個 filter,他會協助每當請求來時只會進行一次驗證,否則 Spring Security 執行第一次後,因為該 Filter 剛好是個元件(bean),於是 Spring Boot 又執行第二次。
然後我們要 Override doFilterInternal()
這個方法,他會過濾請求,然後我們就照先前的解析方式,分別拿到 token 和 username 然後進行驗證,最後目標確認沒問題就會將其轉成 Authentication 物件,存入 Security Context,讓 Spring Security 的 FilterChain 進行驗證時可以取出確認。
下面的一些判斷都是確定請求內有帶對應的 header 才會動作
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
| @Component public class JwtFilter extends OncePerRequestFilter {
@Autowired JwtService jwtService;
@Autowired ApplicationContext context;
@Autowired MyUserDetailsService myUserDetailsService;
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null;
try { if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); username = jwtService.extractUserName(token); }
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
if (jwtService.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authtoken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authtoken); } } } catch (JwtException ex) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } filterChain.doFilter(request, response); } }
|
下面驗證 validateToken 部分會確認是否有過期,還有 token 解開的使用者名稱及 userDetails 內是相同,確認 JWT 是有效,然後才寫入 SecurityContext
config 加入 filter
前面實作完成,要加到 Spring Security 的 filter chain 中,認證的效果才能生效。
Spring Security 的 filter chain 會比其他 Filter 還優先執行。而裡頭負責進行的 Filter 叫做 UsernamePasswordAuthenticationFilter,正是負責帳號密碼認證。會選擇放在這個之前是因為,
JWT 認證通常是無狀態的,不依賴使用者名稱和密碼,如果 JWT 有效,就不需要再進行使用者名稱密碼認證。所以會加入在這個 filter 之前執行我們的 JwtFilter。
補充底下也添加 sessionManagement 選擇為無狀態,因為採用 Jwt 通常就不會採用 sesstion 來進行管理所以加入到配置中 Security 就不會幫我們產生 Session。
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
| @EnableWebSecurity @Configuration public class SecurityConfig {
@Autowired private UserDetailsService userDetailsService;
@Autowired private JwtFilter jwtFilter;
@Bean public AuthenticationProvider authProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(new BCryptPasswordEncoder(12)); return provider; }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }
@Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .csrf(customizer -> customizer.disable()) .authorizeHttpRequests((registry) -> registry .requestMatchers(HttpMethod.POST, "/register", "/login").permitAll() .requestMatchers(HttpMethod.GET, "/error", "/api/products/**", "/who-am-i").permitAll() .requestMatchers(HttpMethod.GET, "/checkAuthentication").hasAnyAuthority("ROLE_BUYER", "ROLE_SELLER", "ROLE_ADMIN") .requestMatchers(HttpMethod.POST, "/api/products").hasAuthority("ROLE_SELLER") .requestMatchers(HttpMethod.DELETE, "/api/products").hasAuthority("ROLE_SELLER") .requestMatchers("/api/users/**").hasAuthority("ROLE_ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }
|
最後進行測試,同樣拿 getUsers() 的方法測試看看用 Jwt 可否取得,這邊用 sean 登入,然後把 jwt 帶入去進行請求,可以故意把 Jwt 填錯看看,上面有特別包一個 try catch 來捕獲 JwtException 會回 403。
1
| !https://ithelp.ithome.com.tw/upload/images/20240913/201509778ZlqzHGsY0.png
|
!https://ithelp.ithome.com.tw/upload/images/20240913/201509778ZlqzHGsY0.png
如果正確可以成功獲得資料
1
| !https://ithelp.ithome.com.tw/upload/images/20240913/20150977KtSpIJXCar.png
|
!https://ithelp.ithome.com.tw/upload/images/20240913/20150977KtSpIJXCar.png
參考資料: