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格式
- Self-contained (JWT) (信息透明)
- 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
成功
总结
框架提供了授权类型扩展,可以更好的适应不同项目的不同需求,也可以增加手机号码、邮箱等登录方式