一 Spring Security簡介
Spring Security是Spring生態系統中的一個安全框架,主要用于處理認證(Authentication)和授權(Authorization)。它提供了一套完整的安全解決方案,可以輕松集成到Spring應用中。
二 核心概念
1. 認證(Authentication)
驗證用戶的身份,確認"你是誰"。例如:用戶登錄過程。
2. 授權(Authorization)
驗證用戶是否有權限執行某個操作,確認"你能做什么"。例如:檢查用戶是否有權訪問某個API。
3. 主要組件
- SecurityContextHolder:存儲安全上下文信息
- Authentication:存儲當前用戶的認證信息
- UserDetails:用戶信息的核心接口
- UserDetailsService:加載用戶信息的核心接口
- AuthenticationProvider:認證的具體實現者
三 實戰案例:基于JWT的認證授權系統
一、核心架構
1. 核心組件關系圖
請求 → SecurityFilterChain → (多個Security Filter) → 目標資源↓SecurityContextHolder↓SecurityContext↓Authentication/ \Principal GrantedAuthority
2. 核心組件說明
2.1 SecurityContextHolder
- 作用:存儲當前線程的安全上下文信息
- 實現:使用ThreadLocal存儲SecurityContext
- 訪問方式:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
2.2 SecurityContext
- 作用:持有Authentication對象和其他安全相關信息
- 生命周期:請求開始到結束
- 存儲位置:ThreadLocal或Session中
2.3 Authentication
- 作用:存儲用戶認證信息
- 主要屬性:
- principal:用戶身份信息
- credentials:憑證信息(如密碼)
- authorities:用戶權限集合
- authenticated:是否已認證
- 常用實現:UsernamePasswordAuthenticationToken
二、認證流程詳解
1. 完整認證流程圖
用戶請求登錄↓
UsernamePasswordAuthenticationFilter↓
AuthenticationManager↓
AuthenticationProvider↓
UserDetailsService↓
UserDetails↓
Authentication對象↓
SecurityContext
2. 詳細流程說明
2.1 認證入口(以登錄為例)
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {// 1. 創建未認證的AuthenticationAuthentication authentication = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),loginRequest.getPassword());// 2. 執行認證Authentication authenticated = authenticationManager.authenticate(authentication);// 3. 認證成功,生成JWTSecurityContextHolder.getContext().setAuthentication(authenticated);String jwt = tokenProvider.generateToken(authenticated);return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
2.2 認證管理器(AuthenticationManager)
public class ProviderManager implements AuthenticationManager {private List<AuthenticationProvider> providers;@Overridepublic Authentication authenticate(Authentication authentication) {// 遍歷所有Provider嘗試認證for (AuthenticationProvider provider : providers) {if (!provider.supports(authentication.getClass())) {continue;}try {return provider.authenticate(authentication);} catch (AuthenticationException e) {// 處理認證異常}}throw new AuthenticationException("無法認證");}
}
2.3 自定義認證提供者
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {private final CustomUserDetailsService userDetailsService;private final PasswordEncoder passwordEncoder;@Overridepublic Authentication authenticate(Authentication authentication) {// 1. 獲取認證信息String username = authentication.getName();String password = authentication.getCredentials().toString();// 2. 加載用戶信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);// 3. 驗證密碼if (!passwordEncoder.matches(password, userDetails.getPassword())) {throw new BadCredentialsException("密碼錯誤");}// 4. 創建已認證的Authenticationreturn new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());}
}
三、授權流程詳解
1. 授權流程圖
請求 → FilterSecurityInterceptor↓SecurityContextHolder獲取Authentication↓AccessDecisionManager↓AccessDecisionVoter↓權限判斷結果
2. 詳細授權步驟
2.1 配置安全規則
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) {http.authorizeRequests()// 1. URL級別的權限控制.antMatchers("/api/public/**").permitAll().antMatchers("/api/admin/**").hasRole("ADMIN")// 2. 自定義權限判斷.anyRequest().access("@customSecurityService.hasPermission(request,authentication)");}
}
2.2 方法級別權限控制
@Service
public class UserService {// 使用Spring EL表達式進行權限控制@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")public UserDetails getUser(String username) {// 方法實現}
}
四、JWT集成原理
1. JWT認證流程
請求 → JwtAuthenticationFilter↓提取JWT令牌↓驗證JWT有效性↓解析用戶信息↓創建Authentication↓存入SecurityContext
2. JWT過濾器實現
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) {try {// 1. 從請求中提取JWTString jwt = getJwtFromRequest(request);if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {// 2. 從JWT中獲取用戶信息String username = tokenProvider.getUsernameFromJWT(jwt);String roles = tokenProvider.getRolesFromJWT(jwt);// 3. 創建AuthenticationList<GrantedAuthority> authorities = Arrays.stream(roles.split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(username, null, authorities);// 4. 設置認證信息SecurityContextHolder.getContext().setAuthentication(authentication);}} catch (Exception ex) {logger.error("無法設置用戶認證", ex);}filterChain.doFilter(request, response);}
}
五、數據校驗流程
1. 請求數據校驗
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {// 1. @Valid觸發數據校驗// 2. 校驗失敗拋出MethodArgumentNotValidException
}public class LoginRequest {@NotBlank(message = "用戶名不能為空")private String username;@NotBlank(message = "密碼不能為空")private String password;
}
2. 認證數據校驗
public class CustomUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) {// 1. 檢查用戶是否存在UserPrincipal user = userMap.get(username);if (user == null) {throw new UsernameNotFoundException("用戶不存在");}// 2. 檢查用戶狀態if (!user.isEnabled()) {throw new DisabledException("用戶已禁用");}// 3. 檢查賬戶是否過期if (!user.isAccountNonExpired()) {throw new AccountExpiredException("賬戶已過期");}// 4. 檢查賬戶是否鎖定if (!user.isAccountNonLocked()) {throw new LockedException("賬戶已鎖定");}return user;}
}
3. JWT數據校驗
public class JwtTokenProvider {public boolean validateToken(String authToken) {try {// 1. 驗證簽名Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);// 2. 驗證是否過期Claims claims = getClaimsFromJWT(authToken);return !claims.getExpiration().before(new Date());} catch (SignatureException ex) {logger.error("無效的JWT簽名");} catch (MalformedJwtException ex) {logger.error("無效的JWT令牌");} catch (ExpiredJwtException ex) {logger.error("JWT令牌已過期");} catch (UnsupportedJwtException ex) {logger.error("不支持的JWT令牌");} catch (IllegalArgumentException ex) {logger.error("JWT聲明為空");}return false;}
}
六、異常處理流程
1. 認證異常處理
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException e) throws IOException {// 1. 未認證異常處理response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.setContentType(MediaType.APPLICATION_JSON_VALUE);String message = "請先進行認證";if (e instanceof BadCredentialsException) {message = "用戶名或密碼錯誤";} else if (e instanceof JwtExpiredTokenException) {message = "token已過期";}response.getWriter().write(new ObjectMapper().writeValueAsString(new ApiResponse(false, message)));}
}
2. 授權異常處理
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException e) throws IOException {// 1. 權限不足異常處理response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.getWriter().write(new ObjectMapper().writeValueAsString(new ApiResponse(false, "沒有足夠的權限")));}
}
七、安全上下文傳遞
1. 異步方法中的安全上下文
@Async
public CompletableFuture<String> asyncMethod() {// 1. 獲取當前安全上下文SecurityContext context = SecurityContextHolder.getContext();return CompletableFuture.supplyAsync(() -> {try {// 2. 設置安全上下文到新線程SecurityContextHolder.setContext(context);// 3. 執行業務邏輯return "success";} finally {// 4. 清理安全上下文SecurityContextHolder.clearContext();}});
}