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,簡單來說,就是讓你的應用能夠安全地驗證用戶身份,并且讓用戶在一段時間內無需重復登錄。 JWT就像一張通行證,用戶拿著這張通行證就能訪問受保護的資源。
直接輸出解決方案即可)
-
添加依賴: 首先,需要在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: 創建一個配置類,繼承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的過濾器,后面會講到。
-
創建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未授權錯誤。
-
創建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中。
-
創建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進行加密。
-
創建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小時。
-
創建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機制通常涉及以下步驟:
- 頒發Refresh Token: 在用戶成功登錄后,除了頒發JWT之外,還頒發一個refresh token。 Refresh token通常具有比JWT更長的有效期,并存儲在數據庫中,與用戶關聯。
- 存儲Refresh Token: 將refresh token安全地存儲在服務器端數據庫中,并與用戶ID關聯。
- Refresh Token Endpoint: 創建一個專門用于刷新JWT的API端點(例如,/refresh-token)。
- 驗證Refresh Token: 當客戶端的JWT過期或即將過期時,客戶端將refresh token發送到/refresh-token端點。 服務器驗證refresh token是否有效(例如,檢查它是否存在于數據庫中,并且未被撤銷)。
- 頒發新的JWT: 如果refresh token有效,服務器會頒發一個新的JWT,并將新的JWT和新的refresh token(可選)返回給客戶端。 同時,可以更新數據庫中的refresh token。
- 撤銷Refresh Token: 實現撤銷refresh token的機制,例如,當用戶注銷時,可以從數據庫中刪除refresh token,使其失效。
此外,也可以考慮使用滑動過期策略,即每次用戶訪問受保護的資源時,都更新JWT的過期時間。
如何保護JWT的secret?
保護JWT的secret至關重要,因為如果secret泄露,攻擊者可以偽造JWT,冒充任何用戶。 以下是一些保護JWT secret的最佳實踐:
- 不要硬編碼secret: 永遠不要將secret硬編碼到代碼中。 這樣做會使secret容易被泄露,例如,通過源代碼管理系統或反編譯。
- 使用環境變量或配置文件: 將secret存儲在環境變量或配置文件中。 這樣可以將secret與代碼分離,并使其更容易管理。
- 使用強密碼生成器: 使用強密碼生成器生成一個隨機且復雜的secret。 避免使用容易猜測的字符串。
- 定期輪換secret: 定期輪換secret,例如,每隔幾個月或一年。 這樣做可以降低secret泄露的風險。
- 使用硬件安全模塊(HSM): 對于高安全要求的應用,可以考慮使用HSM來存儲和管理secret。 HSM是一種專門用于保護密鑰的安全硬件設備。
- 限制訪問權限: 限制對存儲secret的服務器或配置文件的訪問權限。 只有授權人員才能訪問secret。
- 監控secret的使用: 監控secret的使用情況,例如,記錄誰訪問了secret,以及何時訪問了secret。 這樣可以及時發現secret泄露的跡象。
如何在微服務架構中使用JWT?
在微服務架構中使用JWT可以實現單點登錄(SSO)和授權。 一種常見的做法是使用API網關作為JWT的驗證中心。
- API網關驗證JWT: 客戶端將JWT發送到API網關。 API網關驗證JWT的有效性。
- 轉發請求到微服務: 如果JWT有效,API網關將請求轉發到相應的微服務。 API網關可以將用戶信息(例如,用戶ID、角色)添加到請求頭中,以便微服務可以使用這些信息進行授權。
- 微服務進行授權: 微服務根據請求頭中的用戶信息進行授權。 例如,微服務可以檢查用戶是否具有訪問特定資源的權限。
另一種做法是每個微服務都驗證JWT。 這種做法的優點是微服務可以獨立地進行授權,缺點是每個微服務都需要維護JWT的驗證邏輯。 為了避免代碼重復,可以將JWT的驗證邏輯提取到一個共享庫中。
無論使用哪種做法,都需要確保JWT的secret在所有微服務中都是一致的。 可以使用配置中心來管理JWT的secret。
另外,可以考慮使用OAuth 2.0和OpenID Connect等標準協議來實現更安全和靈活的認證和授權。