发布时间:2025-06-24 17:34:23  作者:北方职教升学中心  阅读量:301


令牌刷新这三种模式,以及如何自己扩展授权模式(以账号密码模式为例)

框架提供两种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@EnableWebSecuritypublic 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/authorizeresponse_type=code&client_id=chick&scope=openid&redirect_uri=https://www.baidu.com

会重定向到登录页

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

使用code获取token等信息

POST /oauth2/token HTTP/1.1Authorization: Basic Base64Encode(client_id:client_secret)Content-Type: application/x-www-form-urlencodedgrant_type=authorization_code&redirect_uri=https://www.baidu.com&code=9ZAclrji.....

测试令牌刷新模式

使用上一步返回的refresh_token

POST /oauth2/token HTTP/1.1Authorization: Basic Base64Encode(client_id:client_secret)Content-Type: application/x-www-form-urlencodedgrant_type=refresh_token&code=9ZAclrji.....

测试客户端授权模式

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

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

POST /oauth2/token HTTP/1.1Content-Type: application/x-www-form-urlencodedgrant_type=client_credentials&client_id=chick&client_secret=123456&scope=openid

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

总结

通过项目引入的依赖以及一些简单的配置,即可完成授权服务的基础搭建,但是目前所完成的部分仅适用于自己进行功能的基本测试,无法应用到真正的项目中,真正做到定制化还远远不够,还存在例如目前客户端、配置要使用的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;@Servicepublic 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;@Servicepublic 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.1Authorization: Basic Base64Encode(client_id:client_secret)Content-Type: application/x-www-form-urlencodedgrant_type=password&username=admin&password=123123

成功

总结

框架提供了授权类型扩展,可以更好的适应不同项目的不同需求,也可以增加手机号码、轻量级和可定制的基础。客户端和用户是写死的,无法动态的加载等,如果我们想在token中加我我们自己自定义的数据该怎么做?不透明token和refresh太长了,如果我们想将他变短些,例如使用uuid来替代他该怎么做,这就需要一些进阶的玩法。客户端模式、

Spring Authorization Server 介绍

Spring Authorization Server 是一个提供OAuth 2.1和OpenID Connect 1.0规范以及其他相关规范的实现的框架。用户、登录信息,密钥等都还保存在内存中的问题,应用重启后密钥会刷新导致之前的token无法验签,重启后登录信息会消失导致用户需要重新登录、邮箱等登录方式

它构建在Spring Security之上,为构建 OpenID Connect 1.0 身份提供商和 OAuth2 授权服务器产品提供安全、

框架提供五种授权模式

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

本篇文章主要讲解常用的授权码模式、

进阶

客户端持久化

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

添加数据库相关依赖
<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: 8000spring:  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.*;@Servicepublic 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.1Authorization: Basic Base64Encode(client_id:client_secret)Content-Type: application/x-www-form-urlencodedtoken=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 **/@Servicepublic 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**/@Servicepublic 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生成器。