引言
在现代微服务架构中,安全认证与授权是绕不开的话题。OAuth2 作为业界标准的授权协议,能够帮助我们实现第三方应用授权、单点登录以及资源保护。Spring Security 提供了对 OAuth2 的一流支持,使得开发者可以快速构建符合标准的认证与资源服务器。本文将聚焦于密码模式(Password Grant),使用 JWT 令牌,手把手带你完成一个从零开始的 OAuth2 实战项目。你将学到:
- OAuth2 的核心角色与流程
- 如何配置 Spring Security OAuth2 授权服务器
- 如何配置资源服务器保护 REST API
- JWT 令牌的生成与校验
- 常见问题与注意事项
本文示例基于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 令牌
JWT(JSON Web Token)是一种自包含的令牌格式,包含头部、载荷和签名。相比于默认的内存令牌,JWT 具有无状态、可扩展、跨域验证等优点。在 OAuth2 中,JWT 既可以作为访问令牌(Access Token),也可以通过非对称加密实现资源服务器直接验证令牌,无需频繁访问授权服务器。
二、项目环境准备
2.1 依赖配置
创建一个 Spring Boot 2.7.x 项目,pom.xml关键依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.14</version> </parent> <dependencies> <!-- Spring Security OAuth2 自动配置 --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.6.8</version> </dependency> <!-- JWT 支持 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.1.1.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- web、security 基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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 提供测试 API
@RestController 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_type=password
-username=user(你定义的内存用户)
-password=123456
-client_id=client_app
-client_secret=123456
请求示例:
curl -X POST "http://localhost:8080/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password&username=user&password=123456&client_id=client_app&client_secret=123456"成功响应会返回一个 JSON:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 7199, "scope": "read write", "jti": "uuid" }5.2 使用令牌访问受保护资源
在请求头中添加Authorization: Bearer <access_token>:
curl -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_type=refresh_token&refresh_token=你的刷新令牌&client_id=client_app&client_secret=123456"六、常见问题与注意事项
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+,应使用新的