接續上一篇我們已經成功產生 JWT 回傳,所以後續使用者需要攜帶 JWT 至 Header 內然後發送請求到我們後端,我們需要驗證 JWT 是否有效然後決定使用者是否可以進入瀏覽或是使用功能,而這相對應的驗證流程,其實 Spring Security 有一套 FilterChain 的機制可以協助我們,這其中包含我們前面實作的帳號密碼驗證等等都是包含在裡面,那我們需要做的就是把 JWT 的驗證放入 FilterChain 裡面。
解析 Token 取出 Claims
把之前拿來產生 Token 的 密鑰( getKey() 產生的) 拿來解析 Token,extractAllClaims() 帶入 token 可以取出其中的 Claims 也就是 Payload 的部分。
因為 Payload 裡面有很多屬性,下面另外抽出各別取 username 使用者名稱, expiration 到期時間等等的方法,方便後續驗證時可以引用
| 12
 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 部分直接帶入給我們前面的方法
| 12
 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 才會動作
| 12
 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
 
 | @Componentpublic 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。
| 12
 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
參考資料: