Bootstrap

Spring Authorization Server 1.4.0 使用及详细配置 搭配Spring Boot3.4.0 + Spring Security6.4.1

Spring Authorization Server 介绍

Spring Authorization Server 是一个提供OAuth 2.1和OpenID Connect 1.0规范以及其他相关规范的实现的框架。它构建在Spring Security之上,为构建 OpenID Connect 1.0 身份提供商和 OAuth2 授权服务器产品提供安全、轻量级和可定制的基础。

框架提供五种授权模式

  • Authorization Code(授权码模式)
  • Client Credentials(客户端模式)
  • Refresh Token(令牌刷新)
  • Device Code(设备码模式)
  • Token Exchange(token交换)

本篇文章主要讲解常用的授权码模式、客户端模式、令牌刷新这三种模式,以及如何自己扩展授权模式(以账号密码模式为例)

框架提供两种token格式

  1. Self-contained (JWT) (信息透明)
  2. Reference (Opaque) (信息不透明)

本篇文章会讲解两种token的原理,如何使用不同格式的token,如何自定义token内容的扩展,以及如何自己自定义token生成器(以用短字符串自定义不透明token为例)

入门

创建一个新项目 添加依赖

使用目前最新版Spring Boot 3.4.0 要求JDK版本大于等于17

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0</version>
    </parent>

    <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-oauth2-authorization-server</artifactId>
        </dependency>
    </dependencies>

创建配置类,添加基本配置

