应用安全实践(二):Spring Security核心流程与OAuth 2.0授权

目录

    • 前言
    • 摘要
    • [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 授权服务器配置、资源服务器配置

安全最佳实践

  1. 密码安全:使用BCrypt等强哈希算法,强制密码复杂度
  2. 令牌安全:短过期时间、刷新机制、黑名单管理
  3. 传输安全:强制HTTPS、安全Cookie属性
  4. 审计日志:记录认证、授权、敏感操作事件
  5. 最小权限:默认拒绝、按需授权

思考题

  1. 在微服务架构中,如何设计统一的认证授权方案?

  2. JWT无状态认证与Session有状态认证各有什么优缺点?如何选择?

  3. 如何实现OAuth 2.0的细粒度权限控制(如资源级别的访问控制)?


参考资料

相关推荐
ch.ju2 小时前
Java程序设计(第3版)第二章——java的数据类型:整数
java
程序员清风2 小时前
AI编程最佳实践:一个AI写代码,另一个AI查Bug!
java·后端·面试
计算机学姐2 小时前
基于SpringBoot的高校餐饮档口管理系统
java·vue.js·spring boot·后端·spring·intellij-idea·mybatis
Lyyaoo.2 小时前
【设计模式】工厂模式
java·开发语言·设计模式
派大星酷2 小时前
Java 网络编程全解:TCP、UDP、HTTP、WebSocket
java·网络·tcp/ip
可以简单点2 小时前
分析一个线程日志工具类
java·springboot
进击的野人2 小时前
RAG 最佳实践和调优指南
spring·agent·ai编程
LayJustDoIt2 小时前
Spring 事务为什么会失效?结合真实代码讲清几个常见坑
spring
EvenBoy2 小时前
IDEA中使用Claude Code
java·ide·intellij-idea