Spring Security:/oauth2/token端点整合认证与令牌发放及验证
一、核心依赖配置
添加必要的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; } }
七、测试步骤
获取令牌:发送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错误。
验证令牌:使用access_token发送GET请求到
/api/userinfo,请求头添加Authorization: Bearer {access_token},应返回用户的用户名与权限信息。
关键说明
- 自定义Provider实现了客户端凭证+LDAP用户的双重认证,解决了无密码即可获取令牌的问题;
- 通过
OAuth2TokenCustomizer将用户信息写入JWT,资源服务器可通过JwtAuthenticationConverter解析身份,解决令牌无法识别用户的问题; - 两个
SecurityFilterChain通过@Order优先级区分授权服务器与资源服务器的请求处理,实现同端口部署; - 所有代码使用Spring生态最新API,无废弃逻辑。
内容的提问来源于stack exchange,提问作者Czarek
相关产品推荐
相关产品推荐