package chick.authorization.security;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
                OAuth2AuthorizationServerConfigurer.authorizationServer();

        http
                .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
                .with(authorizationServerConfigurer, Customizer.withDefaults())
                .authorizeHttpRequests((authorize) ->
                        authorize.anyRequest().authenticated()
                )
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                );
        http
                .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());
        return http.build();
    }

    // 用于检索用户进行身份验证
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withUsername("admin")
                .password(passwordEncoder().encode("123123"))
                .roles("admin")
                .build();

        return new InMemoryUserDetailsManager(userDetails);
    }

    // 用于密码加密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //	用于管理客户端
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        TokenSettings tokenSettings = TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofHours(1)) // 设置访问令牌有效期为1小时
                .refreshTokenTimeToLive(Duration.ofDays(30)) // 设置刷新令牌有效期为30天
                //.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 这个设置是开启不透明token
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 使用透明token
                .build();
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("chick")
                .clientSecret(passwordEncoder().encode("123456"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("https://www.baidu.com")
                .postLogoutRedirectUri("http://127.0.0.1:8000/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .tokenSettings(tokenSettings)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();
        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    // 用于签署访问令牌
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    // 启动时生成的密钥,用于创建上面的JWKSource
    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<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    // 用于配置 Spring Authorization Server 
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

}

目前已解锁

  • Authorization Code(授权码模式)
  • Client Credentials(客户端模式)
  • Refresh Token(令牌刷新)

测试授权码模式

浏览器访问:http://127.0.0.1:8000/oauth2/authorize?response_type=code&client_id=chick&scope=openid&redirect_uri=https://www.baidu.com

会重定向到登录页

输入上面UserDetailsService中设置的用户名密码 登录会重定向到百度并 获取到code

使用code获取token等信息

POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&redirect_uri=https://www.baidu.com&code=9ZAclrji.....

测试令牌刷新模式

使用上一步返回的refresh_token

POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&code=9ZAclrji.....

测试客户端授权模式

有多种方式,这里介绍常用的client_secret_basic和client_secret_post

POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

POST /oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=chick&client_secret=123456&scope=openid

参数都是对应创建客户端时的设置的

总结

通过项目引入的依赖以及一些简单的配置,即可完成授权服务的基础搭建,但是目前所完成的部分仅适用于自己进行功能的基本测试,无法应用到真正的项目中,真正做到定制化还远远不够,还存在例如目前客户端、用户、登录信息,密钥等都还保存在内存中的问题,应用重启后密钥会刷新导致之前的token无法验签,重启后登录信息会消失导致用户需要重新登录、客户端和用户是写死的,无法动态的加载等,如果我们想在token中加我我们自己自定义的数据该怎么做?不透明token和refresh太长了,如果我们想将他变短些,例如使用uuid来替代他该怎么做,这就需要一些进阶的玩法。

进阶

客户端持久化

首先翻开源码找到客户端需要的数据库表,在数据库中创建该表

添加数据库相关依赖

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

配置数据库

server:
  port: 8000

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: xxxxxx
    url: jdbc:mysql://ip:port/数据库名?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true&autoReconnect=true

修改配置-RegisteredClientRepository

    // 注入JdbcTemplate
    private final JdbcTemplate jdbcTemplate;

    public SecurityConfig(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

将之前的客户端插入到数据库中

INSERT INTO oauth2_registered_client (id, client_id, client_id_issued_at, client_secret, client_secret_expires_at, client_name, client_authentication_methods, authorization_grant_types, redirect_uris, post_logout_redirect_uris, scopes, client_settings, token_settings) VALUES ('79e31552-9ffb-42f3-8aa6-ff83d0860215', 'chick', '2024-12-04 14:23:41', '$2a$10$ftuHVREYeyjJvzzbkS67.uxo.JfU2SJ7OAeZMC7swZPsFfc69/tJm', null, '79e31552-9ffb-42f3-8aa6-ff83d0860215', 'client_secret_post,client_secret_jwt,client_secret_basic', 'refresh_token,client_credentials,authorization_code', 'https://www.baidu.com', 'http://127.0.0.1:8000/', 'openid,profile', '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":true}', '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.x509-certificate-bound-access-tokens":false,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",2592000.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}')

至此客户端的持久化实现完毕,可以按照上面的方法测试三种授权模式

自定义持久化

可以自己实现RegisteredClientRepository接口,按自己的逻辑实现方法即可

public interface RegisteredClientRepository {

	/**
	 * Saves the registered client.
	 *
	 * <p>
	 * IMPORTANT: Sensitive information should be encoded externally from the
	 * implementation, e.g. {@link RegisteredClient#getClientSecret()}
	 * @param registeredClient the {@link RegisteredClient}
	 */
	void save(RegisteredClient registeredClient);

	/**
	 * Returns the registered client identified by the provided {@code id}, or
	 * {@code null} if not found.
	 * @param id the registration identifier
	 * @return the {@link RegisteredClient} if found, otherwise {@code null}
	 */
	@Nullable
	RegisteredClient findById(String id);

	/**
	 * Returns the registered client identified by the provided {@code clientId}, or
	 * {@code null} if not found.
	 * @param clientId the client identifier
	 * @return the {@link RegisteredClient} if found, otherwise {@code null}
	 */
	@Nullable
	RegisteredClient findByClientId(String clientId);

}

总结

客户端的持久化比较简单,可以直接使用官方提供的JdbcRegisteredClientRepository(),也可以自己实现RegisteredClientRepository,按照自己的逻辑实现客户端保存和查询功能,只需要将实现类作为bean注入到ioc容器中即可

用户持久化

使用JdbcDaoImpl(不推荐,一般也不会用这个)

创建表

找到框架自带的数据库表脚本

修改配置-UserDetailsService

    private final JdbcTemplate jdbcTemplate;

    public SecurityConfig(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    @Bean
    public UserDetailsService userDetailsService() {
        JdbcDaoImpl jdbcDao = new JdbcDaoImpl();
        jdbcDao.setJdbcTemplate(jdbcTemplate);
        return jdbcDao;
    }

将之前的用户据插入到数据库中

INSERT INTO users (username, password, enabled) VALUES ('admin', '$2a$10$TdhQiv3We.BvTayb.KXoUOvI19xgHnw8fmKcE6kLuBD.LhehczevG', 1);
INSERT INTO authorities (username, authority) VALUES ('admin', 'user');

至此用户的持久化实现完毕,可以按照上面的方法测试三种授权模式

自定义持久化(推荐)

实现UserDetails

自定义自己的用户属性

package chick.authorization.security;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ChickUserDetails implements UserDetails {
    private String username;
    private String password;
    private String other;
    private Collection<? extends GrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getOther() {
        return other;
    }

    public void setOther(String other) {
        this.other = other;
    }

    public void setAuthorities(Set<GrantedAuthority> authorities) {
        this.authorities = authorities;
    }
}

实现UserDetailsService
package chick.authorization.security;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.*;

@Service
public class ChickUserDetailsService implements UserDetailsService {
    private final JdbcTemplate jdbcTemplate;

    public ChickUserDetailsService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询逻辑 通过用户名查询用户信息,按照自己的逻辑写
        String query = "SELECT username, password FROM users WHERE username = ?";
        ChickUserDetails chickUserDetails = jdbcTemplate.queryForObject(query, (rs, rowNum) -> {
            ChickUserDetails user = new ChickUserDetails();
            user.setUsername(rs.getString("username"));
            user.setPassword(rs.getString("password"));
            return user;
        }, username);
        if (ObjectUtils.isEmpty(chickUserDetails)){
            throw new UsernameNotFoundException(username + " not found");
        }

        String queryAuthority = "SELECT authority FROM authorities WHERE username = '" + username + "'";
        List<String> authorities = jdbcTemplate.query(queryAuthority, (rs, rowNum) -> rs.getString("authority"));
        Set<GrantedAuthority> simpleGrantedAuthorities = new HashSet<>();
        authorities.forEach(authority -> {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
            simpleGrantedAuthorities.add(simpleGrantedAuthority);
        });
        chickUserDetails.setAuthorities(simpleGrantedAuthorities);
        return chickUserDetails;
    }
}

去除之前配置文件中的UserDetailsService
//    @Bean
//    public UserDetailsService userDetailsService() {
//        JdbcDaoImpl jdbcDao = new JdbcDaoImpl();
//        jdbcDao.setJdbcTemplate(jdbcTemplate);
//        //UserDetails userDetails = User.withUsername("admin")
//        //        .password(passwordEncoder().encode("123123"))
//        //        .roles("admin")
//        //        .build();
//        //return new InMemoryUserDetailsManager(userDetails);
//        return jdbcDao;
//    }

完成并测试

总结

使用自己实现UserDetailsService的bean更具有灵活性,也可以结合redis等缓存技术给系统提速

token等登录信息持久化

创建表

找到框架中的脚本 在数据库中执行创建表

配置

    // 注入依赖
    private final JdbcTemplate jdbcTemplate;
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final RegisteredClientRepository registeredClientRepository;

    public SecurityConfig(JdbcTemplate jdbcTemplate,
                          UserDetailsService userDetailsService,
                          @Lazy PasswordEncoder passwordEncoder, 
                          @Lazy RegisteredClientRepository registeredClientRepository) {
        this.jdbcTemplate = jdbcTemplate;
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
        this.registeredClientRepository = registeredClientRepository;
    }

    // 将OAuth2AuthorizationServiceBean注入容器
    @Bean
    public OAuth2AuthorizationService oAuth2AuthorizationService() {
        // return new InMemoryOAuth2AuthorizationService(); 使用内存
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); // 使用数据库
    }

完成并测试,登录成功后可以发现token的信息保存到数据库中了

Opaque Token(不透明Token)

更改配置

不透明token是返回一个没有意义的id,然后通过id再去获取用户的token,使用不透明id只需要将客户端的TokenSettings的accessTokenFormat设置为OAuth2TokenFormat.REFERENCE

如果已实现持久化,可以直接更改数据库中的配置

测试登录

解析token

POST /oauth2/introspect HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

token=YxxTPrfmz__15X9UYH.......

token信息扩展

透明token扩展

package chick.authorization.token;

import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.stereotype.Service;

/**
 * @Author xkx
 * @Description 透明token扩展
 * @Date 2024/11/28 21:33
 * @Param
 * @return
 **/
@Service
public class ChickSelfContainedTokenEnhancer implements OAuth2TokenCustomizer<JwtEncodingContext> {
    @Override
    public void customize(JwtEncodingContext context) {
        context.getClaims().claims(claims -> {
            claims.put("custom1", "1");
            claims.put("custom2", "2");
        });
    }
}

不透明token扩展

package chick.authorization.token;

import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.stereotype.Service;

/**
* @Author xkx
* @Description 不透明token扩展
* @Date 2024/11/28 21:33
* @Param
* @return
**/
@Service
public class ChickReferenceTokenEnhancer implements OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {
    @Override
    public void customize(OAuth2TokenClaimsContext context) {
        context.getClaims().claims(claims -> {
            claims.put("custom1", "1");
            claims.put("custom2", "2");
        });
    }
}

配置修改

    // 透明token扩展
    private final ChickSelfContainedTokenEnhancer chickSelfContainedTokenEnhancer;
    // 非透明token扩展
    private final ChickReferenceTokenEnhancer chickReferenceTokenEnhancer;
    // 数据库链接
    private final JdbcTemplate jdbcTemplate;
    // 密码编码器
    private final PasswordEncoder passwordEncoder;
    // jwt编码器
    private final JwtEncoder jwtEncoder;
    // 客户端管理
    private final RegisteredClientRepository registeredClientRepository;
    // 用户检索
    private final UserDetailsService userDetailsService;

    public SecurityConfig(ChickSelfContainedTokenEnhancer chickSelfContainedTokenEnhancer,
                          ChickReferenceTokenEnhancer chickReferenceTokenEnhancer,
                          JdbcTemplate jdbcTemplate,
                          @Lazy PasswordEncoder passwordEncoder,
                          @Lazy JwtEncoder jwtEncoder,
                          @Lazy RegisteredClientRepository registeredClientRepository,
                          @Lazy UserDetailsService userDetailsService) {
        this.chickSelfContainedTokenEnhancer = chickSelfContainedTokenEnhancer;
        this.chickReferenceTokenEnhancer = chickReferenceTokenEnhancer;
        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
        this.jwtEncoder = jwtEncoder;
        this.registeredClientRepository = registeredClientRepository;
        this.userDetailsService = userDetailsService;
    }

    /**
     * token生成器。配置要使用的token生成器
     **/
    @Bean
    public OAuth2TokenGenerator<?> tokenGenerator() {
        // 当客户端的tokenSetting的OAuth2TokenFormat设置为OAuth2TokenFormat.SELF_CONTAINED时 使用下面的
        JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);// jwtToken生成器(当客户端的token格式为self-contained时使用)
        jwtGenerator.setJwtCustomizer(chickSelfContainedTokenEnhancer);// 设置jwt-token自定义扩展

        // 当客户端的tokenSetting的OAuth2TokenFormat设置为OAuth2TokenFormat.REFERENCE 使用下面的
        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();// 不透明的token生成器
        accessTokenGenerator.setAccessTokenCustomizer(chickReferenceTokenEnhancer);// 设置id-token自定义扩展

        // refreshToken生成器
        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();// refreshToken生成器
        return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

测试登录

发现不透明和透明token都加上了我们自定义的信息

自定义token生成器

以不透名token为例

package chick.authorization.token;

import org.springframework.security.crypto.keygen.StringKeyGenerator;
import java.util.UUID;

/*
   uuid生成
 */
public class UUIDKeyGenerator implements StringKeyGenerator {
    @Override
    public String generateKey() {
        return UUID.randomUUID().toString().toLowerCase();
    }
}

创建token生成器

package chick.authorization.token;

import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
* @Author xkx
* @Description 自定义token生成器
 * @Date 2024/12/3 22:33
* @Param
* @return
**/
public class UUIDOAuth2TokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
    private final StringKeyGenerator accessTokenGenerator = new UUIDKeyGenerator();

    @Override
    public OAuth2AccessToken generate(OAuth2TokenContext context) {
        // @formatter:off
        if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) ||
                !OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
            return null;
        }
        // @formatter:on

        String issuer = null;
        if (context.getAuthorizationServerContext() != null) {
            issuer = context.getAuthorizationServerContext().getIssuer();
        }
        RegisteredClient registeredClient = context.getRegisteredClient();

        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

        // @formatter:off
        OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
        if (StringUtils.hasText(issuer)) {
            claimsBuilder.issuer(issuer);
        }
        claimsBuilder
                .subject(context.getPrincipal().getName())
                .audience(Collections.singletonList(registeredClient.getClientId()))
                .issuedAt(issuedAt)
                .expiresAt(expiresAt)
                .notBefore(issuedAt)
                .id(UUID.randomUUID().toString());
        if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
            claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
        }
        OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();

        return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
                this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(),
                context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
    }

    private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
        private final Map<String, Object> claims;

        private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue,
                                        Instant issuedAt, Instant expiresAt, Set<String> scopes, Map<String, Object> claims) {
            super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
            this.claims = claims;
        }

        @Override
        public Map<String, Object> getClaims() {
            return this.claims;
        }

    }
}

