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(); } }
关键修改说明
- 重定向地址修正:在
customAuthenticationEntryPoint()中获取原请求的完整URL(包含所有参数)作为重定向目标,确保认证完成后能正确续接原授权流程 - 认证失败处理优化:原代码中创建
LoginUrlAuthenticationEntryPoint但未触发跳转,现在修正为调用commence方法执行登录跳转 - UserDetailsService修正:移除错误的
withDefaultPasswordEncoder()用法,直接使用密码前缀指定编码器(符合Spring Security规范)
内容的提问来源于stack exchange,提问作者neo.k
相关产品推荐
相关产品推荐

