Spring Security OAuth2实战:手把手搭建认证服务器与资源服务器(JWT+密码模式)
引言
在现代微服务架构中,安全认证与授权是绕不开的话题。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+,应使用新的
更多推荐



所有评论(0)