Spring Security OAuth2实战:手把手搭建认证服务器与资源服务器(JWT+密码模式)
2026/7/4 14:05:03 网站建设 项目流程

引言

在现代微服务架构中,安全认证与授权是绕不开的话题。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+,应使用新的

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询