杰瑞科技汇

oauth2.0 java 实现

  1. 资源所有者:用户,他们授权第三方应用访问他们的受保护资源。
  2. 客户端:想要访问用户资源的应用程序(你的 Web 应用或移动 App)。
  3. 授权服务器:负责颁发访问令牌 的服务器,它验证用户身份并获取用户授权。
  4. 资源服务器:托管用户受保护资源的服务器,它使用访问令牌来请求进行身份验证和授权。

在实际项目中,授权服务器资源服务器 通常是同一个应用或服务,它们共享密钥。

oauth2.0 java 实现-图1
(图片来源网络,侵删)

我们将使用目前 Java 生态中最流行、最强大的库之一:Spring Security 来构建一个完整的 OAuth 2.0 授权服务器和资源服务器。


核心概念回顾

在开始编码前,快速回顾一下 OAuth 2.0 的四种核心授权模式:

  • 授权码模式:最安全、最常用,适用于服务器端应用,流程是:客户端引导用户到授权服务器 -> 用户登录并授权 -> 授权服务器重定向回客户端并附上一个授权码 -> 客户端用授权码向授权服务器请求访问令牌。
  • 简化模式:适用于纯前端应用,直接从授权服务器获取访问令牌,没有授权码步骤。
  • 密码模式:用户直接将用户名和密码提供给客户端,客户端再用这些信息去请求令牌。仅适用于你完全信任的客户端(你自己的官方第一方应用)。
  • 客户端模式:用于服务到服务的通信,没有用户参与,客户端直接使用自己的凭据(Client ID/Secret)向授权服务器请求令牌。

项目实战:使用 Spring Security 6 构建 OAuth 2.0 服务

我们将创建一个简单的 Spring Boot 项目,它同时扮演授权服务器资源服务器的角色

第 1 步:创建 Spring Boot 项目

使用 Spring Initializr 创建项目,并添加以下依赖:

oauth2.0 java 实现-图2
(图片来源网络,侵删)
  • Spring Web: 用于构建 Web 应用。
  • Spring Security: 核心安全框架。
  • Spring Security OAuth2 Authorization Server: 这是 Spring Security 5.7+ 引入的官方 OAuth 2.0 授权服务器实现,取代了旧的 spring-security-oauth2-autoconfigure
  • Spring Security OAuth2 Resource Server: 用于实现资源服务器功能。

点击 "Generate" 下载项目并导入你的 IDE(如 IntelliJ IDEA 或 VS Code)。

第 2 步:配置授权服务器

src/main/java/com/example/demo 目录下,创建一个配置类来启用和配置授权服务器功能。

SecurityConfig.java

package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.time.Duration;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    // 1. 配置授权服务器 SecurityFilterChain
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults()); // 启用 OpenID Connect 1.0
        http.exceptionHandling(exceptions ->
                exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) // 设置登录页面
                .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults())); // 配置资源服务器以验证 JWT
        return http.build();
    }
    // 2. 配置常规应用的 SecurityFilterChain
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults()); // 启用表单登录
        return http.build();
    }
    // 3. 配置用户存储(用于演示,生产环境应使用数据库)
    @Bean
    public UserDetailsService userDetailsService() {
        return new InMemoryUserDetailsManager(
                User.withUsername("user")
                        .password(passwordEncoder().encode("password"))
                        .roles("USER")
                        .build(),
                User.withUsername("admin")
                        .password(passwordEncoder().encode("admin"))
                        .roles("ADMIN")
                        .build()
        );
    }
    // 4. 配置客户端存储(用于演示,生产环境应使用数据库)
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client-app") // 客户端 ID
                .clientSecret(passwordEncoder().encode("secret123")) // 客户端密钥
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 客户端认证方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 刷新令牌模式
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 客户端凭据模式
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client-app") // 授权码回调地址
                .scope(OidcScopes.OPENID) // OpenID Connect Scope
                .scope("read") // 自定义 Scope
                .scope("write")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) // 要求用户同意授权
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(30)).build()) // 令牌有效期
                .build();
        return new InMemoryRegisteredClientRepository(client);
    }
    // 5. 配置授权服务器设置
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .issuer("http://auth-server:9000") // 授权服务器地址
                .build();
    }
    // 6. 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

代码解释:

oauth2.0 java 实现-图3
(图片来源网络,侵删)
  1. authorizationServerSecurityFilterChain: 这是授权服务器的核心配置,它设置了端点(如 /oauth2/authorize, /oauth2/token)、OIDC 支持、登录入口点和资源服务器验证。
  2. defaultSecurityFilterChain: 这是为应用其他部分(如 /login 页面)配置的安全过滤器链。
  3. userDetailsService: 定义了用户信息,这里我们使用内存中的两个用户 user/passwordadmin/admin
  4. registeredClientRepository: 定义了客户端信息,这是 OAuth 2.0 的核心,它告诉授权服务器哪些客户端是合法的,它们可以请求什么授权类型和 Scope。
    • clientId: client-app
    • clientSecret: secret123
    • authorizationGrantType: 我们支持 AUTHORIZATION_CODE, REFRESH_TOKEN, 和 CLIENT_CREDENTIALS
    • redirectUri: 授权码模式成功后的回调地址。
    • scope: 客户端可以请求的权限范围。
  5. authorizationServerSettings: 设置授权服务器的基本信息,如 issuer URI。
  6. passwordEncoder: 用于对客户端密钥和用户密码进行加密。

第 3 步:配置资源服务器

资源服务器负责验证传入的访问令牌并保护 API 端点,幸运的是,Spring Security 会自动处理大部分事情,我们只需要创建一个受保护的 API 端点。

ResourceController.java