创建refreshToken生成器

package chick.authorization.token;

import io.micrometer.common.lang.Nullable;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;

import java.time.Instant;

/**
* @Author xkx
* @Description 自定义refreshToken生成器
* @Date 2024/12/3 22:33
* @Param
* @return
**/
public class UUIDOAuth2RefreshTokenGenerator implements OAuth2TokenGenerator<OAuth2RefreshToken> {

    private final StringKeyGenerator refreshTokenGenerator = new UUIDKeyGenerator();

    @Nullable
    @Override
    public OAuth2RefreshToken generate(OAuth2TokenContext context) {
        if (!OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType())) {
            return null;
        }
        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
        return new OAuth2RefreshToken(this.refreshTokenGenerator.generateKey(), issuedAt, expiresAt);
    }
}

配置

    @Bean
    public OAuth2TokenGenerator<?> tokenGenerator() {
        JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
        UUIDOAuth2TokenGenerator accessTokenGenerator = new UUIDOAuth2TokenGenerator();// 不透明的token生成器
        UUIDOAuth2RefreshTokenGenerator refreshTokenGenerator = new UUIDOAuth2RefreshTokenGenerator();// refreshToken生成器
        return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

测试登录

总结

测试完成发现不透明token变成了uuid的形式,成功

扩展授权类型

自定义GrantType类型

package chick.authorization.granter;

import org.springframework.security.oauth2.core.AuthorizationGrantType;

/*
    扩展GrantType类型
 */
public record CustomAuthorizationGrantType(String value) {

    // 账号密码模式
    public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
}

工具类

package chick.authorization.utils;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
* @Author xkx
* @Description 端点工具类
* @Date 2024/12/1 19:55
* @Param
* @return
**/
public class OAuth2EndpointUtils {

	private OAuth2EndpointUtils() {
	}

	public static MultiValueMap<String, String> getFormParameters(HttpServletRequest request) {
		Map<String, String[]> parameterMap = request.getParameterMap();
		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
		parameterMap.forEach((key, values) -> {
			String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
			// If not query parameter then it's a form parameter
			if (!queryString.contains(key) && values.length > 0) {
				for (String value : values) {
					parameters.add(key, value);
				}
			}
		});
		return parameters;
	}

	public static MultiValueMap<String, String> getQueryParameters(HttpServletRequest request) {
		Map<String, String[]> parameterMap = request.getParameterMap();
		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
		parameterMap.forEach((key, values) -> {
			String queryString = StringUtils.hasText(request.getQueryString()) ? request.getQueryString() : "";
			if (queryString.contains(key) && values.length > 0) {
				for (String value : values) {
					parameters.add(key, value);
				}
			}
		});
		return parameters;
	}

	public static Map<String, Object> getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request,
			String... exclusions) {
		if (!matchesAuthorizationCodeGrantRequest(request)) {
			return Collections.emptyMap();
		}
		MultiValueMap<String, String> multiValueParameters = "GET".equals(request.getMethod())
				? getQueryParameters(request) : getFormParameters(request);
		for (String exclusion : exclusions) {
			multiValueParameters.remove(exclusion);
		}

		Map<String, Object> parameters = new HashMap<>();
		multiValueParameters.forEach(
				(key, value) -> parameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0])));

		return parameters;
	}

	public static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) {
		return AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
			.equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))
				&& request.getParameter(OAuth2ParameterNames.CODE) != null;
	}

	public static boolean matchesPkceTokenRequest(HttpServletRequest request) {
		return matchesAuthorizationCodeGrantRequest(request)
				&& request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
	}

	public static void throwError(String errorCode, String parameterName, String errorUri) {
		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
		throw new OAuth2AuthenticationException(error);
	}

	public static String normalizeUserCode(String userCode) {
		Assert.hasText(userCode, "userCode cannot be empty");
		StringBuilder sb = new StringBuilder(userCode.toUpperCase(Locale.ENGLISH).replaceAll("[^A-Z\\d]+", ""));
		Assert.isTrue(sb.length() == 8, "userCode must be exactly 8 alpha/numeric characters");
		sb.insert(4, '-');
		return sb.toString();
	}

	public static boolean validateUserCode(String userCode) {
		return (userCode != null && userCode.toUpperCase(Locale.ENGLISH).replaceAll("[^A-Z\\d]+", "").length() == 8);
	}

}

