引言

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