小工具知识网

当前位置: 首页 > 行业知识 > Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战

行业知识

Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战

2026-11-03 06:52:48 来源:互联网转载

开局一张图

项���源码:youlai-mall

通过 Spring Cloud Gateway 访问认证中心进行认证并获取得到访问令牌。

再根据访问令牌 access_token 获取当前登录的用户信息。

前言

Spring Security OAuth2 的最终版本是2.5.2,并于2022年6月5日正式宣布停止维护。Spring 官方为此推出了新的替代产品,即 Spring Authorization Server。然而,出于安全考虑,Spring Authorization Server 不再支持密码模式,因为密码模式要求客户端直接处理用户的密码。但对于受信任的第一方系统(自有APP和管理系统等),许多情况下需要使用密码模式。在这种情况下,需要在 Spring Authorization Server 的基础上扩展密码模式的支持。本文基于开源微服务商城项目 youlai-mall、Spring Boot 3 和 Spring Authorization Server 1.1 版本,演示了如何扩展密码模式,以及如何将其应用于 Spring Cloud 微服务实战。

数据库初始化

Spring Authorization Server 官方提供的授权服务器示例 demo-authorizationserver 初始化数据库所使用的3个SQL脚本路径如下:

根据路径找到3张表的SQL脚本

  • 令牌发放记录表: oauth2-authorization-schema.sql
  • 授权记录表: oauth2-authorization-consent-schema.sql
  • 客户端信息表: oauth2-registered-client-schema.sql

    整合后的完整数据库 SQL 脚本如下:

    -- ------------------------------ 1. 创建数据库-- ----------------------------CREATE DATABASE IF NOT EXISTS oauth2_server DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;-- ------------------------------ 2. 创建表-- ----------------------------use oauth2_server;SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ 2.1 oauth2_authorization 令牌发放记录表-- ----------------------------CREATE TABLE oauth2_authorization (    id varchar(100) NOT NULL,    registered_client_id varchar(100) NOT NULL,    principal_name varchar(200) NOT NULL,    authorization_grant_type varchar(100) NOT NULL,    authorized_scopes varchar(1000) DEFAULT NULL,    attributes blob DEFAULT NULL,    state varchar(500) DEFAULT NULL,    authorization_code_value blob DEFAULT NULL,    authorization_code_issued_at timestamp DEFAULT NULL,    authorization_code_expires_at timestamp DEFAULT NULL,    authorization_code_metadata blob DEFAULT NULL,    access_token_value blob DEFAULT NULL,    access_token_issued_at timestamp DEFAULT NULL,    access_token_expires_at timestamp DEFAULT NULL,    access_token_metadata blob DEFAULT NULL,    access_token_type varchar(100) DEFAULT NULL,    access_token_scopes varchar(1000) DEFAULT NULL,    oidc_id_token_value blob DEFAULT NULL,    oidc_id_token_issued_at timestamp DEFAULT NULL,    oidc_id_token_expires_at timestamp DEFAULT NULL,    oidc_id_token_metadata blob DEFAULT NULL,    refresh_token_value blob DEFAULT NULL,    refresh_token_issued_at timestamp DEFAULT NULL,    refresh_token_expires_at timestamp DEFAULT NULL,    refresh_token_metadata blob DEFAULT NULL,    user_code_value blob DEFAULT NULL,    user_code_issued_at timestamp DEFAULT NULL,    user_code_expires_at timestamp DEFAULT NULL,    user_code_metadata blob DEFAULT NULL,    device_code_value blob DEFAULT NULL,    device_code_issued_at timestamp DEFAULT NULL,    device_code_expires_at timestamp DEFAULT NULL,    device_code_metadata blob DEFAULT NULL,    PRIMARY KEY (id));-- ------------------------------ 2.2 oauth2_authorization_consent 授权记录表-- ----------------------------CREATE TABLE oauth2_authorization_consent (    registered_client_id varchar(100) NOT NULL,    principal_name varchar(200) NOT NULL,    authorities varchar(1000) NOT NULL,    PRIMARY KEY (registered_client_id, principal_name));-- ------------------------------ 2.3 oauth2-registered-client OAuth2 客户端信息表-- ----------------------------CREATE TABLE oauth2_registered_client (    id varchar(100) NOT NULL,    client_id varchar(100) NOT NULL,    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,    client_secret varchar(200) DEFAULT NULL,    client_secret_expires_at timestamp DEFAULT NULL,    client_name varchar(200) NOT NULL,    client_authentication_methods varchar(1000) NOT NULL,    authorization_grant_types varchar(1000) NOT NULL,    redirect_uris varchar(1000) DEFAULT NULL,    post_logout_redirect_uris varchar(1000) DEFAULT NULL,    scopes varchar(1000) NOT NULL,    client_settings varchar(2000) NOT NULL,    token_settings varchar(2000) NOT NULL,    PRIMARY KEY (id));

    授权服务器

    youlai-auth 模块作为认证授权服务器

    maven 依赖

    在 youlai-auth 模块的 pom.xml 添加授权服务器依赖

        org.springframework.security    spring-security-oauth2-authorization-server    1.1.1

    application.yml

    认证中心配置 oauth2_server 数据库连接信息

    spring:  datasource:    type: com.alibaba.druid.pool.DruidDataSource    driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://localhost:3306/oauth2_server?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true    username: root    password: 123456

    授权服务器配置

    参考 Spring Authorization Server 官方示例 demo-authorizationserver

    AuthorizationServierConfig

    参考: Spring Authorization Server 官方示例 demo-authorizationserver 下的 AuthorizationServerConfig.java 进行授权服务器配置

    package com.youlai.auth.config;/** * 授权服务器配置 * * @author haoxr * @since 3.0.0 */@Configuration@RequiredArgsConstructor@Slf4jpublic class AuthorizationServerConfig {    private final OAuth2TokenCustomizer jwtCustomizer;    /**     * 授权服务器端点配置     */    @Bean    @Order(Ordered.HIGHEST_PRECEDENCE)    public SecurityFilterChain authorizationServerSecurityFilterChain(            HttpSecurity http,            AuthenticationManager authenticationManager,            OAuth2AuthorizationService authorizationService,            OAuth2TokenGenerator tokenGenerator    ) throws Exception {        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();        authorizationServerConfigurer                .tokenEndpoint(tokenEndpoint ->                        tokenEndpoint                                .accessTokenRequestConverters(                                        authenticationConverters ->//  authenticationConverters.addAll(         // 自定义授权模式转换器(Converter)         List.of(                 new PasswordAuthenticationConverter()         ) )                                )                               .authenticationProviders(authenticationProviders ->//                                         authenticationProviders.addAll(                                            // 自定义授权模式提供者(Provider) List.of(         new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator) )                                        )                                )                                .accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应                                .errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应                );        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();        http.securityMatcher(endpointsMatcher)                .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))                .apply(authorizationServerConfigurer);        return http.build();    }    @Bean //     public JWKSource jwkSource() {        KeyPair keyPair = generateRsaKey();        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();        // @formatter:off        RSAKey rsaKey = new RSAKey.Builder(publicKey)                .privateKey(privateKey)                .keyID(UUID.randomUUID().toString())                .build();        // @formatter:on        JWKSet jwkSet = new JWKSet(rsaKey);        return new ImmutableJWKSet(jwkSet);    }    private static KeyPair generateRsaKey() { //         KeyPair keyPair;        try {            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");            keyPairGenerator.initialize(2048);            keyPair = keyPairGenerator.generateKeyPair();        } catch (Exception ex) {            throw new IllegalStateException(ex);        }        return keyPair;    }    @Bean    public JwtDecoder jwtDecoder(JWKSource jwkSource) {        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);    }    @Bean    public AuthorizationServerSettings authorizationServerSettings() {        return AuthorizationServerSettings.builder().build();    }    @Bean    public PasswordEncoder passwordEncoder() {        return PasswordEncoderFactories.createDelegatingPasswordEncoder();    }    @Bean    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);        // 初始化 OAuth2 客户端        initMallAppClient(registeredClientRepository);        initMallAdminClient(registeredClientRepository);        return registeredClientRepository;    }    @Bean    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,            RegisteredClientRepository registeredClientRepository) {        JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);        JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);        rowMapper.setLobHandler(new DefaultLobHandler());        ObjectMapper objectMapper = new ObjectMapper();        ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();        List securityModules = SecurityJackson2Modules.getModules(classLoader);        objectMapper.registerModules(securityModules);        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());        // 使用刷新模式,需要从 oauth2_authorization 表反序列化attributes字段得到用户信息(SysUserDetails)        objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class);        objectMapper.addMixIn(Long.class, Object.class);                rowMapper.setObjectMapper(objectMapper);        service.setAuthorizationRowMapper(rowMapper);        return service;    }    @Bean    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,                          RegisteredClientRepository registeredClientRepository) {        // Will be used by the ConsentController        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);    }    @Bean    OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) {        JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));        jwtGenerator.setJwtCustomizer(jwtCustomizer);        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();        return new DelegatingOAuth2TokenGenerator(                jwtGenerator, accessTokenGenerator, refreshTokenGenerator);    }    @Bean    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {        return authenticationConfiguration.getAuthenticationManager();    }    /**     * 初始化创建商城管理客户端     *     * @param registeredClientRepository     */    private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) {        String clientId = "mall-admin";        String clientSecret = "123456";        String clientName = "商城管理客户端";        /*          如果使用明文,客户端认证时会自动升级加密方式,换句话说直接修改客户端密码,所以直接使用 bcrypt 加密避免不必要的麻烦          官方ISSUE: https://github.com/spring-projects/spring-authorization-server/issues/1099         */        String encodeSecret = passwordEncoder().encode(clientSecret);        RegisteredClient registeredMallAdminClient = registeredClientRepository.findByClientId(clientId);        String id = registeredMallAdminClient != null ? registeredMallAdminClient.getId() : UUID.randomUUID().toString();        RegisteredClient mallAppClient = RegisteredClient.withId(id)                .clientId(clientId)                .clientSecret(encodeSecret)                .clientName(clientName)                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)                .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 密码模式                .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 验证码模式                .redirectUri("http://127.0.0.1:8080/authorized")                .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")                .scope(OidcScopes.OPENID)                .scope(OidcScopes.PROFILE)                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build())                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())                .build();        registeredClientRepository.save(mallAppClient);    }    /**     * 初始化创建商城APP客户端     *     * @param registeredClientRepository     */    private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) {        String clientId = "mall-app";        String clientSecret = "123456";        String clientName = "商城APP客户端";        // 如果使用明文,在客户端认证的时候会自动升级加密方式,直接使用 bcrypt 加密避免不必要的麻烦        String encodeSecret = passwordEncoder().encode(clientSecret);        RegisteredClient registeredMallAppClient = registeredClientRepository.findByClientId(clientId);        String id = registeredMallAppClient != null ? registeredMallAppClient.getId() : UUID.randomUUID().toString();        RegisteredClient mallAppClient = RegisteredClient.withId(id)                .clientId(clientId)                .clientSecret(encodeSecret)                .clientName(clientName)                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)                .authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP) // 微信小程序模式                .authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE) // 短信验证码模式                .redirectUri("http://127.0.0.1:8080/authorized")                .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")                .scope(OidcScopes.OPENID)                .scope(OidcScopes.PROFILE)                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build())                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())                .build();        registeredClientRepository.save(mallAppClient);    }}

    DefaultSecutiryConfig

    • 参考 Spring Authorization Server 官方示例 demo-authorizationserver 下的 DefaultSecurityConfig.java 进行安全配置
      package com.youlai.auth.config;/** * 授权服务器安全配置 * * @author haoxr * @since 3.0.0 */@EnableWebSecurity@Configuration(proxyBeanMethods = false)public class DefaultSecurityConfig {        /**     * Spring Security 安全过滤器链配置     */    @Bean    @Order(0)    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {        http                .authorizeHttpRequests(requestMatcherRegistry ->                        {                            requestMatcherRegistry.anyRequest().authenticated();                        }                )                .csrf(AbstractHttpConfigurer::disable)                .formLogin(Customizer.withDefaults());        return http.build();    }    /**     * Spring Security 自定义安全配置     */    @Bean    public WebSecurityCustomizer webSecurityCustomizer() {        return (web) ->                // 不走过滤器链(场景:静态资源js、css、html)                web.ignoring().requestMatchers(                        "/webjars/**",                        "/doc.html",                        "/swagger-resources/**",                        "/v3/api-docs/**",                        "/swagger-ui/**"                );    }}

      密码模式扩展

      PasswordAuthenticationToken

      package com.youlai.auth.authentication.password;/** * 密码授权模式身份验证令牌(包含用户名和密码等) * * @author haoxr * @since 3.0.0 */public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {    public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");    /**     * 令牌申请访问范围     */    private final Set scopes;    /**     * 密码模式身份验证令牌     *     * @param clientPrincipal      客户端信息     * @param scopes               令牌申请访问范围     * @param additionalParameters 自定义额外参数(用户名和密码)     */    public PasswordAuthenticationToken(            Authentication clientPrincipal,            Set scopes,            @Nullable Map additionalParameters    ) {        super(PASSWORD, clientPrincipal, additionalParameters);        this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet(scopes) : Collections.emptySet());    }    /**     * 用户凭证(密码)     */    @Override    public Object getCredentials() {        return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);    }    public Set getScopes() {        return scopes;    }}

      PasswordAuthenticationConverter

      package com.youlai.auth.authentication.password;/** * 密码模式参数解析器 * 

      * 解析请求参数中的用户名和密码,并构建相应的身份验证(Authentication)对象 * * @author haoxr * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter * @since 3.0.0 */public class PasswordAuthenticationConverter implements AuthenticationConverter { @Override public Authentication convert(HttpServletRequest request) { // 授权类型 (必需) String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { return null; } // 客户端信息 Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); // 参数提取验证 MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); // 令牌申请访问范围验证 (可选) String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); } Set requestedScopes = null; if (StringUtils.hasText(scope)) { requestedScopes = new HashSet(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); } // 用户名验证(必需) String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); if (StrUtil.isBlank(username)) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI ); } // 密码验证(必需) String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); if (StrUtil.isBlank(password)) { OAuth2EndpointUtils.throwError( OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI ); } // 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用于身份认证) Map additionalParameters = parameters .entrySet() .stream() .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) && !e.getKey().equals(OAuth2ParameterNames.SCOPE) ).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); return new PasswordAuthenticationToken( clientPrincipal, requestedScopes, additionalParameters ); }}

      PasswordAuthenticationProvider

      package com.youlai.auth.authentication.password;/** * 密码模式身份验证提供者 * 

      * 处理基于用户名和密码的身份验证 * * @author haoxr * @since 3.0.0 */@Slf4jpublic class PasswordAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; private final AuthenticationManager authenticationManager; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator

上一篇:Mac系统下Jmeter的下载、安装、及环境变量配置

下一篇:服务器电源6针怎么接?