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

如何在Spring Authorization Server中实现无/login页的OAuth2授权码流程

基于Spring Authorization Server的自定义登录&Forward优化配置方案

一、OAuth2客户端配置

核心是修改未认证时的跳转逻辑为forward,替换默认的重定向过滤器:

1. 自定义授权请求转发过滤器

替换默认的OAuth2AuthorizationRequestRedirectFilter,将重定向改为forward:

public class ForwardingOAuth2AuthorizationRequestRedirectFilter extends OAuth2AuthorizationRequestRedirectFilter {

    public ForwardingOAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) {
        super(clientRegistrationRepository);
    }

    @Override
    protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String redirectUri) throws IOException {
        try {
            request.getRequestDispatcher(redirectUri).forward(request, response);
        } catch (ServletException e) {
            throw new IOException("Failed to forward to authorization endpoint", e);
        }
    }
}

2. 客户端SecurityFilterChain配置

调整未认证入口,绑定自定义过滤器:

@Bean
public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(endpoint -> endpoint
                .authorizationRequestRedirectFilter(new ForwardingOAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()))
            )
            .redirectionEndpoint(endpoint -> endpoint.baseUri("/login/oauth2/code/*"))
        )
        .exceptionHandling(ex -> ex
            .authenticationEntryPoint((request, response, authException) -> {
                // 未认证时forward到授权请求端点
                String clientId = "your-client-id"; // 可根据业务动态获取
                request.getRequestDispatcher("/oauth2/authorization/" + clientId).forward(request, response);
            })
        );
    return http.build();
}

@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
    return new InMemoryClientRegistrationRepository(
        ClientRegistration.withRegistrationId("your-client-id")
            .clientId("client-id")
            .clientSecret("client-secret")
            .authorizationUri("http://auth-server:8080/oauth2/authorize")
            .tokenUri("http://auth-server:8080/oauth2/token")
            .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile")
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .clientName("Your Client")
            .build()
    );
}

二、授权服务配置

核心是实现自定义登录API,登录成功后forward到客户端回调端点:

1. 自定义登录接口

处理前端登录请求,认证通过后转发到客户端回调:

@RestController
public class CustomLoginController {

    private final AuthenticationManager authenticationManager;
    private final OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepo;

    public CustomLoginController(AuthenticationManager authenticationManager,
                                 OAuth2AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepo) {
        this.authenticationManager = authenticationManager;
        this.authorizationRequestRepo = authorizationRequestRepo;
    }

    @PostMapping("/login")
    public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 执行用户名密码认证
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
            loginRequest.getUsername(), loginRequest.getPassword()
        );
        Authentication auth = authenticationManager.authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);

        // 加载之前保存的授权请求
        OAuth2AuthorizationRequest authRequest = authorizationRequestRepo.loadAuthorizationRequest(request);
        if (authRequest == null) {
            throw new IllegalStateException("No pending authorization request found");
        }

        // Forward到客户端回调端点
        request.getRequestDispatcher(authRequest.getRedirectUri()).forward(request, response);
    }

    // 登录请求DTO
    public static class LoginRequest {
        private String username;
        private String password;

        // Getters & Setters
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }
}

2. 授权服务Security&AuthorizationServer配置

@Configuration
@EnableWebSecurity
public class AuthServerSecurityConfig {

    @Bean
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http
            .exceptionHandling(ex -> ex
                // 未认证时重定向到前端登录页面
                .authenticationEntryPoint((request, response, authException) -> response.sendRedirect("/your-front-login-page"))
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("client-id")
            .clientSecret("{noop}client-secret") // 生产环境用BCrypt加密
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("http://client:8081/login/oauth2/code/your-client-id")
            .scope("openid")
            .scope("profile")
            .build();
        return new InMemoryRegisteredClientRepository(client);
    }

    // JWT相关配置
    @Bean
    public JwtDecoder jwtDecoder(JwkSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public JwkSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        return (selector, context) -> selector.select(new JWKSet(rsaKey));
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    // 认证管理器配置
    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

关键注意事项

  • Forward的局限性:Forward仅能在同Servlet容器或可内部路由的场景生效,如果客户端和授权服务跨域独立部署,此方式会失效,需改用授权码传递的其他方案。
  • 授权请求存储:默认用Session存储授权请求,跨服务部署时需改用Redis等分布式Session共享方案,否则forward时无法加载授权请求。
  • 前端配合:前端需实现自定义登录页面,调用授权服务的/login接口完成认证,后续流程由服务端自动转发处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:45:05