title: OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client
date: 2023-03-27 01:41:26
tags:
- OAuth2.0
- Spring Authorization Server
categories: - 開發(fā)實踐
cover: https://cover.png
feature: false
1. 授權服務器
目前 Spring 生態(tài)中的 OAuth2 授權服務器是 Spring Authorization Server,原先的 Spring Security OAuth 已經停止更新
1.1 引入依賴
這里的 spring-security-oauth2-authorization-server
用的是 0.4.0 版本,適配 JDK 1.8,Spring Boot 版本為 2.7.7
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
1.2 配置類
可以參考官方的 Samples:spring-authorization-server/samples
1.2.1 最小配置
官網最小配置 Demo 地址:Getting Started
官網最小配置如下,通過添加該配置類,啟動項目,這就能夠完成 OAuth2 的授權
@Configuration
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@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);
}
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);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
在上面的 Demo 里,將所有配置都寫在了一個配置類 SecurityConfig 里,實際上 Spring Authorization Server 還提供了一種實現最小配置的默認配置形式,就是通過 OAuth2AuthorizationServerConfiguration
這個類,源碼如下:
@Configuration(proxyBeanMethods = false)
public class OAuth2AuthorizationServerConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
applyDefaultSecurity(http);
return http.build();
}
// @formatter:off
public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer();
RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
}
// @formatter:on
public static JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
jwsAlgs.addAll(JWSAlgorithm.Family.EC);
jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
JWSKeySelector<SecurityContext> jwsKeySelector =
new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {
RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();
postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build());
return postProcessor;
}
}
這里注入一個叫做 authorizationServerSecurityFilterChain
的 bean,其實對比一下可以看出,這和最小配置的實現基本是相同的。有了這個 bean,就會支持如下協(xié)議端點:
- OAuth2 Authorization endpoint
- OAuth2 Token endpoint
- OAuth2 Token Introspection endpoint
- OAuth2 Token Revocation endpoint
- OAuth2 Authorization Server Metadata endpoint
- JWK Set endpoint
- OpenID Connect 1.0 Provider Configuration endpoint
- OpenID Connect 1.0 UserInfo endpoint
接下來使用 OAuth2AuthorizationServerConfiguration
這個類來實現一個 Authorization Server,將 Spring Security 和 Authorization Server 的配置分開,Spring Security 使用 SecurityConfig
類,創(chuàng)建一個新的Authorization Server 配置類 AuthorizationServerConfig
1.2.2 ServerSecurityConfig
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ServerSecurityConfig {
@Resource
private DataSource dataSource;
/**
* Spring Security 的過濾器鏈,用于 Spring Security 的身份認證
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 配置放行的請求
.antMatchers("/api/**", "/login").permitAll()
// 其他任何請求都需要認證
.anyRequest().authenticated()
)
// 設置登錄表單頁面
.formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login"));
return http.build();
}
// @Bean
// public UserDetailsService userDetailsService() {
// return new JdbcUserDetailsManager(dataSource);
// }
@Bean
UserDetailsManager userDetailsManager() {
return new JdbcUserDetailsManager(dataSource);
}
}
Spring Authorization Server 默認是支持內存和 JDBC 兩種存儲模式的,內存模式只適合簡單的測試,所以這里使用 JDBC 存儲模式。在 1.2.1 最小配置那節(jié)里注入 UserDetailsService
這個 Bean 使用的是 InMemoryUserDetailsManager
,表示內存模式,這里使用 JdbcUserDetailsManager
表示 JDBC 模式
而這兩個類都屬于 UserDetailsManager
接口的實現類,并且后續(xù)我們需要使用到 userDetailsManager.createUser(userDetails)
方法來添加用戶,因此這里需要注入 UserDetailsManager
這個 Bean,由于返回的都是 JdbcUserDetailsManager,因此可以注釋掉 UserDetailsService
這個 Bean 的注入
1.2.3 AuthorizationServerConfig
該類部分配置可以參照前面提到的 OAuth2AuthorizationServerConfiguration
類來配置,同樣使用 JDBC 存儲模式
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 定義授權服務配置器
OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
configurer
// 自定義授權頁面
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
// Enable OpenID Connect 1.0, 啟用 OIDC 1.0
.oidc(Customizer.withDefaults());
// 獲取授權服務器相關的請求端點
RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
http
// 攔截對授權服務器相關端點的請求
.requestMatcher(endpointsMatcher)
// 攔載到的請求需要認證
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
// 忽略掉相關端點的 CSRF(跨站請求): 對授權端點的訪問可以是跨站的
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// 訪問端點時表單登錄
.formLogin()
.and()
// 應用授權服務器的配置
.apply(configurer);
return http.build();
}
/**
* 注冊客戶端應用, 對應 oauth2_registered_client 表
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
* 令牌的發(fā)放記錄, 對應 oauth2_authorization 表
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 把資源擁有者授權確認操作保存到數據庫, 對應 oauth2_authorization_consent 表
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 加載 JWT 資源, 用于生成令牌
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* JWT 解碼
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* AuthorizationServerS 的相關配置
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
1.3 創(chuàng)建數據庫表
一共包括 5 個表,其中 Spring Security 相關的有 2 個表,user 和 authorities,用戶表和權限表,該表的建表 SQL 在
org\springframework\security\core\userdetails\jdbc\users.ddl
SQL 可能會有一些問題,根據自己使用的數據庫進行更改
create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
Spring authorization Server 有 3 個表,建表 SQL 在:
org\springframework\security\oauth2\server\authorization\oauth2-authorization-consent-schema.sql
org\springframework\security\oauth2\server\authorization\oauth2-authorization-schema.sql
org\springframework\security\oauth2\server\authorization\client\oauth2-registered-client-schema.sql
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)
);
/*
IMPORTANT:
If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata blob DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE oauth2_registered_client (
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at timestamp DEFAULT NULL,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
);
創(chuàng)建完成后的數據庫表如下:
1.4 自定義登錄和授權頁面
在項目 resource 目錄下創(chuàng)建一個 templates 文件夾,然后創(chuàng)建 login.html 和 consent.html,登錄頁面的配置在 1.2.2 中配置好了,授權頁面的配置在 1.2.3 中配置好了
登錄頁面 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, shrink-to-fit=no">
<title>Spring Security Example</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" th:action="@{/login}">
<div th:if="${param.error}" class="alert alert-danger" role="alert">
用戶名或密碼無效
</div>
<div th:if="${param.logout}" class="alert alert-success" role="alert">
您已注銷
</div>
<h2 class="form-signin-heading">登錄</h2>
<p>
<label for="username" class="sr-only">用戶名</label>
<input type="text" id="username" name="username" class="form-control" placeholder="用戶名" required autofocus>
</p>
<p>
<label for="password" class="sr-only">密 碼</label>
<input type="password" id="password" name="password" class="form-control" placeholder="密 碼" required>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">登錄</button>
<a class="btn btn-light btn-block bg-white" href="/oauth2/authorization/github-idp" role="link" style="text-transform: none;">
<img width="24" style="margin-right: 5px;" alt="Sign in with GitHub" src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" />
使用Github登錄
</a>
</form>
</div>
</body>
</html>
創(chuàng)建 LoginConroller,用于跳轉到 login.html 頁面
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
授權頁面 consent.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Custom consent page - Consent required</title>
<style>
body {
background-color: aliceblue;
}
</style>
<script>
function cancelConsent() {
document.consent_form.reset();
document.consent_form.submit();
}
</script>
</head>
<body>
<div class="container">
<div class="py-5">
<h1 class="text-center text-primary">應用程序權限</h1>
</div>
<div class="row">
<div class="col text-center">
<p>
應用程序
<span class="font-weight-bold text-primary" th:text="${clientId}"></span>
想要訪問您的帳戶
<span class="font-weight-bold" th:text="${principalName}"></span>
</p>
</div>
</div>
<div class="row pb-3">
<div class="col text-center"><p>上述應用程序請求以下權限<br>如果您批準,請查看這些并同意</p></div>
</div>
<div class="row">
<div class="col text-center">
<form name="consent_form" method="post" th:action="@{/oauth2/authorize}">
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<div th:each="scope: ${scopes}" class="form-group form-check py-1">
<input class="form-check-input"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<label class="form-check-label font-weight-bold" 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)}">您已向上述應用授予以下權限:</p>
<div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
<input class="form-check-input"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<div class="form-group pt-3">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
提交授權
</button>
</div>
<div class="form-group">
<button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
取消
</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>
創(chuàng)建 AuthorizationConsentController,用于跳轉到 consent.html 頁面
@Controller
public class AuthorizationConsentController {
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationConsentService authorizationConsentService;
public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationConsentService authorizationConsentService) {
this.registeredClientRepository = registeredClientRepository;
this.authorizationConsentService = authorizationConsentService;
}
@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) {
// 要批準的范圍和以前批準的范圍
Set<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
// 查詢 clientId 是否存在
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
// 查詢當前的授權許可
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;
}
// 如果已授權范圍包含了請求范圍,則添加到以前批準的范圍的 Set, 否則添加到要批準的范圍
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());
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;
}
public static class ScopeWithDescription {
private static final String DEFAULT_DESCRIPTION = "未知范圍 - 我們無法提供有關此權限的信息, 請在授予此權限時謹慎";
private static final Map<String, String> scopeDescriptions = new HashMap<>();
static {
scopeDescriptions.put(
OidcScopes.PROFILE,
"此應用程序將能夠讀取您的個人資料信息"
);
scopeDescriptions.put(
"message.read",
"此應用程序將能夠讀取您的信息"
);
scopeDescriptions.put(
"message.write",
"此應用程序將能夠添加新信息, 它還可以編輯和刪除現有信息"
);
scopeDescriptions.put(
"other.scope",
"這是范圍描述的另一個范圍示例"
);
}
public final String scope;
public final String description;
ScopeWithDescription(String scope) {
this.scope = scope;
this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
}
}
}
1.5 ServerController
用于添加用戶信息和客戶端信息,這里的 passwordEncoder 使用 BCryptPasswordEncoder 進行加解密,{bcrypt} 表示加密,{noop} 表示明文
@RestController
public class ServerController {
@Resource
private UserDetailsManager userDetailsManager;
@GetMapping("/api/addUser")
public String addUser() {
UserDetails userDetails = User.builder().passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s))
.username("fan")
.password("fan")
.roles("ADMIN")
.build();
userDetailsManager.createUser(userDetails);
return "添加用戶成功";
}
@Resource
private RegisteredClientRepository registeredClientRepository;
@GetMapping("/api/addClient")
public String addClient() {
// JWT(Json Web Token)的配置項:TTL、是否復用refreshToken等等
TokenSettings tokenSettings = TokenSettings.builder()
// 令牌存活時間:2小時
.accessTokenTimeToLive(Duration.ofHours(2))
// 令牌可以刷新,重新獲取
.reuseRefreshTokens(true)
// 刷新時間:30天(30天內當令牌過期時,可以用刷新令牌重新申請新令牌,不需要再認證)
.refreshTokenTimeToLive(Duration.ofDays(30))
.build();
// 客戶端相關配置
ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用戶授權確認
.requireAuthorizationConsent(true)
.build();
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客戶端ID和密碼
.clientId("messaging-client")
// .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))
.clientSecret("{noop}secret")
// 授權方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授權模式(授權碼模式)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新令牌(授權碼模式)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 回調地址:授權服務器向當前客戶端響應時調用下面地址, 不在此列的地址將被拒絕, 只能使用IP或域名,不能使用 localhost
.redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")
// OIDC 支持
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 授權范圍(當前客戶端的授權范圍)
.scope("message.read")
.scope("message.write")
// JWT(Json Web Token)配置項
.tokenSettings(tokenSettings)
// 客戶端配置項
.clientSettings(clientSettings)
.build();
registeredClientRepository.save(registeredClient);
return "添加客戶端信息成功";
}
}
1.6 YAML 配置
配置數據庫連接信息
server:
port: 9000
spring:
datasource:
url: jdbc:mysql://localhost:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
1.7 測試
完整目錄結構如下:
1.7.1 添加用戶和客戶端信息
啟動項目,訪問 http://127.0.0.1:9000/api/addUser
查詢數據庫 users 和 authorities 表,已有用戶和權限信息
訪問 http://127.0.0.1:9000/api/addClient
查詢數據庫 oauth2_registered_client 表,已有客戶端信息
1.7.2 授權碼模式獲取令牌
有關 OAuth2.0 的相關知識可見:OAuth2.0 實戰(zhàn)總結_凡 223 的博客
訪問 http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc
,這里的 127.0.0.1:8000 其實為客戶端地址,后面講到客戶端時,客戶端的地址就為 8000
- response_type:授權類型,code 為授權碼模式
- client_id:客戶端 ID,即前面注冊客戶端的時候定義的
- scope:請求的權限范圍
- redirect_uri:回調地址,也是前面注冊客戶端的時候定義的
未登錄,會跳轉到登錄頁面
輸入前面添加的用戶信息,用戶名和密碼,然后會跳轉到授權頁面
選擇是否授予權限,這里勾選后,點擊提交,會跳轉到回調地址,即 127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,由于這個地址還沒有對應的服務,無法訪問,但我們暫時需要的是地址欄的 code
http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=z_3O1lEdxVsd2fn8_uKA481pO9caGd0N4x_Vbt0deuMA77sDis6fhMJkf2_9uM4KGYzLzv7ujbXZ2JAdg0ACyMapR38jnJruG2iz2XBgptKrru-IJobGVa6NTicgvCZ7
打開接口測試工具,這里我使用的是 Apifox,使用表單格式,包含三個參數
- grant_type:授權類型,authorization_code 表示授權碼模式
- code:即授權碼,上面地址欄里返回給我們的 code 部分,復制到這里,code 使用一次就會失效
- redirect_uri:回調地址,與前面的一致。圖中的地址忘記修改了,注意和前面請求 code 時寫的回調地址一致,即 http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,后面有類似問題同樣修改
然后設置 Auth,Postman 里是 Authorization,選擇 Basic Auth 類型,用戶名密碼則為注冊客戶端時的 client_id 和 clientSecret,客戶端 ID 和密鑰
保存,發(fā)送后,會給我們返回 access_token 和 refresh_token
將 access_token 復制到 JSON Web Tokens - jwt.io 網站,解析后可以看到 JWT 的信息,包括客戶端 ID,權限范圍,服務器地址等
1.7.3 授權碼模式刷新令牌
在前面返回了 access_token 和 refresh_token,access_token 包含了授權信息,refresh_token 則是用來重新獲取 access_token,同樣是表單類型,包含兩個參數
- grant_type:refresh_token 表示刷新令牌
- refresh_token:即前面獲取到的 refresh_token 的值
Auth 信息與前面一致
保存,發(fā)送后,會給我們返回新的 access_token 和 refresh_token,refresh_token 使用一次就會失效
1.7.4 客戶端模式
同樣使用表單格式,grant_type 值為 client_credentials
Auth 與前面一致
保存,發(fā)送后,會給我們返回 access_token,沒有 refresh_token。因為在授權碼模式中的 access_token 是我們通過授權碼 code 換來的,而授權碼 code 是我們請求后授權得到的,為了不用每次獲取 access_token 都需要重新請求授權,所以使用 refresh_token 來重新獲取 access_token,refresh_token 和 access_token 都有過期時間,refresh_token 過期時間比 access_token 長
而客戶端模式可以直接獲取 access_token,所以也就不需要 refresh_token 了
1.7.5 OIDC
有關 OIDC 的相關知識同樣可見:OAuth2.0 實戰(zhàn)總結_凡 223 的博客
在前面 1.2.3 的配置和 1.5 的注冊客戶端時,已經支持了 OIDC,這里直接訪問:http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc
這里的 scope 必須包含 openid
得到授權碼 code
http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=NjvT1z3msYRsjvPPM4LP4EmlyBUixsKes_J6osSB3VAugXEKmyUappvtrmTWp7s_iQzoJsD8xOE3gUXawhMixL0fu2HC6UJv8CeZyCB-d2oiu4NnCO9uJcK1MXOm4poU
然后通過授權碼 code 換取令牌,可以看到除了 access_token 和 refresh_token 外,還返回了一個 id_token
解析這個 id_token,信息如下,是我們的身份認證信息
再通過 refresh_token 重新獲取令牌,同樣也給我們返回了 id_token
通過 access_token,獲取 OIDC 的用戶端點
這里的 sub 就是用戶的標志。在 1.2.3 的配置中,對于 OIDC 使用的是默認配置
我們也可以增加自定義信息,修改后的配置如下,其他配置不變
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 定義授權服務配置器
OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
configurer
// 自定義授權頁面
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
// Enable OpenID Connect 1.0, 啟用 OIDC 1.0
.oidc(oidcConfigurer -> oidcConfigurer.userInfoEndpoint(userInfoEndpointConfigurer ->
userInfoEndpointConfigurer.userInfoMapper(userInfoAuthenticationContext -> {
OAuth2AccessToken accessToken = userInfoAuthenticationContext.getAccessToken();
Map<String, Object> claims = MapUtil.map(false);
claims.put("url", "http://127.0.0.1:9000");
claims.put("accessToken", accessToken);
claims.put("sub", userInfoAuthenticationContext.getAuthorization().getPrincipalName());
return new OidcUserInfo(claims);
})));
// 獲取授權服務器相關的請求端點
RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
http
// 攔截對授權服務器相關端點的請求
.requestMatcher(endpointsMatcher)
// 攔載到的請求需要認證
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
// 忽略掉相關端點的 CSRF(跨站請求): 對授權端點的訪問可以是跨站的
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// 訪問端點時表單登錄
.formLogin()
.and()
// 應用授權服務器的配置
.apply(configurer);
return http.build();
}
// ... 其他配置不變
}
重啟項目,重新獲取到 access_token,通過 access_token 訪問用戶端點,可以看到我們自定義的信息已經被添加了進來
2. 資源服務器
2.1 引入依賴
<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.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
2.2 YAML 配置
server:
port: 8001
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000
2.3 異常處理器
該部分為 Spring Security 相關知識,可見:Spring Security 總結_凡 223 的博客
2.3.1 認證失敗處理器
Response 為自定義的統(tǒng)一結果返回類,這里的返回信息自定義即可
public class UnAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 403, 未授權, 禁止訪問
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 返回響應信息
ServletOutputStream outputStream = response.getOutputStream();
Response fail = Response.fail(HttpServletResponse.SC_FORBIDDEN,
"UnAccessDeniedHandler-未授權, 不允許訪問", "uri-" + request.getRequestURI());
outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));
// 關閉流
outputStream.flush();
outputStream.close();
}
}
2.3.2 鑒權失敗處理器
public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if (authException instanceof InvalidBearerTokenException) {
LogUtil.info("Token 登錄失效");
}
if (response.isCommitted()) {
return;
}
// 401, 未認證
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_ACCEPTED);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 返回響應信息
ServletOutputStream outputStream = response.getOutputStream();
Response fail = Response.fail(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage() + "-UnAuthenticationEntryPoint-認證失敗", "uri-" + request.getRequestURI());
outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));
// 關閉流
outputStream.flush();
outputStream.close();
}
}
2.4 配置類
對資源請求配置了讀、寫、profile 權限
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ResourceServerConfig {
/**
* 資源管理器配置
*
* @param http
* @return {@link SecurityFilterChain}
* @author Fan
* @since 2023/2/2 9:30
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
UnAuthenticationEntryPoint authenticationEntryPoint = new UnAuthenticationEntryPoint();
UnAccessDeniedHandler accessDeniedHandler = new UnAccessDeniedHandler();
http
// security的session生成策略改為security不主動創(chuàng)建session, 即STALELESS
// 資源服務不涉及用戶登錄, 僅靠token訪問, 不需要seesion
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authorize -> authorize
// 對 /resource1 的請求,需要 SCOPE_message.read 權限
.antMatchers("/resource1").hasAuthority("SCOPE_message.read")
// 對 /resource2 的請求,需要 SCOPE_message.write 權限
.antMatchers("/resource2").hasAuthority("SCOPE_message.write")
// 對 /resource3 的請求,需要 SCOPE_profile 權限
.antMatchers("/resource3").hasAuthority("SCOPE_profile")
// 放行請求
.antMatchers("/api/**").permitAll()
// 其他任何請求都需要認證
.anyRequest().authenticated())
// 異常處理器
.exceptionHandling(exceptionConfigurer -> exceptionConfigurer
// 認證失敗
.authenticationEntryPoint(authenticationEntryPoint)
// 鑒權失敗
.accessDeniedHandler(accessDeniedHandler)
)
// 資源服務
.oauth2ResourceServer(resourceServer -> resourceServer
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.jwt());
return http.build();
}
}
2.5 Controller
@RestController
public class MessagesController {
@GetMapping("/resource1")
public Response getResource1(){
return Response.success("服務A -> 資源1 -> 讀權限");
}
@GetMapping("/resource2")
public Response getResource2(){
return Response.success("服務A -> 資源2 -> 寫權限");
}
@GetMapping("/resource3")
public Response resource3(){
return Response.success("服務A -> 資源3 -> profile 權限");
}
@GetMapping("/api/publicResource")
public Response publicResource() {
return Response.success("服務A -> 公共資源");
}
}
2.6 測試
完整目錄結構如下:
啟動項目,打開 Apifox,直接請求時,會提示我們認證失敗,即上面認證失敗處理器的響應結果
添加 Auth,類型選擇 Bearer Token,Token 的值即為前面獲取到的 access_token 的值
保存,發(fā)送后,即可獲取資源 resource1
再獲取資源 resource2,提示沒有權限,這里返回的信息即為鑒權失敗處理器的響應信息。因為在我們申請權限的時候只申請了 message.read 權限,同時也只授權了 message.read 權限,而 resource2 需要 message.write 權限,因此鑒權失敗,無法訪問
3. 客戶端
3.1 引入依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
3.2 YAML 配置
server:
port: 8000
spring:
application:
name: messages-client
security:
oauth2:
client:
registration:
messaging-client-oidc:
provider: authorization-server
client-id: messaging-client
client-secret: secret
authorization-grant-type: authorization_code
# redirect-uri: "127.0.0.1:8000/login/oauth2/code/messaging-client-oidc"
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope: openid,message.read,message.write
client-name: messaging-client-oidc
provider:
# 服務提供地址
authorization-server:
# issuer-uri 可以簡化下面的配置
issuer-uri: http://localhost:9000
# 請求授權碼地址
# authorization-uri: http://localhost:9000/oauth2/authorize
# 請求令牌地址
# token-uri: http://localhost:9000/oauth2/token
# 用戶資源地址
# user-info-uri: http://localhost:9000/oauth2/user
# 用戶資源返回中的一個屬性名
# user-name-attribute: name
# user-info-authentication-method: GET
這里的配置要和注冊客戶端時的配置對應上,同一顏色對應,這里使用的是 OIDC,scope 加上了 openid
注意:使用 OIDC 是為了使用默認的用戶端點,假如不使用 OIDC 需要自定義用戶端點接口,否則會報如下錯誤
[invalid_user_info_response] An error occurred while attempting to retrieve the UserInfo Resource: 403 : “{“error”:“insufficient_scope”}”
3.3 配置類
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ClientSecurityConfig {
/**
* 安全配置
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize ->
// 任何請求都需要認證
authorize.anyRequest().authenticated()
)
// 登錄
// .oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
.oauth2Login(Customizer.withDefaults())
.oauth2Client(Customizer.withDefaults());
return http.build();
}
}
3.4 index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登錄用戶:<span th:text="${user}"></span>
<hr/>
<ul>
<li><a href="./server/a/resource1">服務A —— 資源1</a></li>
<li><a href="./server/a/resource2">服務A —— 資源2</a></li>
<li><a href="./server/a/resource3">服務A —— 資源3</a></li>
<li><a href="./server/a/publicResource">服務A —— 公共資源</a></li>
</ul>
</body>
</html>
創(chuàng)建 IndexController,跳轉到 index.html
@Controller
public class IndexController {
@GetMapping("/")
public String root() {
return "redirect:/index";
}
@GetMapping("/index")
public String index(Model model) {
Map<String, Object> map = MapUtil.map(false);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
map.put("name", auth.getName());
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
List<? extends GrantedAuthority> authoritiesList = authorities.stream().collect(Collectors.toList());
map.put("authorities", authoritiesList);
model.addAttribute("user", JSONUtil.toJsonStr(map));
return "index";
}
}
3.5 ResourceController
@RestController
public class ResourceController {
@GetMapping("/server/a/resource1")
public String getServerARes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return getServer("http://127.0.0.1:8001/resource1", oAuth2AuthorizedClient);
}
@GetMapping("/server/a/resource2")
public String getServerARes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return getServer("http://127.0.0.1:8001/resource2", oAuth2AuthorizedClient);
}
@GetMapping("/server/a/resource3")
public String getServerBRes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return getServer("http://127.0.0.1:8001/resource3", oAuth2AuthorizedClient);
}
@GetMapping("/server/a/publicResource")
public String getServerBRes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
return getServer("http://127.0.0.1:8001/api/publicResource", oAuth2AuthorizedClient);
}
/**
* 綁定token,請求微服務
*/
private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) {
LogUtil.info("getServer");
// 獲取 access_token
String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
// 發(fā)起請求
Mono<String> stringMono = WebClient.builder()
.defaultHeader("Authorization", "Bearer " + tokenValue)
.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(String.class);
return stringMono.block();
}
}
3.6 測試
完整目錄結構如下:
啟動項目,訪問 127.0.0.1:8000
,未登錄會直接跳轉到登錄頁面
輸入用戶名密碼,登錄后進入授權頁面
選擇想要授予的權限,這里勾選 read 權限,點擊提交,跳轉到我們的首頁 index.html
將上面 user 的 JSON 信息格式化一下如下,可以看到就是我們的認證和權限信息
點擊訪問 服務A -> 資源1
點擊訪問 服務A -> 資源2,無法訪問
這是因為之前授權時只給了 read 權限,而資源 2 需要 write 權限,可以看到報了 403 異常,這里可以定義一個異常處理類,來返回對應的信息,而不是白頁
我們關閉當前頁面新開一個頁面,再次訪問 127.0.0.1:8000
可以發(fā)現直接進入了 index.html,無需再次登錄
可以發(fā)現我們訪問時是帶了一個 JESSEIONID 的,用戶登錄后,會在認證服務器和客戶端都保存 session 信息文章來源:http://www.zghlxwxcb.cn/news/detail-475409.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-475409.html
到了這里,關于OAuth2.0 實踐 Spring Authorization Server 搭建授權服務器 + Resource + Client的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!