如何在新版OAuth2.0授权服务器中无表单登录配置SecurityConfig
问题1:调用/oauth2/authorize时抛出AccessDeniedException
原因是授权端点要求用户认证,但你的asSecurityFilterChain(优先级1)仅应用了默认安全配置,未配置任何用户认证入口(如表单登录、HTTP Basic),同时appSecurityFilterChain禁用了formLogin和httpBasic,导致匿名用户访问授权端点被直接拒绝。
解决步骤:
- 在
asSecurityFilterChain中补充认证机制,以表单登录为例(也可替换为自定义认证逻辑):
@Bean @Order(1) public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .authorizationEndpoint(a -> a.authenticationProviders(getAuthorizationEndPoints())) .oidc(Customizer.withDefaults()); // 添加表单登录入口,允许匿名访问登录页 http.formLogin(form -> form.loginPage("/login").permitAll()) .exceptionHandling(exception -> exception .authenticationEntryPoint(authenticationEntryPoint)); return http.build(); }
- 确认
RegisteredClient配置正确,需包含authorization_code授权类型,且redirect_uri已完成注册。
问题2:formLogin与UI集成方案
服务端渲染场景
- 自定义登录页面:配置
formLogin().loginPage("/custom-login"),编写Controller返回自定义登录页面,表单提交至/login接口完成认证。 - 认证成功后,授权服务器自动重定向至客户端注册的
redirect_uri并携带code,客户端拿到code后调用/oauth2/token接口(携带client_id、client_secret、code、redirect_uri)获取令牌。
前端SPA(单页应用)场景
推荐使用授权码流程+PKCE(无client_secret泄露风险):
- 前端生成随机
code_verifier,并通过SHA-256加密生成code_challenge。 - 前端构造授权请求:
GET /oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://localhost:3000&code_challenge=xxx&code_challenge_method=S256 - 授权服务器跳转至登录页(可配置为前端登录页面),用户登录并授权后,重定向回前端
redirect_uri并携带code。 - 前端携带
code、code_verifier、client_id、redirect_uri调用/oauth2/token接口获取令牌。
问题3:自定义认证逻辑实现
方式1:扩展用户名密码认证(自定义UserDetailsService)
适用于从自定义数据源(如数据库)查询用户信息的场景:
@Service public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 自定义逻辑:从数据库/其他数据源查询用户 return User.withUsername(username) .password("{bcrypt}$2a$10$xxx") // 存储的加密密码 .authorities("ROLE_USER") .build(); } }
注入AuthenticationManager供认证使用:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }
方式2:自定义AuthenticationProvider(支持非用户名密码认证)
以手机号验证码认证为例:
- 自定义认证Token:
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; // 未认证状态构造方法 public SmsAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } // 已认证状态构造方法 public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } }
- 实现
AuthenticationProvider:
public class SmsAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsAuthenticationToken authToken = (SmsAuthenticationToken) authentication; String phone = authToken.getPrincipal().toString(); String code = authToken.getCredentials().toString(); // 自定义逻辑:验证手机号与验证码有效性 if (!"123456".equals(code)) { throw new BadCredentialsException("验证码错误"); } // 返回已认证的Token return new SmsAuthenticationToken( User.withUsername(phone).authorities("ROLE_USER").build(), null, Collections.emptyList() ); } @Override public boolean supports(Class<?> authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } }
- 编写认证过滤器:
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public SmsAuthenticationFilter() { super(new AntPathRequestMatcher("/sms/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String phone = request.getParameter("phone"); String code = request.getParameter("code"); SmsAuthenticationToken authToken = new SmsAuthenticationToken(phone, code); return getAuthenticationManager().authenticate(authToken); } }
- 在Security配置中注册:
@Bean @Order(2) public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception { // ... 原有CORS、授权规则等配置 http.addFilterBefore(new SmsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.authenticationProvider(new SmsAuthenticationProvider()); return http.build(); }
方式3:自定义未认证返回逻辑(REST场景)
如果是前后端分离,需要在未认证时返回JSON而非重定向,修改JwtAuthenticationEntryPoint:
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{\"code\":401,\"message\":\"未认证,请先完成登录\"}"); } }
内容的提问来源于stack exchange,提问作者Gaurav
相关产品推荐
相关产品推荐

