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

Spring Boot OAuth2授权服务器:在CustomUserDetailsService中获取client_id

在OAuth2授权服务器的CustomUserDetailsService中获取client_id并验证权限

这个需求在统一认证体系里非常常见,要实现用户登录时同时校验账号密码和对应客户端的权限,下面给你两种可行的方案,第二种更贴合Spring Security的设计原则,优先推荐:

方案一:通过请求上下文直接提取client_id

这种方式适合快速实现,核心是利用RequestContextHolder获取当前请求,从请求参数或请求头中解析出client_id:

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Base64;

public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    // 构造注入依赖
    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            // 获取当前请求上下文
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) {
                throw new IllegalStateException("无法获取当前请求上下文");
            }
            HttpServletRequest request = attributes.getRequest();
            
            // 1. 先尝试从请求参数中获取client_id(覆盖密码模式、授权码模式的授权请求)
            String clientId = request.getParameter("client_id");
            
            // 2. 如果参数里没有,尝试从Basic认证头中解析(覆盖客户端凭证模式、部分密码模式场景)
            if (clientId == null) {
                String authHeader = request.getHeader("Authorization");
                if (authHeader != null && authHeader.startsWith("Basic ")) {
                    String base64Credentials = authHeader.substring("Basic ".length()).trim();
                    String credentials = new String(Base64.getDecoder().decode(base64Credentials));
                    clientId = credentials.split(":", 2)[0];
                }
            }

            if (clientId == null) {
                throw new IllegalStateException("无法获取客户端ID");
            }

            // 加载用户信息
            User domainUser = userRepository.findByUsername(username);
            if (domainUser == null) {
                throw new UsernameNotFoundException("用户不存在:" + username);
            }

            // 验证用户是否有权限访问当前客户端
            if (!hasClientAccessPermission(domainUser, clientId)) {
                throw new UsernameNotFoundException("用户无权限访问客户端:" + clientId);
            }

            // 构建UserDetails返回
            return User.withUsername(domainUser.getUsername())
                    .password(domainUser.getPassword())
                    .authorities(domainUser.getAuthorities())
                    .build();

        } catch (Exception e) {
            throw new UsernameNotFoundException("用户认证失败:" + username, e);
        }
    }

    // 自定义权限校验逻辑,根据你的业务实现
    private boolean hasClientAccessPermission(User domainUser, String clientId) {
        // 示例:检查用户关联的客户端列表是否包含当前clientId
        return domainUser.getAllowedClientIds().contains(clientId);
    }
}

注意点:

  • 不同OAuth2授权模式下,client_id的传递方式不同,代码里覆盖了参数传递和Basic认证头传递两种常见场景
  • 这种方式直接在UserDetailsService里处理权限校验,优点是快速,但稍微违背了单一职责原则(UserDetailsService的核心职责是加载用户信息)

方案二:自定义AuthenticationProvider(推荐)

这种方式更符合Spring Security的设计理念,把客户端权限校验放到专门的认证提供者中,分离职责:

1. 自定义OAuth2认证提供者

import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;

public class CustomOAuth2AuthenticationProvider extends DaoAuthenticationProvider {

    private final UserRepository userRepository;

    public CustomOAuth2AuthenticationProvider(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 仅处理OAuth2类型的认证请求
        if (authentication instanceof OAuth2Authentication) {
            OAuth2Authentication oAuth2Auth = (OAuth2Authentication) authentication;
            OAuth2Request oAuth2Request = oAuth2Auth.getOAuth2Request();
            String clientId = oAuth2Request.getClientId();

            // 先调用父类方法完成用户名密码校验
            Authentication authResult = super.authenticate(authentication);
            UserDetails userDetails = (UserDetails) authResult.getPrincipal();

            // 校验用户对当前客户端的访问权限
            if (!hasClientAccessPermission(userDetails.getUsername(), clientId)) {
                throw new UsernameNotFoundException("用户无权限访问客户端:" + clientId);
            }

            return authResult;
        }
        return super.authenticate(authentication);
    }

    private boolean hasClientAccessPermission(String username, String clientId) {
        // 示例:查询数据库验证用户与客户端的权限关联
        return userRepository.existsByUsernameAndAllowedClientId(username, clientId);
    }
}

2. 替换授权服务器的默认认证提供者

在WebSecurity配置类中注册自定义的AuthenticationProvider:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService customUserDetailsService;
    private final UserRepository userRepository;

    public WebSecurityConfig(UserDetailsService customUserDetailsService, UserRepository userRepository) {
        this.customUserDetailsService = customUserDetailsService;
        this.userRepository = userRepository;
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        CustomOAuth2AuthenticationProvider provider = new CustomOAuth2AuthenticationProvider(userRepository);
        provider.setUserDetailsService(customUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        auth.authenticationProvider(provider);
    }

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

优势:

  • 遵循单一职责原则:UserDetailsService仅负责加载用户信息,权限校验交给AuthenticationProvider处理
  • 直接从OAuth2Request中获取client_id,比解析请求参数更可靠,覆盖所有OAuth2授权模式
  • 代码结构更清晰,便于后续扩展其他认证逻辑

通用注意事项

  1. 确保授权服务器已正确注册所有客户端的client_id和clientSecret
  2. 权限校验逻辑要结合你的业务,比如可以在数据库中建立用户-客户端关联表,存储用户可访问的客户端列表
  3. 异常处理要符合OAuth2规范,抛出的UsernameNotFoundException会被Spring Security自动转换为标准的认证错误响应

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:20:31