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

Spring Authorization Server无重定向:如何传递用户凭证获取Token?

解决方案:基于Spring Authorization Server实现无表单的用户密码认证

针对你的场景,最优方案是启用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 11:02:05