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

如何在新版OAuth2.0授权服务器中无表单登录配置SecurityConfig

Spring Authorization Server 问题解决方案

问题1:调用/oauth2/authorize时抛出AccessDeniedException

原因是授权端点要求用户认证,但你的asSecurityFilterChain(优先级1)仅应用了默认安全配置,未配置任何用户认证入口(如表单登录、HTTP Basic),同时appSecurityFilterChain禁用了formLogin和httpBasic,导致匿名用户访问授权端点被直接拒绝。

解决步骤:

  1. 在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();
}
  1. 确认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泄露风险):

  1. 前端生成随机code_verifier,并通过SHA-256加密生成code_challenge。
  2. 前端构造授权请求:
    GET /oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://localhost:3000&code_challenge=xxx&code_challenge_method=S256
    
  3. 授权服务器跳转至登录页(可配置为前端登录页面),用户登录并授权后,重定向回前端redirect_uri并携带code。
  4. 前端携带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(支持非用户名密码认证)

以手机号验证码认证为例:

  1. 自定义认证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;
    }
}
  1. 实现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);
    }
}
  1. 编写认证过滤器:
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);
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 12:52:52