实现OAuth2AuthorizationGrantAuthenticationToken

package chick.authorization.granter.password;

import chick.authorization.granter.CustomAuthorizationGrantType;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;

import java.util.Map;

/**
* @Author xkx
* @Description 用户名密码令牌扩展
* @Date 2024/12/1 20:16
* @Param
* @return
**/
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
    private final String username;
    private final String password;

    public OAuth2PasswordAuthenticationToken(String username, String password, Authentication clientPrincipal, Map<String, Object> additionalParameters) {
        super(CustomAuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }
}

实现AuthenticationConverter 返回Authentication

框架会依次执行AuthenticationConverter的实现类,直到有一个可以处理并且返回的不是null为止

package chick.authorization.granter.password;


import chick.authorization.granter.CustomAuthorizationGrantType;
import chick.authorization.utils.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

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

@Service
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {

    @Override
    public Authentication convert(HttpServletRequest request) {
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        // 校验grant_type为password的
        if (!CustomAuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
            return null;
        }
        Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);

        // 用户名不能为空
        String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
        if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
            OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, "");
        }
        String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);

        Map<String, Object> additionalParameters = new HashMap<>();
        parameters.forEach((key, value) -> {
            if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && !key.equals(OAuth2ParameterNames.CLIENT_ID) && !key.equals(OAuth2ParameterNames.USERNAME) && !key.equals(OAuth2ParameterNames.PASSWORD)) {
                additionalParameters.put(key, value.getFirst());
            }
        });

        return new OAuth2PasswordAuthenticationToken(username, password, clientPrincipal, additionalParameters);
    }
}

