Spring Security整合JWT的詳細配置與實現

spring security整合jwt的解決方案如下:1. 添加依賴:在pom.xml中添加spring-boot-starter-security和jjwt相關依賴,包括jjwt-api、jjwt-impl和jjwt-jackson;2. 配置spring security:創建securityconfig類繼承websecurityconfigureradapter,禁用csrf,設置認證規則為除/authenticate外均需認證,并配置無狀態會話管理;3. 創建jwtauthenticationentrypoint:實現authenticationentrypoint接口,用于處理未授權訪問,返回401錯誤;4. 創建jwtrequestfilter:繼承onceperrequestfilter,攔截請求提取jwt,驗證有效性后設置認證信息到securitycontextholder;5. 創建jwtuserdetailsservice:實現userdetailsservice,根據用戶名加載用戶信息,實際項目中應從數據庫獲??;6. 創建jwtTokenutil:負責生成、解析和驗證jwt,包含生成token、提取用戶名、判斷過期等功能;7. 創建authenticationcontroller:提供認證接口,接收用戶名密碼并返回生成的jwt;8. 處理jwt過期問題:通過引入refresh token機制,在用戶登錄時同時頒發refresh token,并設置專門的刷新端點,驗證有效后頒發新jwt;9. 保護jwt secret:避免硬編碼,使用環境變量或配置文件存儲,定期輪換,使用強密碼生成器,限制訪問權限并監控使用情況;10. 在微服務架構中使用jwt:可通過api網關統一驗證jwt并將用戶信息轉發給微服務,或每個微服務獨立驗證jwt,推薦將驗證邏輯封裝為共享庫,并通過配置中心統一管理secret。

Spring Security整合JWT的詳細配置與實現

Spring Security整合JWT,簡單來說,就是讓你的應用能夠安全地驗證用戶身份,并且讓用戶在一段時間內無需重復登錄。 JWT就像一張通行證,用戶拿著這張通行證就能訪問受保護的資源。

Spring Security整合JWT的詳細配置與實現

直接輸出解決方案即可)

