认证协议与标准¶
目录¶
Cookie/Session机制¶
Cookie基础¶
什么是Cookie?
Cookie是服务器发送到用户浏览器并保存在本地的小型文本文件,浏览器在后续请求中会自动携带Cookie。
Cookie属性:
Cookie cookie = new Cookie("sessionId", "abc123");
// 1. Domain - 指定Cookie的域名
cookie.setDomain(".example.com"); // 可用于 example.com 及其子域
// 2. Path - 指定Cookie的路径
cookie.setPath("/"); // 对所有路径有效
// 3. Max-Age / Expires - 过期时间
cookie.setMaxAge(3600); // 3600秒后过期
// -1: 会话Cookie(浏览器关闭后删除)
// 0: 立即删除
// >0: 持久Cookie,指定秒数后过期
// 4. HttpOnly - 禁止JavaScript访问
cookie.setHttpOnly(true); // 防止XSS攻击
// 5. Secure - 仅通过HTTPS传输
cookie.setSecure(true);
// 6. SameSite - 限制跨站发送
cookie.setAttribute("SameSite", "Strict");
// Strict: 完全禁止跨站发送
// Lax: 允许安全的跨站请求(GET导航)
// None: 允许跨站发送(需配合Secure=true)
Cookie的局限性: - 大小限制:通常4KB - 数量限制:每个域名约20-50个 - 安全性:容易被窃取(需HTTPS + HttpOnly) - 跨域限制:受同源策略限制
Session机制¶
工作流程:
1. 客户端请求 → 服务器
2. 服务器创建Session对象,生成SessionID
3. 服务器通过Set-Cookie响应头发送SessionID
Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly
4. 客户端后续请求自动携带Cookie
Cookie: JSESSIONID=ABC123
5. 服务器通过SessionID查找Session对象
Java实现:
/**
* Session管理示例
*/
@Controller
public class SessionController {
/**
* 登录 - 创建Session
*/
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
HttpSession session) {
// 验证用户名密码
User user = userService.authenticate(username, password);
if (user == null) {
return "redirect:/login?error";
}
// 存储用户信息到Session
session.setAttribute("userId", user.getId());
session.setAttribute("username", user.getUsername());
session.setAttribute("roles", user.getRoles());
// 设置Session超时时间(秒)
session.setMaxInactiveInterval(1800); // 30分钟
return "redirect:/dashboard";
}
/**
* 获取当前用户
*/
@GetMapping("/api/me")
@ResponseBody
public User getCurrentUser(HttpSession session) {
Long userId = (Long) session.getAttribute("userId");
if (userId == null) {
throw new UnauthorizedException("未登录");
}
return userService.findById(userId);
}
/**
* 登出 - 销毁Session
*/
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 销毁Session
return "redirect:/login";
}
}
分布式Session解决方案:
- Session复制(Session Replication)
- 优点:无需额外组件
-
缺点:性能开销大,不适合大规模集群
-
Session粘性(Sticky Session)
- 优点:实现简单
-
缺点:服务器宕机导致Session丢失,负载不均衡
-
集中式Session存储(推荐)
使用Redis存储Session:
<!-- Spring Session + Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
/**
* Spring Session配置
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
}
# application.properties
spring.redis.host=localhost
spring.redis.port=6379
spring.session.store-type=redis
spring.session.redis.namespace=spring:session
Redis中的Session数据结构:
# Key格式
spring:session:sessions:<sessionId>
spring:session:sessions:expires:<sessionId>
spring:session:expirations:<expirationTime>
# Session内容(Hash结构)
HGETALL spring:session:sessions:abc123
1) "sessionAttr:userId"
2) "10001"
3) "sessionAttr:username"
4) "zhangsan"
5) "creationTime"
6) "1609459200000"
7) "lastAccessedTime"
8) "1609462800000"
9) "maxInactiveInterval"
10) "1800"
Token认证¶
Token vs Session¶
| 特性 | Session | Token |
|---|---|---|
| 状态 | 有状态(服务器存储) | 无状态(客户端存储) |
| 存储位置 | 服务器内存/数据库/Redis | 客户端(Cookie/LocalStorage) |
| 扩展性 | 需要Session共享机制 | 天然支持分布式 |
| 跨域 | 受Cookie限制 | 容易实现跨域 |
| 移动端 | 不友好 | 友好 |
| 吊销 | 容易(直接删除Session) | 困难(需要黑名单机制) |
| 性能 | 需要查询存储 | 验证签名即可 |
JWT (JSON Web Token)¶
JWT结构:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header(头部)
2. Payload(载荷)
{
// 标准声明(Registered Claims)
"iss": "https://auth.example.com", // 签发者
"sub": "user123", // 主题(用户ID)
"aud": "https://api.example.com", // 受众
"exp": 1735689600, // 过期时间(时间戳)
"nbf": 1735686000, // 生效时间
"iat": 1735686000, // 签发时间
"jti": "uuid-1234", // JWT ID(唯一标识)
// 自定义声明
"username": "zhangsan",
"roles": ["USER", "ADMIN"],
"email": "zhangsan@example.com"
}
3. Signature(签名)
JWT实现(使用jjwt库)¶
<!-- Maven依赖 -->
<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>
/**
* JWT工具类
*/
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:3600000}") // 默认1小时
private long validityInMilliseconds;
private Key key;
@PostConstruct
protected void init() {
// 生成密钥
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成Token
*/
public String createToken(User user) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setSubject(user.getUsername())
.setIssuer("https://auth.example.com")
.setIssuedAt(now)
.setExpiration(validity)
.claim("userId", user.getId())
.claim("email", user.getEmail())
.claim("roles", user.getRoles())
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
/**
* 验证Token
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
/**
* 从Token获取用户名
*/
public String getUsername(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 从Token获取Claims
*/
public Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 检查Token是否即将过期
*/
public boolean isTokenExpiringSoon(String token) {
Claims claims = getClaims(token);
Date expiration = claims.getExpiration();
long timeUntilExpiry = expiration.getTime() - System.currentTimeMillis();
// 如果在15分钟内过期,返回true
return timeUntilExpiry < 900000;
}
}
使用Filter进行JWT认证:
/**
* JWT认证过滤器
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 从请求头获取Token
String token = extractTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
// 从Token获取用户信息
String username = tokenProvider.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 设置到Security上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
/**
* 从请求中提取Token
*/
private String extractTokenFromRequest(HttpServletRequest request) {
// 方式1: 从Authorization header获取
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
// 方式2: 从Cookie获取
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
// 方式3: 从查询参数获取(不推荐,仅用于特殊场景)
return request.getParameter("token");
}
}
JWT最佳实践¶
1. Token刷新策略
方案一:双Token机制(Access Token + Refresh Token)
/**
* 双Token实现
*/
@Service
public class TokenService {
// Access Token: 短期有效(如15分钟)
public String generateAccessToken(User user) {
return Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 900000)) // 15分钟
.claim("type", "access")
.signWith(accessTokenKey)
.compact();
}
// Refresh Token: 长期有效(如7天)
public String generateRefreshToken(User user) {
String token = Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 604800000)) // 7天
.claim("type", "refresh")
.signWith(refreshTokenKey)
.compact();
// 存储Refresh Token到数据库(用于吊销)
refreshTokenRepository.save(new RefreshToken(token, user.getId()));
return token;
}
/**
* 使用Refresh Token获取新的Access Token
*/
public TokenResponse refreshAccessToken(String refreshToken) {
// 验证Refresh Token
if (!validateToken(refreshToken)) {
throw new InvalidTokenException("无效的Refresh Token");
}
// 检查Refresh Token是否在数据库中(未被吊销)
if (!refreshTokenRepository.existsByToken(refreshToken)) {
throw new TokenRevokedException("Refresh Token已被吊销");
}
// 生成新的Access Token
String username = getUsername(refreshToken);
User user = userService.findByUsername(username);
String newAccessToken = generateAccessToken(user);
return new TokenResponse(newAccessToken, refreshToken);
}
}
API端点:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/login")
public TokenResponse login(@RequestBody LoginRequest request) {
User user = authenticationService.authenticate(
request.getUsername(),
request.getPassword()
);
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return new TokenResponse(accessToken, refreshToken);
}
@PostMapping("/refresh")
public TokenResponse refresh(@RequestBody RefreshRequest request) {
return tokenService.refreshAccessToken(request.getRefreshToken());
}
@PostMapping("/logout")
public void logout(@RequestBody LogoutRequest request) {
// 吊销Refresh Token
refreshTokenRepository.deleteByToken(request.getRefreshToken());
// 可选:将Access Token加入黑名单(Redis)
String accessToken = request.getAccessToken();
Claims claims = tokenService.getClaims(accessToken);
long expirationTime = claims.getExpiration().getTime();
long ttl = expirationTime - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
"blacklist:" + accessToken,
"1",
ttl,
TimeUnit.MILLISECONDS
);
}
}
}
2. Token吊销(Revocation)
/**
* Token黑名单检查
*/
@Component
public class TokenBlacklistChecker {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 检查Token是否在黑名单中
*/
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(
redisTemplate.hasKey("blacklist:" + token)
);
}
/**
* 将Token加入黑名单
*/
public void addToBlacklist(String token, long ttlMillis) {
redisTemplate.opsForValue().set(
"blacklist:" + token,
"1",
ttlMillis,
TimeUnit.MILLISECONDS
);
}
}
// 在JWT过滤器中检查黑名单
if (tokenBlacklistChecker.isBlacklisted(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
3. JWT安全建议
/**
* JWT安全配置
*/
public class JwtSecurityConfig {
// ✅ 1. 使用强密钥(至少256位)
private static final String SECRET_KEY = generateStrongSecret(); // 不要硬编码!
private static String generateStrongSecret() {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32]; // 256位
random.nextBytes(secret);
return Base64.getEncoder().encodeToString(secret);
}
// ✅ 2. 使用强签名算法
// 对称: HS512 (推荐) / HS256
// 非对称: RS256, RS512, ES256
// ✅ 3. 设置合理的过期时间
private static final long ACCESS_TOKEN_VALIDITY = 15 * 60 * 1000; // 15分钟
private static final long REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000; // 7天
// ✅ 4. 验证所有声明
public Claims validateAndParseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.requireIssuer("https://auth.example.com") // 验证签发者
.requireAudience("https://api.example.com") // 验证受众
.build()
.parseClaimsJws(token)
.getBody();
}
// ❌ 不要在Payload中存储敏感信息
// Payload是Base64编码,不是加密!
// 不要存储: 密码、信用卡号、SSN等
// ✅ 5. 实现Token轮换(Rotation)
// 每次使用Refresh Token时,颁发新的Refresh Token
// ✅ 6. HTTPS Only
// 仅通过HTTPS传输Token
}
OAuth 2.0协议¶
OAuth 2.0概述¶
OAuth 2.0是什么?
OAuth 2.0是一个授权协议,允许第三方应用在用户授权下访问用户在另一个服务上的资源,而无需获取用户的密码。
核心角色:
┌──────────────┐
│ Resource │ 资源所有者(用户)
│ Owner │
└──────┬───────┘
│ 授权
↓
┌──────────────┐ ┌──────────────┐
│ Client │ ───→ │ Authorization│ 授权服务器
│ (第三方应用) │ ←─── │ Server │
└──────┬───────┘ └──────────────┘
│ Access Token
↓
┌──────────────┐
│ Resource │ 资源服务器
│ Server │
└──────────────┘
示例场景:
用户想让"印象笔记"访问他在"Google Drive"中的文件
- Resource Owner: 用户
- Client: 印象笔记
- Authorization Server: Google认证服务器
- Resource Server: Google Drive API
OAuth 2.0授权模式¶
1. 授权码模式(Authorization Code)¶
最安全、最常用的模式,适用于有后端服务器的Web应用。
流程:
用户 客户端 授权服务器 资源服务器
│ │ │ │
│ 1. 访问应用 │ │ │
│──────────────────→│ │ │
│ │ 2. 重定向到授权页 │ │
│ │───────────────────→│ │
│ 3. 用户登录并授权 │ │ │
│───────────────────────────────────────→│ │
│ │ 4. 返回授权码 │ │
│←────────────────────────────────────────│ │
│ │ 5. 用授权码换Token │ │
│ │───────────────────→│ │
│ │ 6. 返回Access Token │ │
│ │←───────────────────│ │
│ │ 7. 请求资源(带Token)│ │
│ │───────────────────────────────────────→│
│ │ 8. 返回受保护资源 │ │
│ │←───────────────────────────────────────│
详细步骤:
Step 1: 客户端请求授权
GET /oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=https://client.com/callback&
scope=read_profile,read_photos&
state=random_state_string
Host: auth.example.com
参数说明:
- response_type=code: 表示使用授权码模式
- client_id: 客户端ID(应用注册时获得)
- redirect_uri: 授权后的回调地址
- scope: 请求的权限范围
- state: 防CSRF攻击的随机字符串
Step 2: 用户授权 用户登录并同意授权
Step 3: 返回授权码
HTTP/1.1 302 Found
Location: https://client.com/callback?
code=AUTHORIZATION_CODE&
state=random_state_string
Step 4: 客户端用授权码换取Token
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://client.com/callback&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
Step 5: 返回Access Token
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read_profile read_photos"
}
Java实现(使用Spring Security OAuth2):
/**
* OAuth2授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置客户端
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret(passwordEncoder.encode("client-secret"))
.authorizedGrantTypes(
"authorization_code",
"refresh_token",
"password",
"client_credentials"
)
.scopes("read", "write")
.redirectUris("https://client.com/callback")
.accessTokenValiditySeconds(3600) // Access Token 1小时
.refreshTokenValiditySeconds(86400); // Refresh Token 24小时
}
/**
* 配置授权和Token端点
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter());
}
/**
* Token存储(使用JWT)
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* JWT Token转换器
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("jwt-secret-key"); // 生产环境使用非对称密钥
return converter;
}
}
/**
* 资源服务器配置
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}
}
客户端实现:
/**
* OAuth2客户端配置
*/
@Configuration
public class OAuth2ClientConfig {
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context) {
return new OAuth2RestTemplate(resourceDetails(), context);
}
@Bean
public OAuth2ProtectedResourceDetails resourceDetails() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId("client-app");
details.setClientSecret("client-secret");
details.setAccessTokenUri("https://auth.example.com/oauth/token");
details.setUserAuthorizationUri("https://auth.example.com/oauth/authorize");
details.setScope(Arrays.asList("read", "write"));
return details;
}
}
/**
* 使用OAuth2调用API
*/
@Service
public class UserService {
@Autowired
private OAuth2RestTemplate restTemplate;
public UserProfile getUserProfile() {
String url = "https://api.example.com/user/profile";
return restTemplate.getForObject(url, UserProfile.class);
}
}
2. 隐式模式(Implicit)¶
适用于纯前端应用(SPA),已不推荐使用,建议使用授权码模式+PKCE。
流程:
# 1. 请求授权(注意response_type=token)
GET /oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=https://client.com/callback&
scope=read&
state=random_state
# 2. 直接返回Access Token(在URL Fragment中)
HTTP/1.1 302 Found
Location: https://client.com/callback#
access_token=ACCESS_TOKEN&
token_type=Bearer&
expires_in=3600&
state=random_state
安全问题: - Token暴露在URL中,容易泄露 - 浏览器历史记录可能记录Token - 无法使用client_secret(前端代码可见)
3. 密码模式(Resource Owner Password Credentials)¶
用户直接将用户名密码提供给客户端,仅适用于高度信任的应用。
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=password&
username=user@example.com&
password=userpassword&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
scope=read write
/**
* 密码模式实现
*/
@RestController
public class TokenController {
@PostMapping("/oauth/token")
public TokenResponse getToken(@RequestParam("grant_type") String grantType,
@RequestParam("username") String username,
@RequestParam("password") String password) {
if (!"password".equals(grantType)) {
throw new UnsupportedGrantTypeException();
}
// 验证用户名密码
User user = userService.authenticate(username, password);
if (user == null) {
throw new InvalidCredentialsException();
}
// 生成Token
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
return TokenResponse.builder()
.accessToken(accessToken)
.tokenType("Bearer")
.expiresIn(3600)
.refreshToken(refreshToken)
.build();
}
}
4. 客户端模式(Client Credentials)¶
适用于服务器到服务器的通信,不涉及用户。
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
scope=api_access
/**
* 客户端模式实现
*/
@Service
public class ClientCredentialsService {
public String getServiceAccessToken(String clientId, String clientSecret) {
// 验证客户端凭证
Client client = clientRepository.findByClientId(clientId);
if (client == null || !client.checkSecret(clientSecret)) {
throw new InvalidClientException();
}
// 生成Token(不包含用户信息)
return Jwts.builder()
.setSubject(clientId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.claim("client_id", clientId)
.claim("scope", client.getScopes())
.signWith(key)
.compact();
}
}
/**
* 微服务间调用示例
*/
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Inventory checkInventory(String productId) {
// 获取服务访问Token
String token = oauth2Client.getClientCredentialsToken();
// 调用库存服务
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Inventory> response = restTemplate.exchange(
"https://inventory-service/api/products/" + productId,
HttpMethod.GET,
entity,
Inventory.class
);
return response.getBody();
}
}
PKCE扩展(Proof Key for Code Exchange)¶
解决问题: 增强授权码模式的安全性,特别是对于无法安全存储client_secret的公共客户端(移动应用、SPA)。
流程:
1. 客户端生成code_verifier(随机字符串)
2. 客户端计算code_challenge = BASE64URL(SHA256(code_verifier))
3. 请求授权时携带code_challenge
4. 用授权码换Token时携带code_verifier
5. 服务器验证:SHA256(code_verifier) == code_challenge
/**
* PKCE实现
*/
public class PKCEHelper {
/**
* 生成Code Verifier
*/
public static String generateCodeVerifier() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(bytes);
}
/**
* 生成Code Challenge
*/
public static String generateCodeChallenge(String codeVerifier) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
// 请求授权
String codeVerifier = PKCEHelper.generateCodeVerifier();
String codeChallenge = PKCEHelper.generateCodeChallenge(codeVerifier);
String authorizeUrl = "https://auth.example.com/oauth/authorize?" +
"response_type=code&" +
"client_id=CLIENT_ID&" +
"redirect_uri=https://app.com/callback&" +
"code_challenge=" + codeChallenge + "&" +
"code_challenge_method=S256";
// 换取Token时携带code_verifier
POST /oauth/token
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
client_id=CLIENT_ID&
redirect_uri=https://app.com/callback&
code_verifier=CODE_VERIFIER
OpenID Connect (OIDC)¶
OIDC概述¶
OpenID Connect (OIDC) 是基于OAuth 2.0的身份认证层,解决"用户是谁"的问题。
OAuth 2.0 vs OIDC: - OAuth 2.0: 授权协议(你能做什么) - OIDC: 认证协议(你是谁)+ 授权
核心概念: - ID Token: JWT格式,包含用户身份信息 - UserInfo Endpoint: 获取用户详细信息的端点 - Standard Claims: 标准化的用户属性
OIDC流程¶
1. 客户端请求授权(scope包含openid)
2. 用户认证并授权
3. 返回授权码
4. 客户端用授权码换取Token
5. 服务器返回: Access Token + ID Token
6. 客户端验证ID Token
7. (可选)使用Access Token调用UserInfo接口获取更多用户信息
ID Token结构¶
{
// 标准声明
"iss": "https://auth.example.com", // 签发者
"sub": "user123", // 用户唯一标识
"aud": "client-app", // 受众(客户端ID)
"exp": 1735689600, // 过期时间
"iat": 1735686000, // 签发时间
"auth_time": 1735685900, // 用户认证时间
"nonce": "random-nonce", // 防重放攻击
"acr": "urn:mace:incommon:iap:silver", // 认证上下文类
"amr": ["pwd", "mfa"], // 认证方法
"azp": "client-app", // 授权方
// 标准用户声明
"name": "Zhang San",
"given_name": "San",
"family_name": "Zhang",
"middle_name": "",
"nickname": "zhangsan",
"preferred_username": "zhangsan",
"profile": "https://example.com/profile/zhangsan",
"picture": "https://example.com/avatar/zhangsan.jpg",
"website": "https://zhangsan.blog",
"email": "zhangsan@example.com",
"email_verified": true,
"gender": "male",
"birthdate": "1990-01-01",
"zoneinfo": "Asia/Shanghai",
"locale": "zh-CN",
"phone_number": "+86 138 0000 0000",
"phone_number_verified": true,
"address": {
"formatted": "北京市朝阳区某某路123号",
"street_address": "某某路123号",
"locality": "朝阳区",
"region": "北京市",
"postal_code": "100000",
"country": "CN"
},
"updated_at": 1735686000
}
Scope说明¶
openid - 必需,表示使用OIDC
profile - 基本资料(name, picture等)
email - 电子邮件
address - 地址
phone - 电话号码
offline_access - 获取Refresh Token
Java实现(使用Spring Security OAuth2)¶
/**
* OIDC提供者配置
*/
@Configuration
public class OIDCProviderConfig {
/**
* 配置OIDC端点
*/
@Bean
public OAuth2AuthorizationServerConfigurer authorizationServerConfigurer() {
return new OAuth2AuthorizationServerConfigurer()
.oidc(oidc -> oidc
.userInfoEndpoint("/oidc/userinfo")
.clientRegistrationEndpoint("/oidc/register")
);
}
/**
* UserInfo端点实现
*/
@RestController
public class UserInfoController {
@GetMapping("/oidc/userinfo")
public Map<String, Object> userInfo(Authentication authentication) {
OAuth2Authentication oauth2Auth = (OAuth2Authentication) authentication;
String username = oauth2Auth.getName();
User user = userService.findByUsername(username);
Map<String, Object> claims = new HashMap<>();
claims.put("sub", user.getId());
claims.put("name", user.getName());
claims.put("email", user.getEmail());
claims.put("email_verified", user.isEmailVerified());
claims.put("picture", user.getAvatarUrl());
return claims;
}
}
}
/**
* OIDC客户端配置(Spring Boot 2.x/3.x)
*/
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService())
)
);
return http.build();
}
/**
* 自定义OIDC用户服务
*/
@Bean
public OidcUserService oidcUserService() {
OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
// 自定义处理:同步用户到本地数据库
syncUserToDatabase(oidcUser);
return oidcUser;
};
}
}
/**
* 使用OIDC信息
*/
@Controller
public class ProfileController {
@GetMapping("/profile")
public String profile(Model model, @AuthenticationPrincipal OidcUser oidcUser) {
// 从ID Token获取用户信息
String name = oidcUser.getName();
String email = oidcUser.getEmail();
String picture = oidcUser.getPicture();
// 访问所有claims
Map<String, Object> claims = oidcUser.getClaims();
// ID Token
OidcIdToken idToken = oidcUser.getIdToken();
String tokenValue = idToken.getTokenValue();
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("picture", picture);
return "profile";
}
}
application.yml配置:
spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
scope:
- openid
- profile
- email
provider:
google:
issuer-uri: https://accounts.google.com
# 或手动配置端点
# authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
# token-uri: https://oauth2.googleapis.com/token
# user-info-uri: https://openidconnect.googleapis.com/v1/userinfo
# jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
SAML 2.0¶
SAML概述¶
SAML (Security Assertion Markup Language) 是基于XML的企业级单点登录(SSO)标准,主要用于企业环境。
核心概念: - Identity Provider (IdP): 身份提供者(如Active Directory、Okta) - Service Provider (SP): 服务提供者(应用) - Assertion: XML格式的身份断言
SAML vs OAuth/OIDC: - SAML: 企业级、XML、重量级、SSO - OAuth/OIDC: 互联网、JSON、轻量级、API访问
SAML流程(SP-Initiated)¶
用户 服务提供者(SP) 身份提供者(IdP)
│ │ │
│ 1. 访问受保护资源 │ │
│──────────────────→│ │
│ │ 2. 生成SAML请求 │
│ │ 重定向到IdP │
│ │─────────────────→│
│ 3. 用户登录IdP │ │
│─────────────────────────────────────→│
│ │ 4. IdP生成SAML断言│
│ │ 重定向回SP │
│←──────────────────────────────────── │
│ 5. SP验证断言 │ │
│ 创建本地会话 │ │
│←───────────────────│ │
Java实现(使用Spring Security SAML)¶
<!-- Maven依赖 -->
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
/**
* SAML配置
*/
@Configuration
@EnableWebSecurity
public class SAMLConfig extends WebSecurityConfigurerAdapter {
@Value("${saml.idp.metadata.url}")
private String idpMetadataUrl;
@Value("${saml.sp.entity-id}")
private String spEntityId;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/saml/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(saml());
}
/**
* SAML配置器
*/
private SAMLConfigurer saml() {
return new SAMLConfigurer()
.serviceProvider(sp -> sp
.entityId(spEntityId)
.signRequests(true)
.wantAssertionsSigned(true)
)
.identityProvider(idp -> idp
.metadataUrl(idpMetadataUrl)
);
}
/**
* SAML认证成功处理器
*/
@Bean
public SAMLAuthenticationSuccessHandler successHandler() {
return (request, response, authentication) -> {
SAMLCredential credential = (SAMLCredential) authentication.getCredentials();
// 获取用户属性
String nameID = credential.getNameID().getValue();
String email = credential.getAttributeAsString("email");
String displayName = credential.getAttributeAsString("displayName");
// 创建本地用户
User user = userService.findOrCreateUser(nameID, email, displayName);
// 重定向到主页
response.sendRedirect("/home");
};
}
}
单点登录(SSO)¶
SSO概述¶
单点登录(Single Sign-On) 允许用户使用一组凭证登录多个相关但独立的应用系统。
优点: - 用户体验好(一次登录,多处使用) - 减少密码疲劳 - 集中的访问控制 - 统一的安全策略
SSO实现方式¶
1. 基于Cookie的SSO(同域)¶
主域名: example.com
子系统: app1.example.com, app2.example.com, app3.example.com
实现: 设置Cookie的Domain为.example.com
// SSO服务器登录成功后
Cookie ssoCookie = new Cookie("SSO_TOKEN", ssoToken);
ssoCookie.setDomain(".example.com"); // 所有子域名可访问
ssoCookie.setPath("/");
ssoCookie.setHttpOnly(true);
ssoCookie.setSecure(true);
ssoCookie.setMaxAge(3600);
response.addCookie(ssoCookie);
2. CAS (Central Authentication Service)¶
CAS协议流程:
用户 应用A CAS服务器 应用B
│ │ │ │
│ 1. 访问A │ │ │
│──────────────→│ │ │
│ │ 2. 重定向到CAS │ │
│ │───────────────────→│ │
│ 3. 用户登录 │ │ │
│───────────────────────────────────→│ │
│ │ 4. 返回TGT/TGC │ │
│ │ 重定向回A(带ST) │ │
│←────────────────────────────────── │ │
│ │ 5. A验证ST │ │
│ │───────────────────→│ │
│ │ 6. 返回用户信息 │ │
│ │←───────────────────│ │
│ │ 7. A创建会话 │ │
│←───────────────│ │ │
│ 8. 访问B │ │ │
│───────────────────────────────────────────────────→│
│ │ │ 9. 重定向到CAS │
│←────────────────────────────────────────────────────│
│ │ │ 10. CAS检查TGC │
│ │ │ 已登录 │
│ │ │ 11. 直接返回ST │
│ │ │ (无需再登录) │
│←────────────────────────────────────────────────────│
关键概念: - TGT (Ticket Granting Ticket): 票据授予票据,存储在CAS服务器 - TGC (Ticket Granting Cookie): 票据授予Cookie,存储在浏览器 - ST (Service Ticket): 服务票据,一次性使用
Java实现(使用Apereo CAS Client):
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.4</version>
</dependency>
/**
* CAS客户端配置
*/
@Configuration
public class CASClientConfig {
@Value("${cas.server.url}")
private String casServerUrl;
@Value("${cas.client.url}")
private String clientUrl;
/**
* CAS认证过滤器
*/
@Bean
public FilterRegistrationBean<AuthenticationFilter> casAuthenticationFilter() {
FilterRegistrationBean<AuthenticationFilter> registration =
new FilterRegistrationBean<>();
AuthenticationFilter filter = new AuthenticationFilter();
filter.setCasServerLoginUrl(casServerUrl + "/login");
filter.setServerName(clientUrl);
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setOrder(1);
return registration;
}
/**
* CAS票据验证过滤器
*/
@Bean
public FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter>
casValidationFilter() {
FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter> registration =
new FilterRegistrationBean<>();
Cas30ProxyReceivingTicketValidationFilter filter =
new Cas30ProxyReceivingTicketValidationFilter();
filter.setServerName(clientUrl);
filter.setTicketValidator(ticketValidator());
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setOrder(2);
return registration;
}
@Bean
public Cas30ServiceTicketValidator ticketValidator() {
return new Cas30ServiceTicketValidator(casServerUrl);
}
}
/**
* 获取当前用户
*/
@Controller
public class UserController {
@GetMapping("/api/me")
@ResponseBody
public Map<String, Object> getCurrentUser(HttpServletRequest request) {
// 从CAS获取用户信息
AttributePrincipal principal =
(AttributePrincipal) request.getUserPrincipal();
if (principal == null) {
throw new UnauthorizedException();
}
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("username", principal.getName());
userInfo.put("attributes", principal.getAttributes());
return userInfo;
}
}
3. 基于OAuth2/OIDC的SSO¶
现代SSO的主流实现方式,详见前面的OAuth2和OIDC章节。
示例:使用Keycloak实现SSO
# application.yml
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-app
client-secret: secret
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
provider:
keycloak:
issuer-uri: http://keycloak-server/realms/myrealm
user-name-attribute: preferred_username
协议选型指南¶
| 场景 | 推荐协议 | 理由 |
|---|---|---|
| 现代Web应用 | OAuth 2.0 + OIDC | 标准化、广泛支持、轻量级 |
| 企业内部SSO | SAML 2.0 或 OIDC | SAML成熟稳定,OIDC更现代 |
| 移动应用 | OAuth 2.0 + PKCE | 安全、适合公共客户端 |
| 前后端分离 | JWT Token | 无状态、跨域友好 |
| 微服务架构 | OAuth 2.0 Client Credentials | 服务间认证 |
| 传统Web应用 | Session/Cookie | 简单、可靠 |
总结¶
本文档介绍了主流的认证授权协议和标准:
- Cookie/Session: 传统的Web会话管理方式
- JWT: 现代无状态Token认证方案
- OAuth 2.0: 授权协议标准,支持多种授权模式
- OIDC: 基于OAuth 2.0的身份认证层
- SAML: 企业级单点登录标准
- SSO: 多种单点登录实现方式
继续学习: - 上一章:认证授权基础 - 下一章:Java认证框架对比