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

OAuth2授权服务器v1.2.3免表单登录认证失效问题排查

OAuth2授权服务器自动认证失效问题

背景

在OAuth2授权服务器v0.3.1版本中,通过自定义customAuthenticationEntryPoint()实现了无需登录表单直接完成用户认证的功能,但升级至v1.2.3版本后,该认证流程无法正常执行。

请求地址

http://localhost:8080/oauth2/authorize?response_type=code&client_id=hts-client-id&scope=openid&redirect_uri=http://127.0.0.1:8081

期望结果

无需跳转登录表单,直接重定向至携带授权码的回调地址:

http://127.0.0.1:8081/?code=alwdijnzldjiwilajiwdj

问题原因

Spring Authorization Server v1.x版本对认证流程的处理逻辑做了调整,原版本中通过AuthenticationEntryPoint设置认证上下文后重定向固定路径的方式,无法正确触发授权码流程的后续步骤:

  • 直接设置SecurityContextHolder的认证信息后,重定向的/oauth2/authorization/code并非正确的授权续接路径
  • 新版本要求认证完成后需重新触发原授权请求(保留所有参数),而非自定义跳转

解决方案

修改自定义AuthenticationEntryPoint的逻辑,在设置认证上下文后重定向回原授权请求地址(保留所有参数),同时调整SecurityFilterChain的配置确保授权端点权限正确。

修改后的完整配置代码

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import io.oauth2.autorizationserver.oauth2.repository.RegisteredClientJpaRepository;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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.web.AuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.provisioning.InMemoryUserDetailsManager;
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.jwt.JwtDecoder;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.List;
import java.util.UUID;

@Configuration
@RequiredArgsConstructor
@Slf4j
public class AuthorizationServerConfig {
    private final UserDetailsService userDetailsService;
    private final RegisteredClientJpaRepository registeredClientEntityRepository;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        List<String> originUris = List.of("http://127.0.0.1:8081", "http://localhost:8080");
        originUris.forEach(config::addAllowedOrigin);
        config.addAllowedHeader("*");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("GET");
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return source;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        http
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                customAuthenticationEntryPoint(),
                                new MediaTypeRequestMatcher(MediaType.ALL))
                )
                .cors(Customizer.withDefaults())
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()))
                .headers(headers -> headers
                        .referrerPolicy(referrerPolicy -> referrerPolicy
                                .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE)
                        )
                );

        return http.build();
    }

    @Bean
    public AuthenticationEntryPoint customAuthenticationEntryPoint() {
        log.info("Custom AuthenticationEntryPoint triggered");
        return (request, response, authException) -> {
            Authentication authentication = getAuthenticationFromUserDetailService();

            if (authentication != null) {
                log.info("Authenticated as user: {}", authentication.getName());
                SecurityContextHolder.getContext().setAuthentication(authentication);
                // 重定向回原授权请求地址,保留所有参数
                String redirectUrl = request.getRequestURL().toString();
                if (request.getQueryString() != null) {
                    redirectUrl += "?" + request.getQueryString();
                }
                response.sendRedirect(redirectUrl);
            } else {
                // 认证失败时触发登录跳转
                new LoginUrlAuthenticationEntryPoint("/login").commence(request, response, authException);
            }
        };
    }

    private Authentication getAuthenticationFromUserDetailService() {
        log.info("Retrieving authentication from UserDetailService");
        try {
            UserDetails userDetails = userDetailsService.loadUserByUsername("user");
            log.info("Loaded user details: {}", userDetails);
            return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        } catch (Exception e) {
            log.error("Error retrieving user details: {}", e.getMessage());
            return null;
        }
    }

    @Bean
    public AuthorizationServerSettings providerSettings() {
        return AuthorizationServerSettings
                .builder()
                .issuer("http://localhost:8080")
                .build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("hts-client-id")
                .clientSecret("{noop}secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("http://127.0.0.1:8081")
                .scope(OidcScopes.OPENID)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
        RSAKey rsaKey = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private RSAKey generateRsa() throws NoSuchAlgorithmException {
        KeyPair keyPair = generateRsaKey();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();
    }

    private KeyPair generateRsaKey() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        return keyPairGenerator.generateKeyPair();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user")
                .password("{noop}1234")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/oauth2/**", "/h2-console/**").permitAll()
                        .anyRequest().authenticated())
                .formLogin(form -> form.disable())
                .anonymous(anon -> anon.disable())
                .oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

关键修改说明

  1. 重定向地址修正:在customAuthenticationEntryPoint()中获取原请求的完整URL(包含所有参数)作为重定向目标,确保认证完成后能正确续接原授权流程
  2. 认证失败处理优化:原代码中创建LoginUrlAuthenticationEntryPoint但未触发跳转,现在修正为调用commence方法执行登录跳转
  3. UserDetailsService修正:移除错误的withDefaultPasswordEncoder()用法,直接使用密码前缀指定编码器(符合Spring Security规范)

内容的提问来源于stack exchange,提问作者neo.k

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 06:55:05