Spring Security整合JWT的詳細配置與實現

  1. 添加依賴: 首先,需要在pom.xml中添加Spring Security和JWT相關的依賴。

    <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt-api</artifactId>     <version>0.11.5</version> </dependency> <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt-impl</artifactId>     <version>0.11.5</version>     <scope>runtime</scope> </dependency> <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt-jackson</artifactId>     <version>0.11.5</version>     <scope>runtime</scope> </dependency>

    這里使用了jjwt庫,它提供了JWT的生成和解析功能。版本號可以根據實際情況調整。

    Spring Security整合JWT的詳細配置與實現

  2. 配置Spring Security: 創建一個配置類,繼承WebSecurityConfigurerAdapter,并重寫configure(httpSecurity http)方法。

    @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {      @Autowired     private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;      @Autowired     private JwtRequestFilter jwtRequestFilter;      @Override     protected void configure(HttpSecurity http) throws Exception {         http.csrf().disable()             .authorizeRequests()             .antMatchers("/authenticate").permitAll() // 允許/authenticate接口匿名訪問             .anyRequest().authenticated() // 其他所有請求都需要認證             .and()             .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)             .and().SessionManagement()             .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用JWT,不創建session          http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);     }      @Bean     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     } }

    這段代碼禁用了CSRF,配置了哪些接口允許匿名訪問,哪些需要認證,并設置了session管理策略為STATELESS,意味著我們不會使用session來存儲用戶信息。JwtRequestFilter是用于攔截請求并驗證JWT的過濾器,后面會講到。

  3. 創建JwtAuthenticationEntryPoint: 當用戶嘗試訪問需要認證的資源,但未提供有效的憑證時,JwtAuthenticationEntryPoint會被調用。

    @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {      private static final long serialVersionUID = -7858869558953243875L;      @Override     public void commence(HttpServletRequest request, HttpServletResponse response,                          AuthenticationException authException) throws IOException {          response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");     } }

    這里簡單地返回一個401未授權錯誤。

  4. 創建JwtRequestFilter: 這個過濾器負責攔截每個請求,從請求頭中提取JWT,驗證其有效性,并設置Spring Security的上下文。

    @Component public class JwtRequestFilter extends OncePerRequestFilter {      @Autowired     private JwtUserDetailsService jwtUserDetailsService;      @Autowired     private JwtTokenUtil jwtTokenUtil;      @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)             throws ServletException, IOException {          final String requestTokenHeader = request.getHeader("Authorization");          String username = null;         String jwtToken = null;          if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {             jwtToken = requestTokenHeader.substring(7);             try {                 username = jwtTokenUtil.getUsernameFromToken(jwtToken);             } catch (IllegalArgumentException e) {                 System.out.println("Unable to get JWT Token");             } catch (ExpiredJwtException e) {                 System.out.println("JWT Token has expired");             }         } else {             logger.warn("JWT Token does not begin with Bearer String");         }          if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {              UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);              if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {                  UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(                         userDetails, null, userDetails.getAuthorities());                 usernamePasswordAuthenticationToken                         .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));                  SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);             }         }          chain.doFilter(request, response);     }  }

    這個過濾器首先從Authorization請求頭中獲取JWT。如果JWT存在且有效,它會從JWT中提取用戶名,然后從JwtUserDetailsService加載用戶信息,并創建一個UsernamePasswordAuthenticationToken,最后將其設置到SecurityContextHolder中。

  5. 創建JwtUserDetailsService: 這個類負責根據用戶名從數據庫或其他數據源加載用戶信息。

    @Service public class JwtUserDetailsService implements UserDetailsService {      @Autowired     private PasswordEncoder passwordEncoder;      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         // 假設從數據庫獲取用戶信息         if ("javahub".equals(username)) {             return new User("javahub", passwordEncoder.encode("password"),                     new ArrayList<>());         } else {             throw new UsernameNotFoundException("User not found with username: " + username);         }     } }

    這里為了簡單起見,直接硬編碼了一個用戶。實際項目中,你需要從數據庫或其他數據源加載用戶信息。注意,密碼需要使用PasswordEncoder進行加密。

  6. 創建JwtTokenUtil: 這個類負責生成和驗證JWT。

    @Component public class JwtTokenUtil implements Serializable {      private static final long serialVersionUID = -2550185165626007488L;      public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;      @Value("${jwt.secret}")     private String secret;      public String getUsernameFromToken(String token) {         return getClaimFromToken(token, Claims::getSubject);     }      public Date getExpirationDateFromToken(String token) {         return getClaimFromToken(token, Claims::getExpiration);     }      public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {         final Claims claims = getAllClaimsFromToken(token);         return claimsResolver.apply(claims);     }      private Claims getAllClaimsFromToken(String token) {         return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();     }      private Boolean isTokenExpired(String token) {         final Date expiration = getExpirationDateFromToken(token);         return expiration.before(new Date());     }      public String generateToken(UserDetails userDetails) {         Map<String, Object> claims = new HashMap<>();         return doGenerateToken(claims, userDetails.getUsername());     }      private String doGenerateToken(Map<String, Object> claims, String subject) {          return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))                 .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))                 .signWith(SignatureAlgorithm.HS512, secret).compact();     }      public Boolean validateToken(String token, UserDetails userDetails) {         final String username = getUsernameFromToken(token);         return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));     } }

    這個類包含了生成JWT、從JWT中提取信息、驗證JWT等方法。secret是用于簽名JWT的密鑰,應該保密。JWT_TOKEN_VALIDITY是JWT的有效期,這里設置為5小時。

  7. 創建AuthenticationController: 這個Controller負責處理用戶認證請求,生成JWT并返回給用戶。

    @RestController @CrossOrigin public class AuthenticationController {      @Autowired     private AuthenticationManager authenticationManager;      @Autowired     private JwtTokenUtil jwtTokenUtil;      @Autowired     private JwtUserDetailsService userDetailsService;      @PostMapping("/authenticate")     public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {          authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());          final UserDetails userDetails = userDetailsService                 .loadUserByUsername(authenticationRequest.getUsername());          final String token = jwtTokenUtil.generateToken(userDetails);          return ResponseEntity.ok(new AuthenticationResponse(token));     }      private void authenticate(String username, String password) throws Exception {         try {             authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));         } catch (DisabledException e) {             throw new Exception("USER_DISABLED", e);         } catch (BadCredentialsException e) {             throw new Exception("INVALID_CREDENTIALS", e);         }     } }  class AuthenticationRequest {     private String username;     private String password;      // Getters and setters }  class AuthenticationResponse {     private final String jwt;      public AuthenticationResponse(String jwt) {         this.jwt = jwt;     }      // Getter }

    這個Controller接收用戶名和密碼,使用AuthenticationManager進行認證。如果認證成功,它會使用JwtTokenUtil生成JWT,并將其返回給用戶。

如何處理JWT過期問題?

JWT過期是使用JWT認證時必須考慮的問題。一個常見的解決方案是使用refresh token。 當用戶的JWT即將過期時,可以使用refresh token來獲取一個新的JWT,而無需用戶重新登錄。 實現refresh token機制通常涉及以下步驟:

  1. 頒發Refresh Token: 在用戶成功登錄后,除了頒發JWT之外,還頒發一個refresh token。 Refresh token通常具有比JWT更長的有效期,并存儲在數據庫中,與用戶關聯。
  2. 存儲Refresh Token: 將refresh token安全地存儲在服務器端數據庫中,并與用戶ID關聯。
  3. Refresh Token Endpoint: 創建一個專門用于刷新JWT的API端點(例如,/refresh-token)。
  4. 驗證Refresh Token: 當客戶端的JWT過期或即將過期時,客戶端將refresh token發送到/refresh-token端點。 服務器驗證refresh token是否有效(例如,檢查它是否存在于數據庫中,并且未被撤銷)。
  5. 頒發新的JWT: 如果refresh token有效,服務器會頒發一個新的JWT,并將新的JWT和新的refresh token(可選)返回給客戶端。 同時,可以更新數據庫中的refresh token。
  6. 撤銷Refresh Token: 實現撤銷refresh token的機制,例如,當用戶注銷時,可以從數據庫中刪除refresh token,使其失效。

此外,也可以考慮使用滑動過期策略,即每次用戶訪問受保護的資源時,都更新JWT的過期時間。

如何保護JWT的secret?

保護JWT的secret至關重要,因為如果secret泄露,攻擊者可以偽造JWT,冒充任何用戶。 以下是一些保護JWT secret的最佳實踐:

  1. 不要硬編碼secret: 永遠不要將secret硬編碼到代碼中。 這樣做會使secret容易被泄露,例如,通過源代碼管理系統或反編譯。
  2. 使用環境變量或配置文件: 將secret存儲在環境變量或配置文件中。 這樣可以將secret與代碼分離,并使其更容易管理。
  3. 使用強密碼生成器: 使用強密碼生成器生成一個隨機且復雜的secret。 避免使用容易猜測的字符串。
  4. 定期輪換secret: 定期輪換secret,例如,每隔幾個月或一年。 這樣做可以降低secret泄露的風險。
  5. 使用硬件安全模塊(HSM): 對于高安全要求的應用,可以考慮使用HSM來存儲和管理secret。 HSM是一種專門用于保護密鑰的安全硬件設備。
  6. 限制訪問權限: 限制對存儲secret的服務器或配置文件的訪問權限。 只有授權人員才能訪問secret。
  7. 監控secret的使用: 監控secret的使用情況,例如,記錄誰訪問了secret,以及何時訪問了secret。 這樣可以及時發現secret泄露的跡象。

如何在微服務架構中使用JWT?

在微服務架構中使用JWT可以實現單點登錄(SSO)和授權。 一種常見的做法是使用API網關作為JWT的驗證中心。

  1. API網關驗證JWT: 客戶端將JWT發送到API網關。 API網關驗證JWT的有效性。
  2. 轉發請求到微服務: 如果JWT有效,API網關將請求轉發到相應的微服務。 API網關可以將用戶信息(例如,用戶ID、角色)添加到請求頭中,以便微服務可以使用這些信息進行授權。
  3. 微服務進行授權: 微服務根據請求頭中的用戶信息進行授權。 例如,微服務可以檢查用戶是否具有訪問特定資源的權限。

另一種做法是每個微服務都驗證JWT。 這種做法的優點是微服務可以獨立地進行授權,缺點是每個微服務都需要維護JWT的驗證邏輯。 為了避免代碼重復,可以將JWT的驗證邏輯提取到一個共享庫中。

無論使用哪種做法,都需要確保JWT的secret在所有微服務中都是一致的。 可以使用配置中心來管理JWT的secret。

另外,可以考慮使用OAuth 2.0和OpenID Connect等標準協議來實現更安全和靈活的認證和授權。

? 版權聲明
THE END
喜歡就支持一下吧
點贊10 分享