在新版本的sas(1.2.1)中獲取token更新了授權(quán)碼校驗(yàn)邏輯,只能用form-data傳遞參數(shù),使用url-params會(huì)失敗,原因見(jiàn)issue1451
對(duì)應(yīng)的 commit 在這里: Fix to ensure endpoints distinguish between form and query parameters
前言
文章較長(zhǎng),步驟比較繁瑣,請(qǐng)各位讀者耐心觀看。
上篇文章大概了解了下框架的相關(guān)理論,本篇文章將帶大家一步步構(gòu)建一個(gè)簡(jiǎn)單的認(rèn)證服務(wù)器
開(kāi)始之前先放一下文檔的鏈接:官網(wǎng)文檔
項(xiàng)目環(huán)境要求(當(dāng)前框架版本1.1.0)
- Spring Boot版本大于等于3.1.0-RC1
- JDK版本大于等于17
認(rèn)證項(xiàng)目搭建
1. 在Idea中或Spring Initializr中創(chuàng)建spring boot項(xiàng)目
-
Spring Boot版本選擇3.1.0,Java版本選擇17以上,在Dependencies中勾選Spring Authorization Server和spring web依賴,其它看自己需要
引入持久層框架(本人用的是MybatisPlus,讀者可自選)
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>
引入webjars和bootstrap,自定義登錄頁(yè)和確認(rèn)頁(yè)面時(shí)使用
<dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.2.3</version> </dependency>
項(xiàng)目pom.xml示例
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>authorization-example</artifactId> <version>0.0.1-SNAPSHOT</version> <name>authorization-example</name> <description>authorization-example</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>5.2.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
-
初始化框架自帶數(shù)據(jù)庫(kù)表
schema位置如圖
修改后適配MySQL的SQL如下
-- 用戶授權(quán)確認(rèn)表 CREATE TABLE oauth2_authorization_consent ( registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorities varchar(1000) NOT NULL, PRIMARY KEY (registered_client_id, principal_name) ); -- 用戶認(rèn)證信息表 CREATE TABLE oauth2_authorization ( id varchar(100) NOT NULL, registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorization_grant_type varchar(100) NOT NULL, authorized_scopes varchar(1000) DEFAULT NULL, attributes blob DEFAULT NULL, state varchar(500) DEFAULT NULL, authorization_code_value blob DEFAULT NULL, authorization_code_issued_at DATETIME DEFAULT NULL, authorization_code_expires_at DATETIME DEFAULT NULL, authorization_code_metadata blob DEFAULT NULL, access_token_value blob DEFAULT NULL, access_token_issued_at DATETIME DEFAULT NULL, access_token_expires_at DATETIME DEFAULT NULL, access_token_metadata blob DEFAULT NULL, access_token_type varchar(100) DEFAULT NULL, access_token_scopes varchar(1000) DEFAULT NULL, oidc_id_token_value blob DEFAULT NULL, oidc_id_token_issued_at DATETIME DEFAULT NULL, oidc_id_token_expires_at DATETIME DEFAULT NULL, oidc_id_token_metadata blob DEFAULT NULL, refresh_token_value blob DEFAULT NULL, refresh_token_issued_at DATETIME DEFAULT NULL, refresh_token_expires_at DATETIME DEFAULT NULL, refresh_token_metadata blob DEFAULT NULL, user_code_value blob DEFAULT NULL, user_code_issued_at DATETIME DEFAULT NULL, user_code_expires_at DATETIME DEFAULT NULL, user_code_metadata blob DEFAULT NULL, device_code_value blob DEFAULT NULL, device_code_issued_at DATETIME DEFAULT NULL, device_code_expires_at DATETIME DEFAULT NULL, device_code_metadata blob DEFAULT NULL, PRIMARY KEY (id) ); -- 客戶端表 CREATE TABLE oauth2_registered_client ( id varchar(100) NOT NULL, client_id varchar(100) NOT NULL, client_id_issued_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, client_secret varchar(200) DEFAULT NULL, client_secret_expires_at DATETIME DEFAULT NULL, client_name varchar(200) NOT NULL, client_authentication_methods varchar(1000) NOT NULL, authorization_grant_types varchar(1000) NOT NULL, redirect_uris varchar(1000) DEFAULT NULL, post_logout_redirect_uris varchar(1000) DEFAULT NULL, scopes varchar(1000) NOT NULL, client_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL, PRIMARY KEY (id) );
2. 在config包下創(chuàng)建AuthorizationConfig類,并添加配置
配置端點(diǎn)的過(guò)濾器鏈
/**
* 配置端點(diǎn)的過(guò)濾器鏈
*
* @param http spring security核心配置類
* @return 過(guò)濾器鏈
* @throws Exception 拋出
*/
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 配置默認(rèn)的設(shè)置,忽略認(rèn)證端點(diǎn)的csrf校驗(yàn)
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 開(kāi)啟OpenID Connect 1.0協(xié)議相關(guān)端點(diǎn)
.oidc(Customizer.withDefaults())
// 設(shè)置自定義用戶確認(rèn)授權(quán)頁(yè)
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
http
// 當(dāng)未登錄時(shí)訪問(wèn)認(rèn)證端點(diǎn)時(shí)重定向至login頁(yè)面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 處理使用access token訪問(wèn)用戶信息端點(diǎn)和客戶端注冊(cè)端點(diǎn)
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
配置身份驗(yàn)證過(guò)濾器鏈
/**
* 配置認(rèn)證相關(guān)的過(guò)濾器鏈
*
* @param http spring security核心配置類
* @return 過(guò)濾器鏈
* @throws Exception 拋出
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
// 放行靜態(tài)資源
.requestMatchers("/assets/**", "/webjars/**", "/login").permitAll()
.anyRequest().authenticated()
)
// 指定登錄頁(yè)面
.formLogin(formLogin ->
formLogin.loginPage("/login")
);
// 添加BearerTokenAuthenticationFilter,將認(rèn)證服務(wù)當(dāng)做一個(gè)資源服務(wù),解析請(qǐng)求頭中的token
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
配置密碼解析器
/**
* 配置密碼解析器,使用BCrypt的方式對(duì)密碼進(jìn)行加密和驗(yàn)證
*
* @return BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
配置客戶端repository
/**
* 配置客戶端Repository
*
* @param jdbcTemplate db 數(shù)據(jù)源信息
* @param passwordEncoder 密碼解析器
* @return 基于數(shù)據(jù)庫(kù)的repository
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客戶端id
.clientId("messaging-client")
// 客戶端秘鑰,使用密碼解析器加密
.clientSecret(passwordEncoder.encode("123456"))
// 客戶端認(rèn)證方式,基于請(qǐng)求頭的認(rèn)證
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置資源服務(wù)器使用該客戶端獲取授權(quán)時(shí)支持的方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 授權(quán)碼模式回調(diào)地址,oauth2.1已改為精準(zhǔn)匹配,不能只設(shè)置域名,并且屏蔽了localhost
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
// 配置一個(gè)百度的域名回調(diào),稍后使用該回調(diào)獲取code
.redirectUri("https://www.baidu.com")
// 該客戶端的授權(quán)范圍,OPENID與PROFILE是IdToken的scope,獲取授權(quán)時(shí)請(qǐng)求OPENID的scope時(shí)認(rèn)證服務(wù)會(huì)返回IdToken
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 自定scope
.scope("message.read")
.scope("message.write")
// 客戶端設(shè)置,設(shè)置用戶需要確認(rèn)授權(quán)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// 基于db存儲(chǔ)客戶端,還有一個(gè)基于內(nèi)存的實(shí)現(xiàn) InMemoryRegisteredClientRepository
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// 初始化客戶端
RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
// 設(shè)備碼授權(quán)客戶端
RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("device-message-client")
// 公共客戶端
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
// 設(shè)備碼授權(quán)
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 自定scope
.scope("message.read")
.scope("message.write")
.build();
RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());
if (byClientId == null) {
registeredClientRepository.save(deviceClient);
}
return registeredClientRepository;
}
客戶端設(shè)置(ClientSettings
)說(shuō)明
requireProofKey
:當(dāng)使用該客戶端發(fā)起PKCE流程時(shí)必須設(shè)置為true。requireAuthorizationConsent
:當(dāng)設(shè)置為true時(shí)登錄后會(huì)先跳轉(zhuǎn)授權(quán)確認(rèn)頁(yè)面,確認(rèn)后才會(huì)跳轉(zhuǎn)到redirect_uri,為false時(shí)不會(huì)跳轉(zhuǎn)至授權(quán)確認(rèn)頁(yè)面。jwkSetUrl
:設(shè)置客戶端jwks的url。tokenEndpointAuthenticationSigningAlgorithm
:設(shè)置token端點(diǎn)對(duì)驗(yàn)證方法為CLIENT_SECRET_JWT,PRIVATE_KEY_JWT的客戶端進(jìn)行身份驗(yàn)證使用的簽名算法。
token設(shè)置(TokenSettings
)說(shuō)明
authorizationCodeTimeToLive
:授權(quán)碼(authorization_code)有效時(shí)長(zhǎng)。accessTokenTimeToLive
:access_token有效時(shí)長(zhǎng)。accessTokenFormat
:access_token的格式,SELF_CONTAINED
是自包含token(jwt格式),REFERENCE
是不透明token,相相當(dāng)于是token元數(shù)據(jù)的一個(gè)id,通過(guò)id找到對(duì)應(yīng)數(shù)據(jù)(自省令牌時(shí)),如下
public final class OAuth2TokenFormat implements Serializable {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
/**
* Self-contained tokens use a protected, time-limited data structure that contains token metadata
* and claims of the user and/or client. JSON Web Token (JWT) is a widely used format.
*/
public static final OAuth2TokenFormat SELF_CONTAINED = new OAuth2TokenFormat("self-contained");
/**
* Reference (opaque) tokens are unique identifiers that serve as a reference
* to the token metadata and claims of the user and/or client, stored at the provider.
*/
public static final OAuth2TokenFormat REFERENCE = new OAuth2TokenFormat("reference");
}
deviceCodeTimeToLive
:設(shè)備碼有效時(shí)長(zhǎng)。reuseRefreshTokens
:刷新token時(shí)是否重用refresh token,設(shè)置為true后refresh token不變,false刷新token時(shí)會(huì)重新簽發(fā)一個(gè)refresh token。refreshTokenTimeToLive
:refresh token有效時(shí)長(zhǎng)。idTokenSignatureAlgorithm
:設(shè)置id token的加密算法。
如果數(shù)據(jù)庫(kù)已經(jīng)存在客戶端數(shù)據(jù)或不需要默認(rèn)設(shè)置,則直接注入一個(gè)JdbcRegisteredClientRepository即可
/**
* 配置客戶端Repository
*
* @param jdbcTemplate db 數(shù)據(jù)源信息
* @return 基于數(shù)據(jù)庫(kù)的repository
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
配置授權(quán)管理服務(wù)
/**
* 配置基于db的oauth2的授權(quán)管理服務(wù)
*
* @param jdbcTemplate db數(shù)據(jù)源信息
* @param registeredClientRepository 上邊注入的客戶端repository
* @return JdbcOAuth2AuthorizationService
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// 基于db的oauth2認(rèn)證服務(wù),還有一個(gè)基于內(nèi)存的服務(wù)InMemoryOAuth2AuthorizationService
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
配置授權(quán)確認(rèn)管理服務(wù)
/**
* 配置基于db的授權(quán)確認(rèn)管理服務(wù)
*
* @param jdbcTemplate db數(shù)據(jù)源信息
* @param registeredClientRepository 客戶端repository
* @return JdbcOAuth2AuthorizationConsentService
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// 基于db的授權(quán)確認(rèn)管理服務(wù),還有一個(gè)基于內(nèi)存的服務(wù)實(shí)現(xiàn)InMemoryOAuth2AuthorizationConsentService
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
配置jwk
/**
* 配置jwk源,使用非對(duì)稱加密,公開(kāi)用于檢索匹配指定選擇器的JWK的方法
*
* @return JWKSource
*/
@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);
}
/**
* 生成rsa密鑰對(duì),提供給jwk
*
* @return 密鑰對(duì)
*/
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;
}
配置jwt解析器
/**
* 配置jwt解析器
*
* @param jwkSource jwk源
* @return JwtDecoder
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
配置認(rèn)證服務(wù)器設(shè)置
/**
* 添加認(rèn)證服務(wù)器配置,設(shè)置jwt簽發(fā)者、默認(rèn)端點(diǎn)請(qǐng)求地址等
*
* @return AuthorizationServerSettings
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
配置一個(gè)基于內(nèi)存的默認(rèn)用戶
/**
* 先暫時(shí)配置一個(gè)基于內(nèi)存的用戶,框架在用戶認(rèn)證時(shí)會(huì)默認(rèn)調(diào)用
* {@link UserDetailsService#loadUserByUsername(String)} 方法根據(jù)
* 賬號(hào)查詢用戶信息,一般是重寫該方法實(shí)現(xiàn)自己的邏輯
*
* @param passwordEncoder 密碼解析器
* @return UserDetailsService
*/
@Bean
public UserDetailsService users(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("admin")
.password(passwordEncoder.encode("123456"))
.roles("admin", "normal")
.authorities("app", "web")
.build();
return new InMemoryUserDetailsManager(user);
}
完整的AuthorizationConfig.java如下
package com.example.config;
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.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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;
/**
* 認(rèn)證配置
* {@link EnableMethodSecurity} 開(kāi)啟全局方法認(rèn)證,啟用JSR250注解支持,啟用注解 {@link Secured} 支持,
* 在Spring Security 6.0版本中將@Configuration注解從@EnableWebSecurity, @EnableMethodSecurity, @EnableGlobalMethodSecurity
* 和 @EnableGlobalAuthentication 中移除,使用這些注解需手動(dòng)添加 @Configuration 注解
* {@link EnableWebSecurity} 注解有兩個(gè)作用:
* 1. 加載了WebSecurityConfiguration配置類, 配置安全認(rèn)證策略。
* 2. 加載了AuthenticationConfiguration, 配置了認(rèn)證信息。
*
* @author vains
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
/**
* 配置端點(diǎn)的過(guò)濾器鏈
*
* @param http spring security核心配置類
* @return 過(guò)濾器鏈
* @throws Exception 拋出
*/
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 配置默認(rèn)的設(shè)置,忽略認(rèn)證端點(diǎn)的csrf校驗(yàn)
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 開(kāi)啟OpenID Connect 1.0協(xié)議相關(guān)端點(diǎn)
.oidc(Customizer.withDefaults())
// 設(shè)置自定義用戶確認(rèn)授權(quán)頁(yè)
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
http
// 當(dāng)未登錄時(shí)訪問(wèn)認(rèn)證端點(diǎn)時(shí)重定向至login頁(yè)面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 處理使用access token訪問(wèn)用戶信息端點(diǎn)和客戶端注冊(cè)端點(diǎn)
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 配置認(rèn)證相關(guān)的過(guò)濾器鏈
*
* @param http spring security核心配置類
* @return 過(guò)濾器鏈
* @throws Exception 拋出
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
// 放行靜態(tài)資源
.requestMatchers("/assets/**", "/webjars/**", "/login").permitAll()
.anyRequest().authenticated()
)
// 指定登錄頁(yè)面
.formLogin(formLogin ->
formLogin.loginPage("/login")
);
// 添加BearerTokenAuthenticationFilter,將認(rèn)證服務(wù)當(dāng)做一個(gè)資源服務(wù),解析請(qǐng)求頭中的token
http.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 配置密碼解析器,使用BCrypt的方式對(duì)密碼進(jìn)行加密和驗(yàn)證
*
* @return BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置客戶端Repository
*
* @param jdbcTemplate db 數(shù)據(jù)源信息
* @param passwordEncoder 密碼解析器
* @return 基于數(shù)據(jù)庫(kù)的repository
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客戶端id
.clientId("messaging-client")
// 客戶端秘鑰,使用密碼解析器加密
.clientSecret(passwordEncoder.encode("123456"))
// 客戶端認(rèn)證方式,基于請(qǐng)求頭的認(rèn)證
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置資源服務(wù)器使用該客戶端獲取授權(quán)時(shí)支持的方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 授權(quán)碼模式回調(diào)地址,oauth2.1已改為精準(zhǔn)匹配,不能只設(shè)置域名,并且屏蔽了localhost,本機(jī)使用127.0.0.1訪問(wèn)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("https://www.baidu.com")
// 該客戶端的授權(quán)范圍,OPENID與PROFILE是IdToken的scope,獲取授權(quán)時(shí)請(qǐng)求OPENID的scope時(shí)認(rèn)證服務(wù)會(huì)返回IdToken
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 自定scope
.scope("message.read")
.scope("message.write")
// 客戶端設(shè)置,設(shè)置用戶需要確認(rèn)授權(quán)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// 基于db存儲(chǔ)客戶端,還有一個(gè)基于內(nèi)存的實(shí)現(xiàn) InMemoryRegisteredClientRepository
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// 初始化客戶端
RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
// 設(shè)備碼授權(quán)客戶端
RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("device-message-client")
// 公共客戶端
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
// 設(shè)備碼授權(quán)
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 自定scope
.scope("message.read")
.scope("message.write")
.build();
RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());
if (byClientId == null) {
registeredClientRepository.save(deviceClient);
}
return registeredClientRepository;
}
/**
* 配置基于db的oauth2的授權(quán)管理服務(wù)
*
* @param jdbcTemplate db數(shù)據(jù)源信息
* @param registeredClientRepository 上邊注入的客戶端repository
* @return JdbcOAuth2AuthorizationService
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// 基于db的oauth2認(rèn)證服務(wù),還有一個(gè)基于內(nèi)存的服務(wù)實(shí)現(xiàn)InMemoryOAuth2AuthorizationService
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 配置基于db的授權(quán)確認(rèn)管理服務(wù)
*
* @param jdbcTemplate db數(shù)據(jù)源信息
* @param registeredClientRepository 客戶端repository
* @return JdbcOAuth2AuthorizationConsentService
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// 基于db的授權(quán)確認(rèn)管理服務(wù),還有一個(gè)基于內(nèi)存的服務(wù)實(shí)現(xiàn)InMemoryOAuth2AuthorizationConsentService
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 配置jwk源,使用非對(duì)稱加密,公開(kāi)用于檢索匹配指定選擇器的JWK的方法
*
* @return JWKSource
*/
@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);
}
/**
* 生成rsa密鑰對(duì),提供給jwk
*
* @return 密鑰對(duì)
*/
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;
}
/**
* 配置jwt解析器
*
* @param jwkSource jwk源
* @return JwtDecoder
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 添加認(rèn)證服務(wù)器配置,設(shè)置jwt簽發(fā)者、默認(rèn)端點(diǎn)請(qǐng)求地址等
*
* @return AuthorizationServerSettings
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
/**
* 先暫時(shí)配置一個(gè)基于內(nèi)存的用戶,框架在用戶認(rèn)證時(shí)會(huì)默認(rèn)調(diào)用
* {@link UserDetailsService#loadUserByUsername(String)} 方法根據(jù)
* 賬號(hào)查詢用戶信息,一般是重寫該方法實(shí)現(xiàn)自己的邏輯
*
* @param passwordEncoder 密碼解析器
* @return UserDetailsService
*/
@Bean
public UserDetailsService users(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("admin")
.password(passwordEncoder.encode("123456"))
.roles("admin", "normal", "unAuthentication")
.authorities("app", "web", "/test2", "/test3")
.build();
return new InMemoryUserDetailsManager(user);
}
}
注意,配置類中提到的基于內(nèi)存存儲(chǔ)的類禁止用于生產(chǎn)環(huán)境
3. 添加AuthorizationController,將請(qǐng)求轉(zhuǎn)發(fā)至自定義的登錄頁(yè)面和用戶確認(rèn)授權(quán)頁(yè)面
以下代碼摘抄自官方示例
使用thymeleaf框架渲染頁(yè)面
package com.example.controller;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 認(rèn)證服務(wù)器相關(guān)自定接口
*
* @author vains
*/
@Controller
@RequiredArgsConstructor
public class AuthorizationController {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state,
@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {
// Remove scopes that were already approved
Set<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throw new RuntimeException("客戶端不存在");
}
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
Set<String> authorizedScopes;
if (currentAuthorizationConsent != null) {
authorizedScopes = currentAuthorizationConsent.getScopes();
} else {
authorizedScopes = Collections.emptySet();
}
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
if (OidcScopes.OPENID.equals(requestedScope)) {
continue;
}
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
}
model.addAttribute("clientId", clientId);
model.addAttribute("state", state);
model.addAttribute("scopes", withDescription(scopesToApprove));
model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
model.addAttribute("principalName", principal.getName());
model.addAttribute("userCode", userCode);
if (StringUtils.hasText(userCode)) {
model.addAttribute("requestURI", "/oauth2/device_verification");
} else {
model.addAttribute("requestURI", "/oauth2/authorize");
}
return "consent";
}
private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
for (String scope : scopes) {
scopeWithDescriptions.add(new ScopeWithDescription(scope));
}
return scopeWithDescriptions;
}
@Data
public static class ScopeWithDescription {
private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
private static final Map<String, String> scopeDescriptions = new HashMap<>();
static {
scopeDescriptions.put(
OidcScopes.PROFILE,
"This application will be able to read your profile information."
);
scopeDescriptions.put(
"message.read",
"This application will be able to read your message."
);
scopeDescriptions.put(
"message.write",
"This application will be able to add new messages. It will also be able to edit and delete existing messages."
);
scopeDescriptions.put(
"other.scope",
"This is another scope example of a scope description."
);
}
public final String scope;
public final String description;
ScopeWithDescription(String scope) {
this.scope = scope;
this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
}
}
}
4. 在application.yml中配置db數(shù)據(jù)源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/authorization-example?serverTimezone=UTC&userUnicode=true&characterEncoding=utf-8
username: root
password: root
5. 編寫登錄頁(yè)面和用戶授權(quán)確認(rèn)頁(yè)面
以下代碼摘抄自官方示例
登錄頁(yè)面 login.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spring Authorization Server sample</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<link rel="stylesheet" href="/assets/css/signin.css" th:href="@{/assets/css/signin.css}" />
</head>
<body>
<div class="container">
<form class="form-signin w-100 m-auto" method="post" th:action="@{/login}">
<div th:if="${param.error}" class="alert alert-danger" role="alert">
Invalid username or password.
</div>
<div th:if="${param.logout}" class="alert alert-success" role="alert">
You have been logged out.
</div>
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="text" id="username" name="username" class="form-control" required autofocus>
<label for="username">Username</label>
</div>
<div class="form-floating">
<input type="password" id="password" name="password" class="form-control" required>
<label for="password">Password</label>
</div>
<div>
<button class="w-100 btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</div>
</form>
</div>
</body>
</html>
登錄頁(yè)面css, signin.css
html,
body {
height: 100%;
}
body {
display: flex;
align-items: start;
padding-top: 100px;
background-color: #f5f5f5;
}
.form-signin {
max-width: 330px;
padding: 15px;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="username"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
用戶授權(quán)確認(rèn)頁(yè)面consent.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Custom consent page - Consent required</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
<script>
function cancelConsent() {
document.consent_form.reset();
document.consent_form.submit();
}
</script>
</head>
<body>
<div class="container">
<div class="row py-5">
<h1 class="text-center text-primary">App permissions</h1>
</div>
<div class="row">
<div class="col text-center">
<p>
The application
<span class="fw-bold text-primary" th:text="${clientId}"></span>
wants to access your account
<span class="fw-bold" th:text="${principalName}"></span>
</p>
</div>
</div>
<div th:if="${userCode}" class="row">
<div class="col text-center">
<p class="alert alert-warning">
You have provided the code
<span class="fw-bold" th:text="${userCode}"></span>.
Verify that this code matches what is shown on your device.
</p>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<p>
The following permissions are requested by the above app.<br/>
Please review these and consent if you approve.
</p>
</div>
</div>
<div class="row">
<div class="col text-center">
<form name="consent_form" method="post" th:action="${requestURI}">
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<input th:if="${userCode}" type="hidden" name="user_code" th:value="${userCode}">
<div th:each="scope: ${scopes}" class="form-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}" th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">
You have already granted the following permissions to the above app:
</p>
<div th:each="scope: ${previouslyApprovedScopes}" class="form-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}" th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<div class="pt-3">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
Submit Consent
</button>
</div>
<div class="pt-3">
<button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
Cancel
</button>
</div>
</form>
</div>
</div>
<div class="row pt-4">
<div class="col text-center">
<p>
<small>
Your consent to provide access is required.<br/>
If you do not approve, click Cancel, in which case no information will be shared with the app.
</small>
</p>
</div>
</div>
</div>
</body>
</html>
至此,一個(gè)簡(jiǎn)單的認(rèn)證服務(wù)就搭建成功了。
本來(lái)不想設(shè)置自定義頁(yè)面的,但是不知道是本人的網(wǎng)絡(luò)問(wèn)題,還是默認(rèn)的頁(yè)面里的css相關(guān)cdn無(wú)法訪問(wèn),頁(yè)面加載巨慢還丑,只能從官方示例中拿一下登錄頁(yè)面和用戶授權(quán)確認(rèn)頁(yè)面,css改為從項(xiàng)目的webjars中引入
最后放一下項(xiàng)目結(jié)構(gòu)圖
6. 簡(jiǎn)單測(cè)試
1. 拼接url,訪問(wèn)授權(quán)接口
http://127.0.0.1:8080/oauth2/authorize?client_id=messaging-client&response_type=code&scope=message.read&redirect_uri=https%3A%2F%2Fwww.baidu.com
2. 授權(quán)接口檢測(cè)到未登錄,重定向至登錄頁(yè)面
3. 輸入AuthorizationConfig中配置的賬號(hào)密碼
賬號(hào):admin, 密碼:123456
4. 登錄成功后跳轉(zhuǎn)至授權(quán)確認(rèn)頁(yè)面
登錄成功跳轉(zhuǎn)至第1步的授權(quán)接口,授權(quán)接口檢測(cè)到用戶未確認(rèn)授權(quán),跳轉(zhuǎn)至授權(quán)確認(rèn)頁(yè)面
選擇對(duì)應(yīng)的scope并提交確認(rèn)權(quán)限
5. 提交后重定向至第1步的授權(quán)接口
授權(quán)接口生成code并重定向至第1步請(qǐng)求授權(quán)接口時(shí)攜帶的redirectUri地址,重定向時(shí)攜帶上參數(shù)code和state,我這里省略掉了state參數(shù),重定向之后只會(huì)攜帶code參數(shù);state用來(lái)防止CSRF攻擊,正式請(qǐng)求需生成并攜帶state參數(shù)。
6. 用戶確認(rèn)授權(quán)后攜帶code跳轉(zhuǎn)至redirectUri
一般來(lái)說(shuō)配置的回調(diào)地址都是客戶端的接口,接口在接收到回調(diào)時(shí)根據(jù)code去換取accessToken,接下來(lái)我會(huì)用postman模擬客戶端發(fā)起一個(gè)http請(qǐng)求去換取token
不知道為什么在手機(jī)瀏覽器上看回調(diào)至百度的圖片在平臺(tái)顯示違規(guī),這里我放一張另一個(gè)回調(diào)地址的圖片替代
7. 根據(jù)code換取AccessToken
請(qǐng)求/oauth2/token接口
1. 設(shè)置Basic Auth
之前客戶端設(shè)置的認(rèn)證方式是CLIENT_SECRET_BASIC
,所以需將客戶端信息添加至請(qǐng)求頭
2. 添加表單數(shù)據(jù),發(fā)起POST請(qǐng)求
下列表單數(shù)據(jù)可添加至form-data也可添加至url params
參數(shù)中的code就是第6步回調(diào)時(shí)攜帶的code注意:添加url params時(shí)redirect_uri參數(shù)要經(jīng)過(guò)encodeURIComponent函數(shù)對(duì)回調(diào)地址進(jìn)行編碼
在新版本的sas(1.2.1)中獲取token更新了授權(quán)碼校驗(yàn)邏輯,只能用form-data傳遞參數(shù),使用url-params會(huì)失敗,原因見(jiàn)issue1451
對(duì)應(yīng)的 commit 在這里: Fix to ensure endpoints distinguish between form and query parameters文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-723709.html
8. 參數(shù)解釋
1. client_id: 客戶端的id
2. client_secret: 客戶端秘鑰
3. redirect_uri:申請(qǐng)授權(quán)成功后的回調(diào)地址
4. response_type:授權(quán)碼模式固定參數(shù)code
5. code_verifier:一段隨機(jī)字符串
6. code_challenge:根據(jù)指定的加密方式將code_verifier加密后得到的字符串
7. code_challenge_method:加密方式
8. scope:客戶端申請(qǐng)的授權(quán)范圍
9. state:跟隨authCode原樣返回,防止CSRF攻擊
10. grant_type:指定獲取token 的方式:
1. refresh_token:刷新token
2. authorization_code:根據(jù)授權(quán)碼模式的授權(quán)碼獲取
3. client_credentials:客戶端模式獲取
總結(jié)
本篇文章從0到1搭建了一個(gè)簡(jiǎn)單認(rèn)證服務(wù),解釋了認(rèn)證服務(wù)的各項(xiàng)配置用意,如何設(shè)置自己的登錄頁(yè)和授權(quán)確認(rèn)頁(yè),如何讓認(rèn)證服務(wù)解析請(qǐng)求時(shí)攜帶的token,文章過(guò)長(zhǎng)難免有遺漏的地方,如果文章中有遺漏或錯(cuò)誤的地方請(qǐng)各位讀者在評(píng)論區(qū)指出。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-723709.html
到了這里,關(guān)于Spring Authorization Server入門 (二) Spring Boot整合Spring Authorization Server的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!