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

如何在Spring Boot API中同时实现OAuth2.0/OpenID Connect与内部认证?

适配Spring Authorization Server的内部账号JWT令牌获取方案

针对你的场景——既要保留内部邮箱密码登录(复用Angular现有表单),又要引入OAuth2/OIDC外部登录,同时规避已被弃用的password授权类型,以下是一套符合规范且安全的实现方案:

核心思路

利用Spring Security的会话认证机制,结合授权码流(带PKCE)的无交互授权流程:前端先通过REST接口完成账号密码验证并建立认证会话,再通过已认证的会话直接获取授权码,最终兑换JWT令牌。既规避了password授权的风险,又复用了现有前端登录表单。

1. 实现REST式内部登录接口

不要使用OAuth的password授权,而是基于Spring Security的AuthenticationManager实现自定义登录接口,验证用户凭证后创建认证会话:

@RestController
@RequestMapping("/api/auth")
public class InternalAuthController {
    private final AuthenticationManager authenticationManager;

    public InternalAuthController(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public ResponseEntity<Void> login(@RequestBody LoginRequest request, HttpServletRequest servletRequest) {
        // 构造认证令牌
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                request.getEmail(), request.getPassword()
        );
        // 执行认证逻辑
        Authentication authentication = authenticationManager.authenticate(authToken);
        // 将认证信息存入安全上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 创建并绑定会话
        HttpSession session = servletRequest.getSession(true);
        session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                            SecurityContextHolder.getContext());
        return ResponseEntity.ok().build();
    }

    // 登录请求DTO
    public static class LoginRequest {
        private String email;
        private String password;
        
        // Getters & Setters
    }
}

2. 配置Spring Authorization Server

重点配置客户端支持授权码流+PKCE,同时允许已认证用户自动获取授权码:

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;

    public AuthServerConfig(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("angular-spa-client")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:4200/auth/callback") // 前端回调地址
                .scopes("openid", "profile", "api:read", "api:write")
                .autoApprove(true) // 第一方应用自动授权,无需用户确认
                .requireSecret(false) // SPA无需客户端密钥,依赖PKCE保障安全
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(jwtTokenStore());
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("your-signing-key"); // 生产环境使用非对称密钥
        return converter;
    }
}

3. 前端登录流程调整

按照以下步骤实现Angular端的登录逻辑:

  • 用户输入邮箱密码后,POST请求/api/auth/login,后端返回200则表示会话建立成功(会话Cookie会自动存入浏览器)。
  • 生成PKCE的code_verifier和code_challenge(采用SHA-256哈希)。
  • 构造授权码请求:GET访问/oauth2/authorize,携带参数client_id=angular-spa-client、redirect_uri=http://localhost:4200/auth/callback、response_type=code、scope=openid profile api:read、code_challenge={your-challenge}、code_challenge_method=S256。
  • 由于会话已认证,授权服务器直接跳转到回调地址并携带授权码code。
  • 前端用授权码+code_verifierPOST请求/oauth2/token,获取JWT访问令牌、ID令牌和刷新令牌。
  • 后续API请求携带JWT令牌的Authorization: Bearer {token}头。

4. 资源服务器支持多签发方JWT

配置Spring Security OAuth Resource Server,同时验证内部授权服务器和外部OIDC提供商签发的JWT:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt()
                .decoder(multiIssuerJwtDecoder());
    }

    @Bean
    public JwtDecoder multiIssuerJwtDecoder() {
        // 内部授权服务器的JWKS端点
        NimbusJwtDecoder internalDecoder = NimbusJwtDecoder.withJwkSetUri("http://your-auth-server/oauth2/jwks").build();
        // 外部OIDC提供商的JWKS端点
        NimbusJwtDecoder externalDecoder = NimbusJwtDecoder.withJwkSetUri("https://external-oidc-provider/.well-known/jwks.json").build();

        // 根据签发方动态选择解码器
        return new DelegatingJwtDecoder(issuer -> {
            if ("http://your-auth-server/oauth2".equals(issuer)) {
                return internalDecoder;
            } else if ("https://external-oidc-provider".equals(issuer)) {
                return externalDecoder;
            }
            throw new IllegalArgumentException("未知的JWT签发方: " + issuer);
        });
    }
}

关键注意事项

  • 安全配置:生产环境必须使用HTTPS,会话Cookie需设置HttpOnly=true、Secure=true、SameSite=Strict,防止XSS和CSRF攻击。
  • PKCE必要性:SPA是公开客户端,必须使用PKCE防止授权码被劫持,不能依赖客户端密钥。
  • 密钥管理:JWT签名密钥生产环境需使用RSA非对称密钥,避免对称密钥泄露风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 08:48:21