目录
-
- 前言
- 摘要
- [1. 引言](#1. 引言)
-
- [1.1 Spring Security概述](#1.1 Spring Security概述)
- [1.2 OAuth 2.0简介](#1.2 OAuth 2.0简介)
- [1.3 技术架构概览](#1.3 技术架构概览)
- [2. Spring Security核心架构](#2. Spring Security核心架构)
-
- [2.1 过滤器链架构](#2.1 过滤器链架构)
- [2.2 核心组件解析](#2.2 核心组件解析)
-
- [2.2.1 SecurityContext](#2.2.1 SecurityContext)
- [2.2.2 Authentication](#2.2.2 Authentication)
- [2.2.3 AuthenticationManager](#2.2.3 AuthenticationManager)
- [2.2.4 UserDetailsService](#2.2.4 UserDetailsService)
- [2.3 认证流程源码解析](#2.3 认证流程源码解析)
- [3. Spring Security实战配置](#3. Spring Security实战配置)
-
- [3.1 基础安全配置](#3.1 基础安全配置)
- [3.2 方法级安全控制](#3.2 方法级安全控制)
- [3.3 自定义认证逻辑](#3.3 自定义认证逻辑)
- [4. OAuth 2.0授权协议详解](#4. OAuth 2.0授权协议详解)
-
- [4.1 OAuth 2.0核心概念](#4.1 OAuth 2.0核心概念)
- [4.2 四种授权模式](#4.2 四种授权模式)
-
- [4.2.1 授权码模式(Authorization Code)](#4.2.1 授权码模式(Authorization Code))
- [4.2.2 隐式模式(Implicit)](#4.2.2 隐式模式(Implicit))
- [4.2.3 密码模式(Resource Owner Password Credentials)](#4.2.3 密码模式(Resource Owner Password Credentials))
- [4.2.4 客户端凭证模式(Client Credentials)](#4.2.4 客户端凭证模式(Client Credentials))
- [4.3 授权模式对比](#4.3 授权模式对比)
- [5. JWT令牌机制](#5. JWT令牌机制)
-
- [5.1 JWT结构解析](#5.1 JWT结构解析)
- [5.2 JWT工具类实现](#5.2 JWT工具类实现)
- [5.3 JWT认证过滤器](#5.3 JWT认证过滤器)
- [6. Spring Authorization Server实战](#6. Spring Authorization Server实战)
-
- [6.1 依赖配置](#6.1 依赖配置)
- [6.2 授权服务器配置](#6.2 授权服务器配置)
- [6.3 资源服务器配置](#6.3 资源服务器配置)
- [7. 安全最佳实践](#7. 安全最佳实践)
-
- [7.1 密码安全](#7.1 密码安全)
- [7.2 令牌安全](#7.2 令牌安全)
- [7.3 安全审计日志](#7.3 安全审计日志)
- [8. 总结](#8. 总结)
- 参考资料
前言
在上一篇文章中,我们系统性地介绍了OWASP Top 10中的十大Web安全漏洞及其防护措施。作为应用安全实践系列的第二篇,本文将深入探讨Spring Security这一Java领域最主流的安全框架,以及OAuth 2.0这一现代授权协议。
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,它是保护基于Spring应用程序的事实标准。而OAuth 2.0则是当今互联网上最广泛使用的授权协议,支持第三方应用在用户授权下访问用户资源。两者的结合为现代应用提供了完整的安全解决方案。
摘要
本文深入剖析Spring Security的核心架构与认证授权流程,并详细介绍OAuth 2.0授权协议的原理与实践应用。内容涵盖Spring Security的过滤器链架构、核心组件解析、认证流程源码分析、方法级安全控制,以及OAuth 2.0的四种授权模式、JWT令牌机制、Spring Authorization Server实战等关键技术点。通过本文的学习,读者将掌握企业级应用安全架构的设计与实现能力。
通过本文你将学到:
- Spring Security过滤器链架构与核心组件
- 认证授权流程的完整源码解析
- OAuth 2.0四种授权模式的原理与应用场景
- JWT令牌的生成、验证与刷新机制
- Spring Authorization Server的实战配置
1. 引言

图:Spring Security与OAuth 2.0技术架构概览
1.1 Spring Security概述
Spring Security是一个专注于为Java应用程序提供身份验证和授权的安全框架。它提供了一套全面的安全服务,包括认证、授权、防止攻击(如会话固定、点击劫持、CSRF等),并支持与多种认证方式集成(如LDAP、OAuth 2.0、SAML等)。
Spring Security的核心优势:
| 特性 | 说明 |
|---|---|
| 认证支持 | 支持表单登录、Basic认证、Remember-Me、OAuth 2.0等多种认证方式 |
| 授权控制 | 支持URL级别、方法级别的细粒度授权控制 |
| 防护机制 | 内置CSRF、Session固定、点击劫持等攻击防护 |
| 可扩展性 | 高度模块化的架构,易于扩展和定制 |
| 集成能力 | 与Spring生态系统无缝集成 |
1.2 OAuth 2.0简介
OAuth 2.0是一个开放标准的授权协议,允许用户授权第三方应用访问其在某服务上存储的私有资源(如照片、视频、联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 2.0的核心概念:
| 概念 | 说明 |
|---|---|
| Resource Owner | 资源所有者,通常是终端用户 |
| Client | 第三方应用,请求访问受保护资源 |
| Authorization Server | 授权服务器,颁发访问令牌 |
| Resource Server | 资源服务器,存储受保护资源 |
| Access Token | 访问令牌,用于访问受保护资源的凭证 |
1.3 技术架构概览
数据层
OAuth 2.0
Spring Security
客户端
Web应用
移动应用
单页应用SPA
Security Filter Chain
Authentication Manager
Access Decision Manager
Authorization Server
Resource Server
用户数据库
令牌存储
2. Spring Security核心架构
2.1 过滤器链架构

图:Spring Security过滤器链架构
Spring Security的核心是基于Servlet过滤器链实现的。当一个HTTP请求到达应用程序时,它会经过一系列过滤器的处理,每个过滤器负责特定的安全功能。
HTTP请求
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
Controller
核心过滤器说明:
| 过滤器 | 功能 | 优先级 |
|---|---|---|
| SecurityContextPersistenceFilter | 在请求开始时从Session加载SecurityContext,请求结束时保存 | 最高 |
| CsrfFilter | 防止CSRF攻击,验证请求中的CSRF Token | 高 |
| LogoutFilter | 处理注销请求,清除认证信息 | 高 |
| UsernamePasswordAuthenticationFilter | 处理表单登录认证 | 中 |
| BasicAuthenticationFilter | 处理HTTP Basic认证 | 中 |
| ExceptionTranslationFilter | 处理安全异常,触发认证入口 | 低 |
| FilterSecurityInterceptor | 执行授权检查,验证访问权限 | 最低 |
2.2 核心组件解析
2.2.1 SecurityContext
SecurityContext是存储当前用户认证信息的上下文对象,它持有Authentication对象。
java
// SecurityContext接口定义
public interface SecurityContext extends Serializable {
// 获取当前认证信息
Authentication getAuthentication();
// 设置认证信息
void setAuthentication(Authentication authentication);
}
// 使用示例
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
String username = auth.getName();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
2.2.2 Authentication
Authentication接口代表了用户的认证信息,包括身份标识、凭证和权限。
java
// Authentication接口定义
public interface Authentication extends Principal, Serializable {
// 获取用户权限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 获取凭证(密码等,认证后通常被清除)
Object getCredentials();
// 获取额外详情(如IP地址、Session ID等)
Object getDetails();
// 获取主体标识(通常是用户名)
Object getPrincipal();
// 是否已认证
boolean isAuthenticated();
// 设置认证状态
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
2.2.3 AuthenticationManager
AuthenticationManager是认证管理的核心接口,负责验证Authentication对象。
java
// AuthenticationManager接口定义
public interface AuthenticationManager {
// 认证方法,返回认证成功的Authentication或抛出异常
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
// ProviderManager是AuthenticationManager的默认实现
public class ProviderManager implements AuthenticationManager {
private List<AuthenticationProvider> providers;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 遍历所有Provider,找到支持的Provider进行认证
for (AuthenticationProvider provider : getProviders()) {
if (provider.supports(authentication.getClass())) {
Authentication result = provider.authenticate(authentication);
if (result != null) {
return result;
}
}
}
throw new ProviderNotFoundException("No AuthenticationProvider found");
}
}
2.2.4 UserDetailsService
UserDetailsService是加载用户信息的核心接口,用于从数据源加载用户详情。
java
// UserDetailsService接口定义
public interface UserDetailsService {
// 根据用户名加载用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
// 自定义实现示例
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList()))
.accountExpired(!user.isActive())
.accountLocked(user.isLocked())
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
2.3 认证流程源码解析
Spring Security的认证流程涉及多个组件的协作,下面通过源码分析完整的认证过程。
SecurityContext UserDetailsService DaoAuthenticationProvider AuthenticationManager UsernamePasswordAuthenticationFilter 客户端 SecurityContext UserDetailsService DaoAuthenticationProvider AuthenticationManager UsernamePasswordAuthenticationFilter 客户端 POST /login (username, password) 创建UsernamePasswordAuthenticationToken authenticate(token) authenticate(token) loadUserByUsername(username) 返回UserDetails 验证密码 创建认证成功的Token 返回已认证Token 返回已认证Token 存储认证信息 重定向到成功页面
认证流程核心代码:
java
// UsernamePasswordAuthenticationFilter核心逻辑
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 获取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 2. 创建未认证的Token
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 3. 设置详情信息
setDetails(request, authRequest);
// 4. 调用AuthenticationManager进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// DaoAuthenticationProvider核心逻辑
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
try {
// 调用UserDetailsService加载用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null");
}
return loadedUser;
} catch (UsernameNotFoundException ex) {
throw new BadCredentialsException("用户名或密码错误");
}
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
// 加载用户信息
UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
// 验证用户状态
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
// 验证密码
if (authentication.getCredentials() == null) {
throw new BadCredentialsException("密码不能为空");
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, user.getPassword())) {
throw new BadCredentialsException("用户名或密码错误");
}
// 创建认证成功的Token
return createSuccessAuthentication(authentication, user);
}
}
3. Spring Security实战配置
3.1 基础安全配置
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register", "/login").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
)
// 配置注销
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID", "remember-me")
.permitAll()
)
// 配置Remember Me
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400) // 1天
.rememberMeParameter("remember-me")
)
// 配置Session管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
)
// 配置CSRF
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/**")
)
// 配置安全Headers
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://cdn.jsdelivr.net"
))
)
// 配置异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密
return new BCryptPasswordEncoder();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint("/login");
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
上述配置类展示了Spring Security的完整安全配置。它定义了URL级别的访问控制规则,配置了表单登录、注销、Remember Me、Session管理、CSRF防护和安全Headers等功能。@EnableMethodSecurity注解启用了方法级别的安全控制,允许在方法上使用@PreAuthorize、@Secured等注解进行权限控制。
3.2 方法级安全控制
java
@Service
public class OrderService {
// 只有ADMIN角色可以访问
@Secured("ROLE_ADMIN")
public List<Order> findAllOrders() {
return orderRepository.findAll();
}
// 只有订单所有者或ADMIN可以访问
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(authentication, #orderId)")
public Order getOrderById(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("订单不存在"));
}
// 只有订单所有者可以修改
@PreAuthorize("@orderSecurity.isOwner(authentication, #orderId)")
public void updateOrder(Long orderId, OrderDTO orderDTO) {
Order order = getOrderById(orderId);
// 更新逻辑...
}
// 方法执行后验证
@PostAuthorize("returnObject.owner.username == authentication.name or hasRole('ADMIN')")
public Order getOrderByUserId(Long userId) {
return orderRepository.findByUserId(userId);
}
// 过滤返回结果
@PostFilter("filterObject.owner.username == authentication.name or hasRole('ADMIN')")
public List<Order> getUserOrders() {
return orderRepository.findAll();
}
}
3.3 自定义认证逻辑
java
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private AuditLogService auditLogService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// 记录登录日志
auditLogService.logLogin(authentication.getName(), request.getRemoteAddr(), true);
// 更新最后登录时间
userService.updateLastLoginTime(authentication.getName());
// 根据用户角色重定向到不同页面
Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
if (roles.contains("ROLE_ADMIN")) {
response.sendRedirect("/admin/dashboard");
} else {
response.sendRedirect("/user/dashboard");
}
}
}
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private LoginAttemptService loginAttemptService;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
String username = request.getParameter("username");
String ip = request.getRemoteAddr();
// 记录失败尝试
loginAttemptService.recordFailedAttempt(username, ip);
// 根据异常类型返回不同错误信息
String errorMessage;
if (exception instanceof BadCredentialsException) {
errorMessage = "用户名或密码错误";
} else if (exception instanceof DisabledException) {
errorMessage = "账户已被禁用";
} else if (exception instanceof AccountExpiredException) {
errorMessage = "账户已过期";
} else if (exception instanceof LockedException) {
errorMessage = "账户已被锁定";
} else if (exception instanceof CredentialsExpiredException) {
errorMessage = "密码已过期";
} else {
errorMessage = "登录失败,请稍后重试";
}
response.sendRedirect("/login?error=" + URLEncoder.encode(errorMessage, "UTF-8"));
}
}
4. OAuth 2.0授权协议详解
4.1 OAuth 2.0核心概念
OAuth 2.0定义了四个角色:
| 角色 | 说明 | 示例 |
|---|---|---|
| Resource Owner | 资源所有者,能够授予对受保护资源的访问权限 | 微信用户 |
| Resource Server | 资源服务器,托管受保护资源 | 微信API服务器 |
| Client | 客户端,请求访问受保护资源的应用 | 第三方网站 |
| Authorization Server | 授权服务器,颁发访问令牌 | 微信开放平台 |
4.2 四种授权模式

图:OAuth 2.0授权模式选择流程
OAuth 2.0定义了四种授权模式,适用于不同的应用场景:
授权模式选择
是
否
是
否
是
否
应用类型判断
是否有后端服务?
是否高度可信?
隐式模式
Implicit
密码模式
Resource Owner Password Credentials
是否是原生应用?
授权码+PKCE模式
授权码模式
Authorization Code
4.2.1 授权码模式(Authorization Code)
授权码模式是最完整、最安全的OAuth 2.0授权模式,适用于有后端服务的Web应用。
资源服务器 授权服务器 客户端 用户 资源服务器 授权服务器 客户端 用户 点击"使用微信登录" 重定向到授权页面 response_type=code 显示授权确认页面 同意授权 重定向回客户端,携带授权码code 使用code换取access_token POST /oauth/token 返回access_token和refresh_token 使用access_token请求资源 返回用户资源
授权码模式流程详解:
text
Step 1: 引导用户到授权页面
GET /oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=https://client.example.com/callback&
scope=read:user,write:post&
state=RANDOM_STATE
Step 2: 用户授权后,重定向回客户端
HTTP/1.1 302 Found
Location: https://client.example.com/callback?
code=AUTHORIZATION_CODE&
state=RANDOM_STATE
Step 3: 客户端使用授权码换取令牌
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://client.example.com/callback&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
Step 4: 授权服务器返回令牌
{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN",
"scope": "read:user write:post"
}
4.2.2 隐式模式(Implicit)
隐式模式适用于纯前端应用(SPA),直接在前端获取访问令牌。由于安全性较低,现在已不推荐使用。
text
Step 1: 引导用户到授权页面
GET /oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=https://client.example.com/callback&
scope=read:user&
state=RANDOM_STATE
Step 2: 授权后,令牌直接在URL片段中返回
HTTP/1.1 302 Found
Location: https://client.example.com/callback#
access_token=ACCESS_TOKEN&
token_type=Bearer&
expires_in=3600&
scope=read:user&
state=RANDOM_STATE
4.2.3 密码模式(Resource Owner Password Credentials)
密码模式适用于高度可信的客户端(如官方移动应用),用户直接将密码提供给客户端。
text
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&
username=USER_USERNAME&
password=USER_PASSWORD&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
scope=read:user
Response:
{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN"
}
4.2.4 客户端凭证模式(Client Credentials)
客户端凭证模式适用于服务间通信,客户端以自己的身份获取令牌。
text
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
scope=service:read
Response:
{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "service:read"
}
4.3 授权模式对比
| 授权模式 | 安全性 | 适用场景 | 是否需要用户参与 | 是否需要客户端密钥 |
|---|---|---|---|---|
| 授权码模式 | 最高 | Web应用、移动应用 | 是 | 是 |
| 授权码+PKCE | 高 | 原生应用、SPA | 是 | 否 |
| 隐式模式 | 低 | 纯前端应用(已废弃) | 是 | 否 |
| 密码模式 | 中 | 官方应用、高度可信客户端 | 是 | 是 |
| 客户端凭证 | 中 | 服务间通信 | 否 | 是 |
5. JWT令牌机制
5.1 JWT结构解析
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT由三部分组成:Header、Payload和Signature。
text
JWT格式: xxxxx.yyyyy.zzzzz
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header(头部):
json
{
"alg": "HS256",
"typ": "JWT"
}
Payload(载荷):
json
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["USER", "ADMIN"],
"permissions": ["read:user", "write:post"]
}
Signature(签名):
text
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
5.2 JWT工具类实现
java
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}") // 默认24小时
private long jwtExpiration;
@Value("${jwt.refresh-expiration:604800000}") // 默认7天
private long refreshExpiration;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成访问令牌
public String generateAccessToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey())
.compact();
}
// 生成刷新令牌
public String generateRefreshToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + refreshExpiration))
.claim("type", "refresh")
.signWith(getSigningKey())
.compact();
}
// 解析令牌
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
// 从令牌获取用户名
public String getUsernameFromToken(String token) {
return parseToken(token).getSubject();
}
// 验证令牌
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
// 检查令牌是否过期
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
5.3 JWT认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenBlacklistService tokenBlacklistService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求头获取JWT
String jwt = resolveToken(request);
// 2. 验证JWT
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
// 检查是否在黑名单中
if (tokenBlacklistService.isBlacklisted(jwt)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token已失效");
return;
}
// 3. 从JWT获取用户名
String username = jwtTokenProvider.getUsernameFromToken(jwt);
// 4. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 6. 设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
6. Spring Authorization Server实战
6.1 依赖配置
xml
<dependencies>
<!-- Spring Authorization Server -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.2.0</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
6.2 授权服务器配置
java
@Configuration
public class AuthorizationServerConfig {
// 注册客户端
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret("{bcrypt}$2a$10$XYZ...") // BCrypt加密
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://localhost:8080/login/oauth2/code/web-client")
.redirectUri("http://localhost:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("read:user")
.scope("write:post")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.reuseRefreshTokens(false)
.build())
.build();
RegisteredClient mobileClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("mobile-client")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE) // 公共客户端
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("myapp://callback")
.scope("read:user")
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // 启用PKCE
.build())
.build();
return new InMemoryRegisteredClientRepository(webClient, mobileClient);
}
// 授权信息存储
@Bean
public AuthorizationService authorizationService() {
return new InMemoryAuthorizationService();
}
// 授权同意信息存储
@Bean
public AuthorizationConsentService authorizationConsentService(
RegisteredClientRepository registeredClientRepository) {
return new InMemoryAuthorizationConsentService();
}
// JWK源(用于签名JWT)
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
// 授权服务器设置
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:9000")
.authorizationEndpoint("/oauth2/authorize")
.tokenEndpoint("/oauth2/token")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.tokenRevocationEndpoint("/oauth2/revoke")
.jwkSetEndpoint("/oauth2/jwks")
.oidcUserInfoEndpoint("/userinfo")
.build();
}
// 授权服务器Security配置
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // 启用OpenID Connect
http
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(server -> server
.jwt(Customizer.withDefaults()));
return http.build();
}
}
6.3 资源服务器配置
java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
@Order(2)
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasAuthority("SCOPE_read:user")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return converter;
}
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation("http://localhost:9000");
}
}
7. 安全最佳实践
7.1 密码安全
java
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt,强度为12
return new BCryptPasswordEncoder(12);
}
}
@Service
public class PasswordService {
@Autowired
private PasswordEncoder passwordEncoder;
// 安全的密码哈希
public String hashPassword(String rawPassword) {
return passwordEncoder.encode(rawPassword);
}
// 验证密码
public boolean matches(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
// 检查密码强度
public PasswordStrength checkStrength(String password) {
int score = 0;
if (password.length() >= 8) score++;
if (password.length() >= 12) score++;
if (password.matches(".*[a-z].*")) score++;
if (password.matches(".*[A-Z].*")) score++;
if (password.matches(".*\\d.*")) score++;
if (password.matches(".*[!@#$%^&*].*")) score++;
// 检查常见弱密码
if (isCommonPassword(password)) {
return PasswordStrength.WEAK;
}
if (score >= 5) return PasswordStrength.STRONG;
if (score >= 3) return PasswordStrength.MEDIUM;
return PasswordStrength.WEAK;
}
}
7.2 令牌安全
java
@Service
public class TokenSecurityService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String TOKEN_BLACKLIST_PREFIX = "token:blacklist:";
private static final String USER_TOKENS_PREFIX = "user:tokens:";
// 将令牌加入黑名单
public void blacklistToken(String token, long expirationTime) {
String key = TOKEN_BLACKLIST_PREFIX + token;
long ttl = expirationTime - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(key, "revoked", ttl, TimeUnit.MILLISECONDS);
}
}
// 检查令牌是否在黑名单中
public boolean isBlacklisted(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
// 记录用户的所有活跃令牌
public void recordUserToken(String username, String token) {
String key = USER_TOKENS_PREFIX + username;
redisTemplate.opsForSet().add(key, token);
}
// 撤销用户所有令牌
public void revokeAllUserTokens(String username) {
String key = USER_TOKENS_PREFIX + username;
Set<String> tokens = redisTemplate.opsForSet().members(key);
if (tokens != null) {
for (String token : tokens) {
blacklistToken(token, Long.MAX_VALUE);
}
}
redisTemplate.delete(key);
}
}
7.3 安全审计日志
java
@Aspect
@Component
@Slf4j
public class SecurityAuditAspect {
@Autowired
private AuditLogRepository auditLogRepository;
// 记录认证事件
@AfterReturning(pointcut = "execution(* org.springframework.security.authentication.AuthenticationManager.authenticate(..))",
returning = "result")
public void logAuthenticationSuccess(JoinPoint joinPoint, Authentication result) {
AuditLog log = AuditLog.builder()
.eventType("AUTHENTICATION_SUCCESS")
.username(result.getName())
.ipAddress(getCurrentIpAddress())
.timestamp(LocalDateTime.now())
.details("用户登录成功")
.build();
auditLogRepository.save(log);
}
// 记录授权失败
@AfterThrowing(pointcut = "execution(* org.springframework.security.access.AccessDecisionManager.decide(..))",
throwing = "ex")
public void logAccessDenied(JoinPoint joinPoint, AccessDeniedException ex) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
AuditLog log = AuditLog.builder()
.eventType("ACCESS_DENIED")
.username(auth != null ? auth.getName() : "anonymous")
.ipAddress(getCurrentIpAddress())
.timestamp(LocalDateTime.now())
.details("访问被拒绝: " + ex.getMessage())
.build();
auditLogRepository.save(log);
}
// 记录敏感操作
@Around("@annotation(auditLog)")
public Object logSensitiveOperation(ProceedingJoinPoint joinPoint, AuditLogAnnotation auditLog)
throws Throwable {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 执行前记录
log.info("用户 {} 开始执行敏感操作: {}",
auth.getName(), auditLog.operation());
Object result = joinPoint.proceed();
// 执行后记录
AuditLog record = AuditLog.builder()
.eventType(auditLog.operation())
.username(auth.getName())
.ipAddress(getCurrentIpAddress())
.timestamp(LocalDateTime.now())
.details(auditLog.description())
.build();
auditLogRepository.save(record);
return result;
}
private String getCurrentIpAddress() {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
8. 总结
本文深入探讨了Spring Security的核心架构与认证授权流程,并详细介绍了OAuth 2.0授权协议的原理与实践应用。
核心要点总结:
| 主题 | 核心内容 |
|---|---|
| Spring Security架构 | 过滤器链、SecurityContext、AuthenticationManager |
| 认证流程 | UsernamePasswordAuthenticationFilter → AuthenticationManager → AuthenticationProvider |
| 授权控制 | URL级别、方法级别、SpEL表达式 |
| OAuth 2.0模式 | 授权码、隐式、密码、客户端凭证四种模式 |
| JWT机制 | Header.Payload.Signature结构、生成与验证 |
| Spring Authorization Server | 授权服务器配置、资源服务器配置 |
安全最佳实践:
- 密码安全:使用BCrypt等强哈希算法,强制密码复杂度
- 令牌安全:短过期时间、刷新机制、黑名单管理
- 传输安全:强制HTTPS、安全Cookie属性
- 审计日志:记录认证、授权、敏感操作事件
- 最小权限:默认拒绝、按需授权
思考题:
-
在微服务架构中,如何设计统一的认证授权方案?
-
JWT无状态认证与Session有状态认证各有什么优缺点?如何选择?
-
如何实现OAuth 2.0的细粒度权限控制(如资源级别的访问控制)?