You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 3.2.0替代废弃密码凭证模式生成用户AccessToken方案咨询

替代OAuth2密码凭证流的解决方案(Spring Boot 3.2.0)

由于OAuth2的密码凭证流(Password Credentials Grant)已被废弃,结合你的多前端应用认证需求,推荐使用**授权码流(Authorization Code Flow)+ PKCE(Proof Key for Code Exchange)**作为替代方案——这是OAuth2.1规范推荐的安全模式,无需依赖OpenID Connect即可实现前端应用的用户认证,同时天然支持多客户端隔离。

一、核心方案说明

  • 安全优势:授权码流+PKCE避免了前端直接暴露用户密码,防止授权码劫持风险,适配单页应用、原生应用等各类前端场景
  • 多客户端支持:为每个前端分配独立的client-id(可配置client-secret,纯前端应用建议省略secret只使用PKCE),授权服务器通过客户端标识区分不同应用的权限范围与认证规则

二、具体实现步骤

1. 依赖配置(Maven)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

2. 授权服务器核心配置

创建Spring Security配置类,完成客户端注册、用户认证、token规则定义:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.UserDetails;
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.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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import java.util.UUID;

@Configuration
@EnableWebSecurity
public class OAuth2AuthorizationServerConfig {

    // 授权服务器安全规则
    @Bean
    public SecurityFilterChain authorizationServerFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        // 禁用OpenID Connect(无需依赖OIDC)
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(oidc -> oidc.disable());

        // 未认证时跳转登录页
        http.exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));

        return http.build();
    }

    // 通用登录与资源访问规则
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated())
                .formLogin(form -> form.loginPage("/login").permitAll());

        return http.build();
    }

    // 用户信息服务(实际项目建议从数据库读取)
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userA = User.withUsername("user_a")
                .password(passwordEncoder().encode("pass_a123"))
                .roles("APP_USER")
                .build();
        UserDetails userB = User.withUsername("user_b")
                .password(passwordEncoder().encode("pass_b123"))
                .roles("APP_USER")
                .build();
        return username -> switch (username) {
            case "user_a" -> userA;
            case "user_b" -> userB;
            default -> throw new RuntimeException("用户不存在");
        };
    }

    // 密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 多客户端注册(每个前端对应一个客户端)
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        // 前端应用1配置
        RegisteredClient shopFront = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("shop_front")
                .clientSecret(passwordEncoder().encode("shop_secret_001"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://localhost:3000/login/oauth2/code/shop-front")
                .scope("product:read")
                .scope("order:write")
                .clientSettings(settings -> settings.requireAuthorizationConsent(false))
                .build();

        // 前端应用2配置
        RegisteredClient adminFront = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("admin_front")
                .clientSecret(passwordEncoder().encode("admin_secret_002"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://localhost:3001/login/oauth2/code/admin-front")
                .scope("user:manage")
                .scope("system:config")
                .clientSettings(settings -> settings.requireAuthorizationConsent(false))
                .build();

        return new InMemoryRegisteredClientRepository(shopFront, adminFront);
    }
}

3. 前端对接流程(匹配指定OAuth认证流程)

  1. 前端发起授权请求:

    GET /oauth2/authorize?response_type=code&client_id=shop_front&redirect_uri=http://localhost:3000/login/oauth2/code/shop-front&scope=product:read&code_challenge=xxx&code_challenge_method=S256
    

    其中code_challenge由前端通过code_verifier生成并存储,用于后续token请求校验

  2. 用户认证:用户在授权服务器的登录页输入用户名密码,完成身份验证

  3. 获取授权码:授权服务器重定向至前端配置的redirect_uri,携带授权码code

  4. 兑换Access Token:前端发起POST请求获取token:

    POST /oauth2/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=xxx(步骤3返回的授权码)
    &redirect_uri=http://localhost:3000/login/oauth2/code/shop-front
    &client_id=shop_front
    &client_secret=shop_secret_001
    &code_verifier=xxx(前端存储的原始校验值)
    
  5. 调用API:前端使用返回的access_token在请求头中携带Authorization: Bearer {token},访问受保护的后端API

4. 资源服务器配置(可选,用于保护API)

如果需要单独配置资源服务器验证token,可添加如下类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/api/products/**").hasAuthority("SCOPE_product:read")
                        .requestMatchers("/api/orders/**").hasAuthority("SCOPE_order:write")
                        .anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new CustomJwtConverter())));

        return http.build();
    }
}

三、关键注意事项

  • 多客户端隔离:每个前端应用的client-id、redirect_uri、权限范围需独立配置,避免跨应用权限泄露
  • PKCE强制使用:对于无后端的纯前端应用(如Vue/React单页应用),建议省略client-secret,仅通过PKCE保障安全
  • 生产环境优化:客户端信息、用户信息建议从数据库读取,而非内存存储;同时配置HTTPS保障传输安全

内容的提问来源于stack exchange,提问作者SonaliKoripally

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 08:37:39