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

Spring Security:/oauth2/token端点整合认证与令牌发放及验证

完整解决方案(Spring Boot3.4.x + Spring Authorization Server3.4.x + Spring Security6.4.x)

一、核心依赖配置

添加必要的Maven依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-ldap</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    </dependency>
</dependencies>

二、LDAP认证配置

配置LDAP连接与用户验证逻辑:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.search.LdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;

@Configuration
public class LdapConfig {

    @Bean
    public AuthenticationProvider ldapAuthenticationProvider() {
        // 替换为你的LDAP服务器地址与管理员账号
        DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://your-ldap-server:389/dc=example,dc=com");
        contextSource.setUserDn("cn=admin,dc=example,dc=com");
        contextSource.setPassword("admin-password");
        contextSource.afterPropertiesSet();

        // 用户搜索规则:根据uid匹配用户名
        LdapUserSearch userSearch = new FilterBasedLdapUserSearch(
                "ou=users",
                "(uid={0})",
                contextSource
        );

        // 绑定认证器:用用户自身DN与密码验证身份
        BindAuthenticator authenticator = new BindAuthenticator(contextSource);
        authenticator.setUserSearch(userSearch);

        // 权限填充器:从LDAP组中读取用户权限
        DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(
                contextSource,
                "ou=groups"
        );
        authoritiesPopulator.setGroupRoleAttribute("cn");
        authoritiesPopulator.setConvertToUpperCase(true);

        return new LdapAuthenticationProvider(authenticator, authoritiesPopulator);
    }
}

三、自定义ClientCredentials认证Provider

重写客户端凭证认证逻辑,加入LDAP用户验证,并注入用户身份信息:

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.StringUtils;

import java.util.Map;

public class CustomClientCredentialsAuthenticationProvider extends OAuth2ClientCredentialsAuthenticationProvider {

    private final AuthenticationManager authenticationManager;

    public CustomClientCredentialsAuthenticationProvider(RegisteredClientRepository registeredClientRepository, AuthenticationManager authenticationManager) {
        super(registeredClientRepository);
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2ClientCredentialsAuthenticationToken clientToken = (OAuth2ClientCredentialsAuthenticationToken) authentication;

        // 1. 先完成客户端身份认证
        Authentication clientAuth = super.authenticate(authentication);

        // 2. 提取请求中的用户名与密码参数
        Map<String, Object> additionalParams = clientToken.getAdditionalParameters();
        String username = (String) additionalParams.get("username");
        String password = (String) additionalParams.get("password");

        // 3. 参数校验
        if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
            throw new OAuth2AuthenticationException(
                    new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT, "Username and password are required", null)
            );
        }

        // 4. LDAP用户身份验证
        try {
            Authentication userAuth = authenticationManager.authenticate(
                    new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(username, password)
            );
            // 将用户认证信息存入详情,供令牌自定义使用
            clientToken.setDetails(userAuth);
        } catch (AuthenticationException e) {
            throw new OAuth2AuthenticationException(
                    new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT, "Invalid username or password", null)
            );
        }

        return clientAuth;
    }
}

四、授权服务器配置

注册自定义Provider,配置客户端与令牌生成规则:

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.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
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.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.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;

import java.util.UUID;

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .authenticationProvider(customClientCredentialsAuthenticationProvider(registeredClientRepository(), authenticationManager(http)));

        // 允许表单提交方式调用/oauth2/token端点
        http.formLogin();

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        // 注册测试客户端,生产环境需替换为真实配置
        RegisteredClient testClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("test-client")
                .clientSecret("{noop}test-secret") // 生产环境请使用BCrypt加密密码
                .authorizationGrantType(org.springframework.security.oauth2.core.AuthorizationGrantType.CLIENT_CREDENTIALS)
                .scope("api.read")
                .scope("api.write")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                .build();

        return new InMemoryRegisteredClientRepository(testClient);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
        return context -> {
            // 为ClientCredentials令牌注入用户信息
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                Authentication userAuth = (Authentication) context.getAuthentication().getDetails();
                if (userAuth != null) {
                    context.getClaims()
                            .claim("username", userAuth.getName())
                            .claim("authorities", userAuth.getAuthorities().stream()
                                    .map(grantedAuthority -> grantedAuthority.getAuthority())
                                    .toList());
                }
            }
        };
    }

    private CustomClientCredentialsAuthenticationProvider customClientCredentialsAuthenticationProvider(
            RegisteredClientRepository registeredClientRepository, AuthenticationManager authenticationManager) {
        return new CustomClientCredentialsAuthenticationProvider(registeredClientRepository, authenticationManager);
    }

    private AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManager.class);
    }
}

五、资源服务器与应用同端口配置

配置资源保护与JWT解析规则:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/oauth2/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(jwtAuthenticationConverter())
                        )
                );

        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("authorities");
        authoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        converter.setPrincipalClaimName("username"); // 从JWT的username字段解析用户名
        return converter;
    }
}

六、测试端点

创建验证令牌身份的测试接口:

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class TestController {

    @GetMapping("/api/userinfo")
    public Map<String, Object> getUserInfo(Authentication authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("username", authentication.getName());
        info.put("authorities", authentication.getAuthorities());
        info.put("authenticated", authentication.isAuthenticated());
        return info;
    }
}

七、测试步骤

  1. 获取令牌:发送POST请求到/oauth2/token,表单参数如下:

    client_id: test-client
    client_secret: test-secret
    grant_type: client_credentials
    scope: api.read
    username: test-user
    password: test-password
    
    • 用户名密码正确时返回access_token;
    • 密码错误时返回invalid_grant错误。
  2. 验证令牌:使用access_token发送GET请求到/api/userinfo,请求头添加Authorization: Bearer {access_token},应返回用户的用户名与权限信息。

关键说明

  • 自定义Provider实现了客户端凭证+LDAP用户的双重认证,解决了无密码即可获取令牌的问题;
  • 通过OAuth2TokenCustomizer将用户信息写入JWT,资源服务器可通过JwtAuthenticationConverter解析身份,解决令牌无法识别用户的问题;
  • 两个SecurityFilterChain通过@Order优先级区分授权服务器与资源服务器的请求处理,实现同端口部署;
  • 所有代码使用Spring生态最新API,无废弃逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:13:15