Spring Security Token 认证原理
Spring Security 是一个功能强大且高度可定制的身份验证和授权框架。在现代无状态应用(如 RESTful API)中,基于 Token 的认证是常见的选择,尤其是 JSON Web Token (JWT)。
Spring Security Token 认证原理
Spring Security 的 Token 认证通常基于过滤器链(Filter Chain)实现。其核心原理如下:
- 用户凭证提交: 客户端(浏览器、移动应用等)向服务器发送登录请求,包含用户名和密码。
- 认证过程:
- Spring Security 拦截到登录请求。
UsernamePasswordAuthenticationFilter
(或自定义的认证过滤器)从请求中提取用户名和密码。- 这些凭证被封装成一个
Authentication
对象(通常是UsernamePasswordAuthenticationToken
)。 Authentication
对象被传递给AuthenticationManager
。AuthenticationManager
委托给一个或多个AuthenticationProvider
进行实际的认证。AuthenticationProvider
(例如DaoAuthenticationProvider
)使用UserDetailsService
加载用户详细信息(UserDetails
),并使用PasswordEncoder
验证密码。- 如果认证成功,
AuthenticationProvider
返回一个完全填充的Authentication
对象,其中包含用户的权限信息。 AuthenticationManager
将此Authentication
对象设置到SecurityContextHolder
中,表示当前用户已认证。
- Token 生成:
- 认证成功后,服务器会生成一个 Token。对于无状态应用,通常是 JWT。
- JWT 包含三部分:Header(头部)、Payload(负载)和 Signature(签名)。
- Header: 通常包含 Token 类型(JWT)和加密算法(如 HS256)。
- Payload: 包含声明(Claims),例如用户ID、用户名、角色、过期时间等。这些声明可以是标准化的,也可以是自定义的。
- Signature: 使用 Header 和 Payload 以及一个服务器私钥进行签名,用于验证 Token 的完整性和真实性。
- 服务器将生成的 Token 返回给客户端。
- 客户端存储 Token: 客户端接收到 Token 后,通常将其存储在本地存储(localStorage)、会话存储(sessionStorage)或 Cookie 中。
- 后续请求携带 Token: 客户端在后续访问受保护资源时,会在 HTTP 请求的
Authorization
头中携带此 Token,通常格式为Bearer <token>
。 - Token 校验:
- Spring Security 再次拦截到请求。
- 自定义的 Token 过滤器(例如
JwtAuthenticationFilter
)从Authorization
头中提取 Token。 - 过滤器使用服务器的公钥(如果是非对称加密)或私钥(如果是对称加密)验证 Token 的签名,确保其未被篡改。
- 验证 Token 的过期时间。
- 解析 Token 的 Payload,获取用户身份和权限信息。
- 根据这些信息,加载对应的
UserDetails
对象。 - 创建一个
UsernamePasswordAuthenticationToken
对象,并将其设置到SecurityContextHolder
中。这样,后续的授权决策就可以基于此Authentication
对象进行。
- 授权决策: Spring Security 的授权管理器(
AccessDecisionManager
)根据SecurityContextHolder
中的Authentication
对象以及定义的访问规则(如@PreAuthorize
,hasRole()
,hasAuthority()
等)来决定用户是否有权访问请求的资源。 - 响应: 如果授权通过,请求继续处理;否则,返回 401 Unauthorized 或 403 Forbidden 错误。
关键特点:
- 无状态 (Stateless): 服务器无需在自身维护用户会话状态,减轻了服务器压力,更易于扩展和负载均衡。所有必要的用户信息都包含在 Token 中。
- 可伸缩性 (Scalability): 由于无状态,多个服务器实例可以处理请求,而无需共享会话状态。
- 安全性 (Security): Token 经过签名,可以防止篡改。可以通过 HTTPS 传输防止中间人攻击。
- 跨域 (CORS): Token 可以方便地在不同域的客户端和服务器之间传递。
Spring Security Token 认证的调用方法(以 JWT 为例)
实现 JWT Token 认证需要以下主要组件:
- JWT 工具类: 用于生成、解析和验证 JWT。
- 自定义
UserDetailsService
: 从数据库或其他存储中加载用户详细信息。 - 自定义认证过滤器: 拦截请求,解析 Token,并设置
Authentication
到SecurityContextHolder
。 - Spring Security 配置: 配置过滤器链,禁用 Session,并注册自定义组件。
下面是一个简化的示例:
1. Maven 依赖
XML
<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>
2. JWT 工具类 (JwtUtil.java)
Java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret; // 你的密钥,通常存储在配置文件中@Value("${jwt.expiration}")private long expiration; // Token过期时间,例如 24小时public String extractUsername(String token) {return extractClaim(token, Claims::getSubject);}public Date extractExpiration(String token) {return extractClaim(token, Claims::getExpiration);}public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {final Claims claims = extractAllClaims(token);return claimsResolver.apply(claims);}private Claims extractAllClaims(String token) {return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();}private Boolean isTokenExpired(String token) {return extractExpiration(token).before(new Date());}public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();// 可以添加自定义声明,例如角色等return createToken(claims, userDetails.getUsername());}private String createToken(Map<String, Object> claims, String subject) {return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(getSigningKey(), SignatureAlgorithm.HS256).compact();}public Boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}private Key getSigningKey() {byte[] keyBytes = Decoders.BASE64.decode(secret);return Keys.hmacShaKeyFor(keyBytes);}
}
3. 自定义 UserDetailsService
(CustomUserDetailsService.java)
Java
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.ArrayList;@Service
public class CustomUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 在这里模拟从数据库加载用户。实际应用中应从数据库或其他存储中获取。if ("user".equals(username)) {return new User("user", "{noop}password", new ArrayList<>()); // {noop}表示不使用密码编码} else if ("admin".equals(username)) {return new User("admin", "{noop}adminpass", new ArrayList<>());}throw new UsernameNotFoundException("User not found with username: " + username);}
}
4. 自定义认证过滤器 (JwtAuthenticationFilter.java)
Java
import com.example.yourproject.util.JwtUtil; // 替换为你的 JwtUtil 路径
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtUtil jwtUtil;private final UserDetailsService userDetailsService;public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {final String authorizationHeader = request.getHeader("Authorization");String username = null;String jwt = null;if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {jwt = authorizationHeader.substring(7);username = jwtUtil.extractUsername(jwt);}if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);if (jwtUtil.validateToken(jwt, userDetails)) {UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}}filterChain.doFilter(request, response);}
}
5. Spring Security 配置 (SecurityConfig.java)
Java
import com.example.yourproject.filter.JwtAuthenticationFilter; // 替换为你的过滤器路径
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder; // 仅用于示例,生产环境请使用BCryptPasswordEncoder等
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级别的安全,例如@PreAuthorize
public class SecurityConfig {private final UserDetailsService userDetailsService;private final JwtAuthenticationFilter jwtAuthenticationFilter;public SecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter) {this.userDetailsService = userDetailsService;this.jwtAuthenticationFilter = jwtAuthenticationFilter;}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()) // 禁用 CSRF,因为我们使用无状态 Token.authorizeHttpRequests(auth -> auth.requestMatchers("/api/authenticate").permitAll() // 允许所有用户访问认证接口.anyRequest().authenticated() // 其他所有请求都需要认证).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置会话策略为无状态).authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // 在UsernamePasswordAuthenticationFilter之前添加JWT过滤器return http.build();}@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService);authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic PasswordEncoder passwordEncoder() {// 生产环境请使用 BCryptPasswordEncoder 等,例如:// return new BCryptPasswordEncoder();return NoOpPasswordEncoder.getInstance(); // 仅用于演示,不进行密码编码}
}
6. 认证接口 (AuthController.java)
Java
import com.example.yourproject.util.JwtUtil;
import com.example.yourproject.service.CustomUserDetailsService; // 替换为你的服务路径
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/api")
public class AuthController {private final AuthenticationManager authenticationManager;private final JwtUtil jwtUtil;private final CustomUserDetailsService userDetailsService;public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, CustomUserDetailsService userDetailsService) {this.authenticationManager = authenticationManager;this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@PostMapping("/authenticate")public String createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {try {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));} catch (Exception e) {throw new Exception("Incorrect username or password", e);}final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());final String jwt = jwtUtil.generateToken(userDetails);return jwt; // 返回JWT Token}
}// 辅助类:认证请求体
class AuthenticationRequest {private String username;private String password;// Getters and Setterspublic String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}
7. 配置文件 (application.properties)
Properties
jwt.secret=your_super_secret_key_that_is_at_least_256_bits_long_and_base64_encoded
jwt.expiration=86400000 # 24小时 (毫秒)
注意: jwt.secret
应该是一个足够长且复杂的字符串,并且在生产环境中应该使用环境变量或更安全的配置方式。
调用方法:
-
用户登录:
- 客户端向
/api/authenticate
发送 POST 请求,请求体为 JSON 格式的{"username": "user", "password": "password"}
。 - 服务器验证凭证,如果成功,返回一个 JWT Token 字符串。
- 客户端向
-
访问受保护资源:
- 客户端在后续请求中,将获取到的 JWT Token 放入 HTTP 请求头
Authorization
中,格式为Bearer <your_jwt_token>
。 - 例如,访问
/api/someProtectedResource
。 - Spring Security 的
JwtAuthenticationFilter
会拦截请求,验证 Token,并设置SecurityContextHolder
。 - 如果 Token 有效且用户有权限,请求将成功处理。
- 客户端在后续请求中,将获取到的 JWT Token 放入 HTTP 请求头
示例使用:
-
登录请求 (POST /api/authenticate)
- Header:
Content-Type: application/json
- Body:
{"username": "user", "password": "password"}
- 响应:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjMxMzA1NjAwLCJleHAiOjE2MzEzOTE2MDB9.some_jwt_token_string
- Header:
-
访问受保护资源 (GET /api/protected)
- Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjMxMzA1NjAwLCJleHAiOjE2MzEzOTE2MDB9.some_jwt_token_string
- Header:
通过以上步骤,你就可以在 Spring Security 中实现基于 Token 的认证机制,实现无状态的 API 安全。记住,实际生产环境中还需要考虑刷新 Token、Token 吊销、日志记录、错误处理等更复杂的场景。