实现AuthenticationProvider

框架会调用所有AuthenticationProvider的实现类的supports方法,如果返回true,代表该provider可以处理当前的Authentication

package chick.authorization.granter.password;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {

    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
    private final OAuth2AuthorizationService authorizationService;

    public OAuth2PasswordAuthenticationProvider(OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator, OAuth2AuthorizationService authorizationService) {
        this.tokenGenerator = tokenGenerator;
        this.authorizationService = authorizationService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2PasswordAuthenticationToken passwordAuthentication =
                (OAuth2PasswordAuthenticationToken) authentication;

        // Ensure the client is authenticated
        OAuth2ClientAuthenticationToken clientPrincipal =
                getAuthenticatedClientElseThrowInvalidClient(passwordAuthentication);
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();

        // Ensure the client is configured to use this authorization grant type
        assert registeredClient != null;
        if (!registeredClient.getAuthorizationGrantTypes().contains(passwordAuthentication.getGrantType())) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
        }

        // 校验账户
        String username = passwordAuthentication.getUsername();
        if (!StringUtils.hasText(username)) {
            throw new OAuth2AuthenticationException("账户不能为空");
        }
        // 校验密码
        String password = passwordAuthentication.getPassword();
        if (!StringUtils.hasText(password)) {
            throw new OAuth2AuthenticationException("密码不能为空");
        }

        // 查询账户信息 实际要根据自己的业务来写
//        SsoUserDetail ssoUserDetail = (SsoUserDetail) userDetailService.loadUserByUsername(username);
//        if (ssoUserDetail == null) {
//            throw new OAuth2AuthenticationException("账户信息不存在,请联系管理员");
//        }
//         校验密码
//        if (!passwordEncoder.matches(password, ssoUserDetail.getPassword())) {
//            throw new OAuth2AuthenticationException("密码不正确");
//        }

        // Generate the access token
        OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
                .registeredClient(registeredClient)
                .principal(clientPrincipal)
                .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                .tokenType(OAuth2TokenType.ACCESS_TOKEN)
                .authorizationGrantType(passwordAuthentication.getGrantType())
                .authorizationGrant(passwordAuthentication)
                .build();

        OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
        if (generatedAccessToken == null) {
            OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                    "The token generator failed to generate the access token.", null);
            throw new OAuth2AuthenticationException(error);
        }
        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
                generatedAccessToken.getExpiresAt(), null);

        // Initialize the OAuth2Authorization
        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(clientPrincipal.getName())
                .authorizationGrantType(passwordAuthentication.getGrantType());
        if (generatedAccessToken instanceof ClaimAccessor) {
            authorizationBuilder.token(accessToken, (metadata) ->
                    metadata.put(
                            OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
                            ((ClaimAccessor) generatedAccessToken).getClaims())
            );
        } else {
            authorizationBuilder.accessToken(accessToken);
        }
        OAuth2Authorization authorization = authorizationBuilder.build();

        // Save the OAuth2Authorization
        this.authorizationService.save(authorization);

        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient, clientPrincipal, accessToken);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
        OAuth2ClientAuthenticationToken clientPrincipal = null;
        if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
            clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
        }
        if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
            return clientPrincipal;
        }
        throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
    }
}