package com.example.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ResourceController {
    // 这是一个公开的端点,不需要认证
    @GetMapping("/public")
    public String publicEndpoint() {
        return "This is a public endpoint.";
    }
    // 这是一个需要 "read" Scope 的受保护端点
    @GetMapping("/resource")
    @PreAuthorize("hasAuthority('SCOPE_read')")
    public String resourceEndpoint(@AuthenticationPrincipal Jwt jwt) {
        return "Hello, " + jwt.getSubject() + "! You have accessed a protected resource with 'read' scope.";
    }
    // 这是一个需要 "write" Scope 的受保护端点
    @GetMapping("/admin")
    @PreAuthorize("hasAuthority('SCOPE_write')")
    public String adminEndpoint(@AuthenticationPrincipal Jwt jwt) {
        return "Hello, Admin " + jwt.getSubject() + "! You have accessed a protected resource with 'write' scope.";
    }
}
  • @PreAuthorize("hasAuthority('SCOPE_read')"): 这是 Spring Security 的方法级安全注解,它确保只有拥有 read Scope 的有效访问令牌才能访问此端点。
  • @AuthenticationPrincipal Jwt jwt: 将当前认证的主体(即解析后的 JWT)注入到方法中,方便我们获取用户信息(如 jwt.getSubject())。

第 4 步:测试流程

我们可以启动应用了,主应用类 DemoApplication.java 应该可以直接运行。

启动后,你将拥有以下服务:

  • 授权服务器: http://localhost:8080 (默认端口)
  • 资源服务器: http://localhost:8080/api (与授权服务器同端口)

我们将使用 Postmancurl 来测试整个流程。

流程 1:获取授权码

  1. 在浏览器中访问以下 URL:

    http://localhost:8080/oauth2/authorize?response_type=code&client_id=client-app&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/client-app&scope=read+write&state=abc123
    • response_type=code: 指定使用授权码模式。
    • client_id: 填写我们在配置中定义的客户端 ID。
    • redirect_uri: 必须与配置中的 redirectUri 完全匹配。
    • scope: 请求 readwrite 权限。
    • state: 一个随机字符串,用于防止 CSRF 攻击。
  2. 浏览器会重定向到 /login 页面,输入我们定义的用户凭据,user / password

  3. 登录后,你会看到一个授权页面,询问你是否授权 client-app 访问你的 readwrite 权限,点击 "Authorize"。

  4. 页面会重定向到 redirect_uri,URL 中会包含一个授权码,

    http://127.0.0.1:8080/login/oauth2/code/client-app?code=YOUR_AUTHORIZATION_CODE&state=abc123

    复制 code 的值。

流程 2:使用授权码换取访问令牌

我们使用上一步获取的授权码来请求访问令牌,这通常是在你的后端服务器(客户端应用)中完成的,而不是在浏览器中。

使用 curl 命令:

curl -X POST http://localhost:8080/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Authorization: Basic Y2xpZW50LWFwcDpzZWNyZXQxMjM=" \
  -d "grant_type=authorization_code" \
  -d "code=YOUR_AUTHORIZATION_CODE" \
  -d "redirect_uri=http://127.0.0.1:8080/login/oauth2/code/client-app"

参数解释:

  • -H "Authorization: Basic Y2xpZW50LWFwcDpzZWNyZXQxMjM=": 这是 client-id:client-secret 进行 Base64 编码的结果。client-app:secret123 -> Y2xpZW50LWFwcDpzZWNyZXQxMjM=
  • grant_type=authorization_code: 指定授权类型。
  • code: 填入你刚刚获取的授权码。
  • redirect_uri: 必须与请求授权码时使用的 redirect_uri 一致。

如果成功,你会收到类似以下的 JSON 响应,包含了 access_token, refresh_token, expires_in 等:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 1799,
    "scope": "read write openid",
    "jti": "..."
}

流程 3:使用访问令牌访问受保护的资源

我们使用刚刚获取的 access_token 来访问之前创建的 /api/resource 端点。

curl -X GET http://localhost:8080/api/resource \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

YOUR_ACCESS_TOKEN 替换为上面返回的 access_token 的值。

如果成功,你会得到:

Hello, user! You have accessed a protected resource with 'read' scope.

如果你尝试访问 /api/admin 端点,并且你的 access_token 只有 read scope,你会收到 403 Forbidden 错误。

curl -X GET http://localhost:8080/api/admin \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

响应:

{
    "error": "insufficient_scope",
    "error_description": "The token does not contain the required scopes."
}

总结与最佳实践

  1. 不要在生产环境使用内存存储:本示例中的 InMemoryRegisteredClientRepositoryInMemoryUserDetailsManager 仅用于演示,在生产环境中,你必须将客户端和用户信息存储在数据库中(如 MySQL, PostgreSQL)。
  2. 选择合适的授权模式
    • 对于 Web 应用,授权码模式 是首选。
    • 对于纯前端 SPA (React, Vue),可以使用 授权码模式 + PKCE (Proof Key for Code Exchange),以增强安全性。
    • 对于服务到服务通信,使用 客户端凭据模式
  3. 安全性
    • 始终使用 HTTPS。
    • 对客户端密钥进行安全存储。
    • 合理设置令牌的有效期 (accessTokenTimeToLive)。
    • 使用强密码哈希算法(如 BCrypt)。
  4. Spring Security 的版本:注意,Spring Security 5.7 引入了新的 spring-security-oauth2-authorization-server 依赖,与旧的 spring-security-oauth2-autoconfigure 有很大不同,新版本功能更强大,配置更灵活,是未来的方向。

通过以上步骤,你已经成功地在 Java (Spring Boot) 中实现了一个功能完备的 OAuth 2.0 授权服务器和资源服务器,并完整地测试了最常用的授权码流程。

分享:
扫描分享到社交APP
上一篇
下一篇