Spring Security OAuth2实战:手把手搭建认证服务器与资源服务器(JWT+密码模式)

📅 2026/7/4 14:05:10
Spring Security OAuth2实战:手把手搭建认证服务器与资源服务器(JWT+密码模式)
引言在现代微服务架构中安全认证与授权是绕不开的话题。OAuth2 作为业界标准的授权协议能够帮助我们实现第三方应用授权、单点登录以及资源保护。Spring Security 提供了对 OAuth2 的一流支持使得开发者可以快速构建符合标准的认证与资源服务器。本文将聚焦于密码模式Password Grant使用 JWT 令牌手把手带你完成一个从零开始的 OAuth2 实战项目。你将学到OAuth2 的核心角色与流程如何配置 Spring Security OAuth2 授权服务器如何配置资源服务器保护 REST APIJWT 令牌的生成与校验常见问题与注意事项本文示例基于Spring Boot 2.7 Spring Security OAuth2 2.5 JWT所有代码均可直接复制运行。如果你使用的是 Spring Boot 3.x官方推荐使用新的Spring Authorization Server但传统方案在大量存量项目中依然广泛存在且理解它是学习新方案的重要基础。一、OAuth2 核心概念速览在动手之前我们先快速回顾那些容易混淆的角色和概念。1.1 四大角色资源所有者Resource Owner通常就是用户拥有受保护资源。客户端Client想要访问用户资源的第三方应用如手机 App、Web 前段。授权服务器Authorization Server负责认证用户并颁发令牌。资源服务器Resource Server托管受保护资源根据令牌决定是否放行。1.2 四种授权模式授权码模式Authorization Code最安全、最完整适合前后端分离的第三方应用。简化模式Implicit不安全已不推荐。密码模式Resource Owner Password Credentials用户将用户名密码直接交给客户端客户端换取令牌。仅适用于高度信任的应用如官方 App。客户端模式Client Credentials无用户参与客户端以自己的身份访问资源。本文实战选用密码模式因为它最直观也最能体现认证流程的每一步。1.3 JWT 令牌JWTJSON Web Token是一种自包含的令牌格式包含头部、载荷和签名。相比于默认的内存令牌JWT 具有无状态、可扩展、跨域验证等优点。在 OAuth2 中JWT 既可以作为访问令牌Access Token也可以通过非对称加密实现资源服务器直接验证令牌无需频繁访问授权服务器。二、项目环境准备2.1 依赖配置创建一个 Spring Boot 2.7.x 项目pom.xml关键依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.14/version /parent dependencies !-- Spring Security OAuth2 自动配置 -- dependency groupIdorg.springframework.security.oauth.boot/groupId artifactIdspring-security-oauth2-autoconfigure/artifactId version2.6.8/version /dependency !-- JWT 支持 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-jwt/artifactId version1.1.1.RELEASE/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- web、security 基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency /dependencies项目启动类无需特殊注解保持默认即可。三、授权服务器Authorization Server实战授权服务器是整个 OAuth2 流程的心脏负责客户端认证、用户认证并颁发令牌。3.1 Spring Security 基础配置首先定义内存用户用于后续密码模式的身份验证。Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Bean Override protected UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager new InMemoryUserDetailsManager(); manager.createUser(User.withUsername(user) .password({noop}123456) // {noop} 表示不加密仅用于演示 .roles(USER) .build()); manager.createUser(User.withUsername(admin) .password({noop}654321) .roles(ADMIN) .build()); return manager; } Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); // 演示用生产必须用 Bcrypt } Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/oauth/**).permitAll() // 认证接口放行 .anyRequest().authenticated() .and() .formLogin().permitAll(); } }3.2 配置授权服务器EnableAuthorizationServer注解会启用 OAuth2 授权服务器并提供一个默认的端点映射如/oauth/token、/oauth/authorize。Configuration EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { Autowired private PasswordEncoder passwordEncoder; Autowired private AuthenticationManager authenticationManager; // 需要暴露 Autowired private UserDetailsService userDetailsService; // 配置客户端详情 Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(client_app) // 客户端ID .secret(passwordEncoder.encode(123456)) // 客户端密钥需要用加密 .scopes(read, write) // 授权范围 .authorizedGrantTypes(password, refresh_token) // 允许密码模式和令牌刷新 .accessTokenValiditySeconds(7200) // 访问令牌有效期2小时 .refreshTokenValiditySeconds(86400); // 刷新令牌有效期24小时 } // 配置端点将认证管理器与用户服务绑定 Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(jwtTokenStore()) // 使用JWT存储 .accessTokenConverter(jwtAccessTokenConverter()); } // 暴露 AuthenticationManager 为 Bean Bean Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } // JWT 存储与转换器 Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setSigningKey(my-secret-key); // JWT 签名密钥实际应更复杂 return converter; } }代码解释-configure(ClientDetailsServiceConfigurer)定义了哪些客户端可以请求令牌以及它们具备的授权模式、权限范围等。客户端密码必须加密后存储这里我们直接注入PasswordEncoder进行加密。-configure(AuthorizationServerEndpointsConfigurer)绑定了authenticationManager负责用户认证和userDetailsService加载用户并将TokenStore设置为 JWT 存储这样生成的令牌就是 JWT 格式。-jwtTokenStore()和jwtAccessTokenConverter()共同工作JwtAccessTokenConverter负责生成和解析 JWT签名密钥my-secret-key用于对称加密。生产环境中可使用非对称密钥对RSA并配置公私钥。四、资源服务器Resource Server实战资源服务器负责保护 API只有持有有效令牌的请求才能访问。4.1 配置资源服务器Configuration EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { // 使用与授权服务器相同的 JWT 配置 Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setSigningKey(my-secret-key); // 必须与授权服务器一致 return converter; } Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } Override public void configure(ResourceServerSecurityConfigurer resources) { resources.tokenStore(jwtTokenStore()); } // 配置 URL 保护规则 Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/api/public).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated(); } }资源服务器通过EnableResourceServer注入一个 Spring Security 过滤器该过滤器会解析请求中的Authorization: Bearer token头调用TokenStore验证令牌。由于我们使用了 JWT验证是在本地完成的无状态不需要每次请求都去授权服务器询问。4.2 提供测试 APIRestController public class ApiController { GetMapping(/api/public) public String publicApi() { return 这是一个公开接口无需令牌即可访问。; } GetMapping(/api/user) public String userApi(Principal principal) { return 用户 principal.getName() 的资源访问成功; } GetMapping(/api/admin) public String adminApi() { return 管理员专属数据; } }五、项目启动与测试完成以上配置后启动项目我们通过curl或 Postman 进行测试。5.1 密码模式获取令牌请求POST /oauth/token参数如下-grant_typepassword-usernameuser你定义的内存用户-password123456-client_idclient_app-client_secret123456请求示例curl -X POST http://localhost:8080/oauth/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typepasswordusernameuserpassword123456client_idclient_appclient_secret123456成功响应会返回一个 JSON{ access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., token_type: bearer, refresh_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..., expires_in: 7199, scope: read write, jti: uuid }5.2 使用令牌访问受保护资源在请求头中添加Authorization: Bearer access_tokencurl -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... \ http://localhost:8080/api/user返回用户 user 的资源访问成功如果尝试访问/api/admin由于user的角色为ROLE_USER会收到403 Forbidden。换成admin账号获取令牌后即可访问。5.3 刷新令牌当访问令牌过期时可用刷新令牌获取新的访问令牌curl -X POST http://localhost:8080/oauth/token \ -d grant_typerefresh_tokenrefresh_token你的刷新令牌client_idclient_appclient_secret123456六、常见问题与注意事项6.1 为什么使用 NoOpPasswordEncoder 和 {noop}示例中使用了明文存储密码仅用于本地演示。实际项目必须使用BCryptPasswordEncoder等强哈希算法并在用户存储中使用{bcrypt}前缀或直接通过PasswordEncoder编码。客户端密钥同样需要加密存储。6.2 401 Unauthorized 的可能原因请求头格式错误必须是Authorization: Bearer token令牌过期或无效签名资源服务器与授权服务器使用了不同的 JWT 签名密钥客户端 ID 或密钥错误6.3 403 Forbidden 分析403 表示令牌有效但无权限访问该资源。检查资源服务器的权限规则如hasRole(ADMIN)以及令牌中携带的角色信息。默认情况下Spring Security 会将用户的GrantedAuthority序列化到 JWT 载荷中资源服务器反序列化后使用。如果两边角色格式不匹配会导致 403。可以在JwtAccessTokenConverter中自定义转换规则。6.4 使用非对称密钥推荐生产环境建议使用 RSA 非对称密钥这样资源服务器只持有公钥授权服务器持有私钥。实现方式KeyPair keyPair KeyStoreKeyFactory( new ClassPathResource(jwt.jks), password.toCharArray() ).getKeyPair(jwt); converter.setKeyPair(keyPair);资源服务器则通过公钥验证converter.setVerifierKey(public_key_text);6.5 Spring Authorization Server 迁移如果你从spring-security-oauth2-autoconfigure迁移到 Spring Boot 3.x 或更高版本的 Spring Security 5.7应使用新的