配置

    // 透明token扩展
    private final ChickSelfContainedTokenEnhancer chickSelfContainedTokenEnhancer;
    // 非透明token扩展
    private final ChickReferenceTokenEnhancer chickReferenceTokenEnhancer;
    // 数据库链接
    private final JdbcTemplate jdbcTemplate;
    // 密码编码器
    private final PasswordEncoder passwordEncoder;
    // jwt编码器
    private final JwtEncoder jwtEncoder;
    // 客户端管理
    private final RegisteredClientRepository registeredClientRepository;
    // 用户检索
    private final UserDetailsService userDetailsService;
    // 扩展Provider
    private final OAuth2PasswordAuthenticationProvider oAuth2PasswordAuthenticationProvider;
    // 扩展Converter
    private final OAuth2PasswordAuthenticationConverter oAuth2PasswordAuthenticationConverter;

    public SecurityConfig(ChickSelfContainedTokenEnhancer chickSelfContainedTokenEnhancer,
                          ChickReferenceTokenEnhancer chickReferenceTokenEnhancer,
                          JdbcTemplate jdbcTemplate,
                          @Lazy PasswordEncoder passwordEncoder,
                          @Lazy JwtEncoder jwtEncoder,
                          @Lazy RegisteredClientRepository registeredClientRepository,
                          @Lazy UserDetailsService userDetailsService,
                          @Lazy OAuth2PasswordAuthenticationProvider oAuth2PasswordAuthenticationProvider,
                          @Lazy OAuth2PasswordAuthenticationConverter oAuth2PasswordAuthenticationConverter) {
        this.chickSelfContainedTokenEnhancer = chickSelfContainedTokenEnhancer;
        this.chickReferenceTokenEnhancer = chickReferenceTokenEnhancer;
        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
        this.jwtEncoder = jwtEncoder;
        this.registeredClientRepository = registeredClientRepository;
        this.userDetailsService = userDetailsService;
        this.oAuth2PasswordAuthenticationProvider = oAuth2PasswordAuthenticationProvider;
        this.oAuth2PasswordAuthenticationConverter = oAuth2PasswordAuthenticationConverter;
    }

    // 配置自定义的授权类型
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
                OAuth2AuthorizationServerConfigurer.authorizationServer();

        http
                .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
                .with(authorizationServerConfigurer, Customizer.withDefaults())
                .authorizeHttpRequests((authorize) ->
                        authorize.anyRequest().authenticated()
                )
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                ).oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()))
                .with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer
                        .tokenEndpoint(tokenEndpoint -> tokenEndpoint
                                .accessTokenRequestConverter(oAuth2PasswordAuthenticationConverter)
                                .authenticationProvider(oAuth2PasswordAuthenticationProvider)
                        )
                );
        http
                .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        return http.build();
    }

    // 客户端增加支持的授权类型
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        // JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        TokenSettings tokenSettings = TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofHours(1)) // 设置访问令牌有效期为1小时
                .refreshTokenTimeToLive(Duration.ofDays(30)) // 设置刷新令牌有效期为30天
                //.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 这个设置是开启不透明token
                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 使用透明token(默认)
                .build();
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("chick")
                .clientSecret(passwordEncoder().encode("123456"))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 新增
                .authorizationGrantType(CustomAuthorizationGrantType.PASSWORD)
                .redirectUri("https://www.baidu.com")
                .postLogoutRedirectUri("http://127.0.0.1:8000/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .tokenSettings(tokenSettings)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();
        return new InMemoryRegisteredClientRepository(oidcClient);
        //jdbcRegisteredClientRepository.save(oidcClient);//第一次启动可以打开这个 将客户端保存到数据库
        //return jdbcRegisteredClientRepository;
    }

测试登录

POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=admin&password=123123

成功

总结

框架提供了授权类型扩展,可以更好的适应不同项目的不同需求,也可以增加手机号码、邮箱等登录方式

;