Spring Authorization Server无重定向:如何传递用户凭证获取Token?
针对你的场景,最优方案是启用OAuth2资源所有者密码凭证授权(Resource Owner Password Credentials Grant),并自定义Spring Authorization Server的组件来支持该授权类型,直接通过REST请求传递用户凭证完成认证,无需跳转登录表单,同时保留授权服务器的会话存储能力。
步骤1:修改授权服务器配置
首先在application.yml中为客户端添加password授权类型:
security: oauth2: authorizationserver: client: myclientid: registration: client-id: clientid client-secret: "{noop}secret" client-name: clientid client-authentication-methods: - client_secret_basic authorization-grant-types: - authorization_code - client_credentials - refresh_token - password # 新增密码授权类型 redirect-uris: - "####" scopes: - articles.read server: port: 9000
步骤2:自定义密码授权的认证组件
Spring Authorization Server默认不支持密码授权,需要自定义以下核心组件:
1. 密码授权请求转换器
负责解析请求中的用户名、密码参数,转换为认证Token:
import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import jakarta.servlet.http.HttpServletRequest; import java.util.Map; public class PasswordGrantAuthenticationConverter implements AuthenticationConverter { private static final String GRANT_TYPE = "password"; private static final String USERNAME_PARAM = "username"; private static final String PASSWORD_PARAM = "password"; @Override public Authentication convert(HttpServletRequest request) { String grantType = request.getParameter("grant_type"); if (!GRANT_TYPE.equals(grantType)) { return null; } // 获取已认证的客户端信息 Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); } // 提取用户名和密码 MultiValueMap<String, String> params = OAuth2TokenGrantAuthenticationConverter.getParameters(request); String username = params.getFirst(USERNAME_PARAM); String password = params.getFirst(PASSWORD_PARAM); if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } Map<String, Object> additionalParams = Map.of( USERNAME_PARAM, username, PASSWORD_PARAM, password ); return new OAuth2PasswordGrantAuthenticationToken(clientPrincipal, additionalParams); } }
2. 自定义密码授权认证Token
用于传递用户凭证信息:
import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; import java.util.Map; public class OAuth2PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { public OAuth2PasswordGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) { super("password", clientPrincipal, additionalParameters); } public String getUsername() { return (String) getAdditionalParameters().get("username"); } public String getPassword() { return (String) getAdditionalParameters().get("password"); } }
3. 密码授权认证提供者
负责校验用户凭证、生成AccessToken并存储授权记录:
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; 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.token.OAuth2TokenGenerator; import org.springframework.util.Assert; import java.util.Collections; import java.util.Set; public class OAuth2PasswordGrantAuthenticationProvider extends OAuth2AuthorizationGrantAuthenticationProvider { private final AuthenticationManager authenticationManager; private final RegisteredClientRepository registeredClientRepository; public OAuth2PasswordGrantAuthenticationProvider(AuthenticationManager authenticationManager, RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<?> tokenGenerator) { super(authorizationService, tokenGenerator); Assert.notNull(authenticationManager, "authenticationManager cannot be null"); Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); this.authenticationManager = authenticationManager; this.registeredClientRepository = registeredClientRepository; } @Override public boolean supports(Class<?> authentication) { return OAuth2PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication); } @Override protected Authentication authenticateGrant(Authentication authentication) throws AuthenticationException { OAuth2PasswordGrantAuthenticationToken grantToken = (OAuth2PasswordGrantAuthenticationToken) authentication; String clientId = grantToken.getPrincipal().getName(); RegisteredClient registeredClient = registeredClientRepository.findByClientId(clientId); if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains("password")) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } // 认证用户凭证 String username = grantToken.getUsername(); String password = grantToken.getPassword(); Authentication userAuth = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password) ); // 处理Scope Set<String> scopes = grantToken.getAdditionalParameters().containsKey("scope") ? Set.of(((String) grantToken.getAdditionalParameters().get("scope")).split(" ")) : Collections.emptySet(); for (String scope : scopes) { if (!registeredClient.getScopes().contains(scope)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); } } // 生成并存储AccessToken OAuth2Authorization.Builder authBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(userAuth.getName()) .authorizationGrantType(new org.springframework.security.oauth2.core.AuthorizationGrantType("password")) .authorizedScopes(scopes); OAuth2AccessToken accessToken = (OAuth2AccessToken) getTokenGenerator().generate( authBuilder.build(), org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(userAuth) .authorizationGrantType(new org.springframework.security.oauth2.core.AuthorizationGrantType("password")) .authorizedScopes(scopes) .build() ); if (accessToken == null) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR); } authBuilder.token(accessToken, metadata -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, accessToken.getClaims())); getAuthorizationService().save(authBuilder.build()); return new org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken( registeredClient, grantToken.getPrincipal(), accessToken ); } }
步骤3:注册自定义组件到授权服务器
创建配置类注册上述组件:
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.configuration.AuthenticationConfiguration; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.web.authentication.AuthenticationConverter; @Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { @Bean public AuthenticationConverter passwordGrantAuthenticationConverter() { return new PasswordGrantAuthenticationConverter(); } @Bean public OAuth2PasswordGrantAuthenticationProvider passwordGrantAuthenticationProvider( AuthenticationManager authenticationManager, RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<?> tokenGenerator) { return new OAuth2PasswordGrantAuthenticationProvider( authenticationManager, registeredClientRepository, authorizationService, tokenGenerator ); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
步骤4:配置用户认证服务
添加用户信息来源(示例用内存用户,实际可替换为JDBC或自定义UserDetailsService):
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("user") .password("{noop}password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }
调用方式
你的Kong插件可以构造如下请求调用授权服务器的/oauth2/token接口:
- 请求方法:POST
- 请求头:携带客户端Basic Auth(
Authorization: Basic Y2xpZW50aWQ6c2VjcmV0,由clientid:secret Base64编码得到) - 请求参数:
grant_type=password username=客户端请求头解析出的用户名 password=客户端请求头解析出的密码 scope=articles.read(可选)
调用成功后,授权服务器会直接返回包含用户信息的AccessToken,无需重定向,同时Spring Security会自动管理用户会话(默认会话策略为IF_REQUIRED,满足你在授权服务器存储会话的需求)。
注意:务必确保所有请求通过HTTPS传输,避免用户凭证泄露。
内容的提问来源于stack exchange,提问作者Otis Ottington
相关产品推荐
相关产品推荐

