我已經試過了 教程很完美
Spring Security Oauth2
OAuth是一個關於授權的開放網絡標準,在全世界得到的廣泛的應用,目前是2.0的版本。OAuth2在“客戶端”與“服務提供商”之間,設置了一個授權層(authorization layer)。“客戶端”不能直接登錄“服務提供商”,只能登錄授權層,以此將用戶與客戶端分離。“客戶端”登錄需要獲取OAuth提供的令牌,否則將提示認證失敗而導致客戶端無法訪問服務。關於OAuth2這裏就不多作介紹了,網上資料詳盡。下面我們實現一個 整合 SpringBoot 、Spring Security OAuth2 來實現單點登錄功能的案例並對執行流程進行詳細的剖析。
案例實現
目錄
認證服務配置 AuthorizationServerConfigurerAdapter
安全配置 WebSecurityConfigurerAdapter
安全配置 WebSecurityConfigurerAdapter
項目介紹
這個單點登錄系統包括下面幾個模塊:
spring-oauth-parent : 父模塊,管理打包
spring-oauth-server : 認證服務端、資源服務端(端口:8881)
spring-oauth-client : 單點登錄客戶端示例(端口:8882)
spring-oauth-client2: 單點登錄客戶端示例(端口:8883)
當通過任意客戶端訪問資源服務器受保護的接口時,會跳轉到認證服務器的統一登錄界面,要求登錄,登錄之後,在登錄有效時間內任意客戶端都無需再登錄。
認證服務端 spring-oauth-server
添加依賴 pom.xml
主要是添加 spring-security-oauth2 依賴。
pom.xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-oauth-server</artifactId>
<name>spring-oauth-server</name>
<packaging>war</packaging>
<parent>
<groupId>com.louis</groupId>
<artifactId>spring-oauth-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${oauth.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
</project>
配置文件 application.yml
配置文件內容如下。
application.yml
server:
port: 8881
servlet:
context-path: /auth
啓動類
啓動類添加 @EnableResourceServer 註解,表示作爲資源服務器。
OAuthServerApplication.java
package com.louis.spring.oauth.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class OAuthServerApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(OAuthServerApplication.class, args);
}
}
認證服務配置 AuthorizationServerConfigurerAdapter
添加認證服務器配置,這裏採用內存方式獲取,其他方式獲取在這裏定製即可。
OAuthServerConfig.java
package com.louis.spring.oauth.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
@Configuration
@EnableAuthorizationServer
public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("SampleClientId") // clientId, 可以類比爲用戶名
.secret(passwordEncoder.encode("secret")) // secret, 可以類比爲密碼
.authorizedGrantTypes("authorization_code") // 授權類型,這裏選擇授權碼
.scopes("user_info") // 授權範圍
.autoApprove(false) // 不用自動認證 可以更清楚的觀察
.redirectUris("http://localhost:8882/login","http://localhost:8883/login") // 認證成功重定向URL
.accessTokenValiditySeconds(10); // 超時時間,10s
}
}
安全配置 WebSecurityConfigurerAdapter
Spring Security 安全配置。在安全配置類裏我們配置了:
1. 配置請求URL的訪問策略。
2. 自定義了同一認證登錄頁面URL。
3. 配置用戶名密碼信息從內存中創建並獲取。
SecurityConfig.java
package com.louis.spring.oauth.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login")
.antMatchers("/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll() // 自定義登錄頁面,這裏配置了 loginPage, 就會通過 LoginController 的 login 接口加載登錄頁面
.and().csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用戶名密碼,這裏採用內存方式,生產環境需要從數據庫獲取
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("123"))
.roles("USER");
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
自定義登錄接口提供 LoginController 及頁面
這裏提供了一個自定義的登錄接口,用於跳轉到自定義的同一認證登錄頁面。
LoginController.java
package com.louis.spring.oauth.server.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
/**
* 自定義登錄頁面
* @return
*/
@GetMapping("/login")
public String login() {
return "login";
}
}
登錄頁面放置在 resources/templates 下,需要在登錄時提交 post表單到 auth/login。
login.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://cdn.bootcss.com/vue/2.5.17/vue.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
</head>
<body>
<div class="login-box" id="app" >
<el-form action="/auth/login" method="post" label-position="left" label-width="0px" class="demo-ruleForm login-container">
<h2 class="title" >統一認證登錄平臺</h2>
<el-form-item>
<el-input type="text" name="username" v-model="username" auto-complete="off" placeholder="賬號"></el-input>
</el-form-item>
<el-form-item>
<el-input type="password" name="password" v-model="password" auto-complete="off" placeholder="密碼"></el-input>
</el-form-item>
<el-form-item style="width:100%; text-align:center;">
<el-button type="primary" style="width:47%;" @click.native.prevent="reset">重 置</el-button>
<el-button type="primary" style="width:47%;" native-type="submit" :loading="loading">登 錄</el-button>
</el-form-item>
<el-form>
</div>
</body>
<script type="text/javascript">
new Vue({
el : '#app',
data : {
loading: false,
username: 'admin',
password: '123'
},
methods : {
}
})
</script>
<style lang="scss" scoped>
.login-container {
-webkit-border-radius: 5px;
border-radius: 5px;
-moz-border-radius: 5px;
background-clip: padding-box;
margin: 100px auto;
width: 320px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.title {
margin: 0px auto 20px auto;
text-align: center;
color: #505458;
}
</style>
</html>
受保護的接口 UserController 要求登錄認證。
這裏提供了一個受保護的接口,用於獲取用戶信息,客戶端訪問這個接口的時候要求登錄認證。
UserController.java
package com.louis.spring.oauth.server.controller;
import java.security.Principal;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
/**
* 資源服務器提供的受保護接口
* @param principal
* @return
*/
@RequestMapping("/user")
public Principal user(Principal principal) {
System.out.println(principal);
return principal;
}
}
客戶端實現
添加依賴 pom.xml
主要添加 Spring Security 依賴,另外因爲 Spring Boot 2.0 之後代碼的合併, 需要添加 spring-security-oauth2-autoconfigure ,才能使用 @EnableOAuth2Sso 註解。
pom.xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-oauth-client</artifactId>
<name>spring-oauth-client</name>
<packaging>war</packaging>
<parent>
<groupId>com.louis</groupId>
<artifactId>spring-oauth-parent</artifactId>
<version>1.0.0-SNAPSHOT</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.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${oauth-auto.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
</dependencies>
</project>
啓動類
啓動類需要添加 RequestContextListener,用於監聽HTTP請求事件。
OAuthClientApplication.java
package com.louis.spring.oauth.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.request.RequestContextListener;
@SpringBootApplication
public class OAuthClientApplication extends SpringBootServletInitializer {
@Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
public static void main(String[] args) {
SpringApplication.run(OAuthClientApplication.class, args);
}
}
安全配置 WebSecurityConfigurerAdapter
添加安全配置類,添加 @EnableOAuth2Sso 註解支持單點登錄。
OAuthClientSecurityConfig.java
package com.louis.spring.oauth.client.config;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableOAuth2Sso
@Configuration
public class OAuthClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login**")
.permitAll()
.anyRequest()
.authenticated();
}
}
頁面配置
添加 Spring MVC 配置,主要是添加 index 和 securedPage 頁面對應的訪問配置。
OAuthClientWebConfig.java
package com.louis.spring.oauth.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
@EnableWebMvc
public class OAuthClientWebConfig implements WebMvcConfigurer {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Override
public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(final ViewControllerRegistry registry) {
registry.addViewController("/")
.setViewName("forward:/index");
registry.addViewController("/index");
registry.addViewController("/securedPage");
}
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/resources/");
}
}
配置文件 application.yml
主要配置 oauth2 認證相關的配置。
application.yml
auth-server: http://localhost:8881/auth
server:
port: 8882
servlet:
context-path: /
session:
cookie:
name: SESSION1
security:
basic:
enabled: false
oauth2:
client:
clientId: SampleClientId
clientSecret: secret
accessTokenUri: ${auth-server}/oauth/token
userAuthorizationUri: ${auth-server}/oauth/authorize
resource:
userInfoUri: ${auth-server}/user
spring:
thymeleaf:
cache: false
頁面文件 index securedPage
頁面文件只有兩個,
index 是首頁,無須登錄即可訪問,在首頁通過添加 login 按鈕訪問 securedPage 頁面,
securedPage 訪問資源服務器的 /user 接口獲取用戶信息。
/resources/templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring Security SSO</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
<div class="col-sm-12">
<h1>Spring Security SSO</h1>
<a class="btn btn-primary" href="securedPage">Login</a>
</div>
</div>
</body>
</html>
/resources/templates/securedPage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Spring Security SSO</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
<div class="col-sm-12">
<h1>Secured Page</h1>
Welcome, <span th:text="${#authentication.name}">Name</span>
</div>
</div>
</body>
</html>
spring-oauth-client2 內容跟 spring-oauth-client 基本一樣,除了端口爲 8883 外,securedPage 顯示的內容稍微有點不一樣用於區分。
測試效果
啓動認證服務端和客戶端。
訪問 http://localhost:8882/,返回結果如下。
點擊 login,跳轉到 securedPage 頁面,頁面調用資源服務器的受保護接口 /user ,會跳轉到認證服務器的登錄界面,要求進行登錄認證。
同理,訪問 http://localhost:8883/,返回結果如下。
點擊 login,同樣跳轉到認證服務器的登錄界面,要求進行登錄認證。
輸入用戶名密碼,默認是後臺配置的用戶信息,用戶名:admin, 密碼:123 ,點擊登錄。
從 http://localhost:8882/ 發出的請求登錄成功之後返回8882的安全保護頁面。
如果是從 http://localhost:8883/ 發出的登錄請求,則會跳轉到8883的安全保護頁面。
從 8882 發出登錄請求,登錄成功之後,訪問 http://localhost:8883/ ,點擊登錄。
結果不需要再進行登錄,直接跳轉到了 8883 的安全保護頁面,因爲在訪問 8882 的時候已經登錄過了。
同理,假如先訪問 8883 資源進行登錄之後,訪問 8882 也無需重複登錄,到此,單點登錄的案例實現就完成了。
執行流程剖析
接下來,針對上面的單點登錄案例,我們對整個體系的執行流程進行詳細的剖析。
在此之前,我們先描述一下OAuth2授權碼模式的整個大致流程。
1. 瀏覽器向UI服務器點擊觸發要求安全認證
2. 跳轉到授權服務器獲取授權許可碼
3. 從授權服務器帶授權許可碼跳回來
4. UI服務器向授權服務器獲取AccessToken
5. 返回AccessToken到UI服務器
6. 發出/resource請求到UI服務器
7. UI服務器將/resource請求轉發到Resource服務器
8. Resource服務器要求安全驗證,於是直接從授權服務器獲取認證授權信息進行判斷後(最後會響應給UI服務器,UI服務器再響應給瀏覽中器)
結合我們的案例,首先,我們通過 http://localhost:8882/,訪問 8882 的首頁,8883 同理。
然後點擊 Login,重定向到了 http://localhost:8882/securedPage,而 securedPage 是受保護的頁面。所以就重定向到了 8882 的登錄URL: http://localhost:8882/login, 要求首先進行登錄認證。
因爲客戶端配置了單點登錄(@EnableOAuth2Sso),所以單點登錄攔截器會讀取授權服務器的配置,發起形如: http://localhost:8881/auth/oauth/authorize?client_id=SampleClientId&redirect_uri=http://localhost:8882/ui/login&response_type=code&state=xtDCY2 的授權請求獲取授權碼。
然後因爲上面訪問的是認證服務器的資源,所以又重定向到了認證服務器的登錄URL: http://localhost:8881/auth/login,也就是我們自定義的統一認證登錄平臺頁面,要求先進行登錄認證,然後才能繼續發送獲取授權碼的請求。
我們輸入用戶名和密碼,點擊登錄按鈕進行登錄認證。
登錄認證的大致流程如下:
AbstractAuthenticationProcessingFilter.doFilter()
默認的登錄過濾器 UsernamePasswordAuthenticationFilter 攔截到登錄請求,調用父類的 doFilter 的方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
...
successfulAuthentication(request, response, chain, authResult);
}
UsernamePasswordAuthenticationFilter.attemptAuthentication()
doFilter 方法調用 UsernamePasswordAuthenticationFilter 自身的 attemptAuthentication 方法進行登錄認證。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
...
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
ProviderManager.authenticate()
attemptAuthentication 繼續調用認證管理器 ProviderManager 的 authenticate 方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
}
}
AbstractUserDetailsAuthenticationProvider.authenticate()
而 ProviderManager 又是通過一組 AuthenticationProvider 來完成登錄認證的,其中的默認實現是 DaoAuthenticationProvider,繼承自 AbstractUserDetailsAuthenticationProvider, 所以 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法被調用。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
...
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
...return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider.retrieveUser()
AbstractUserDetailsAuthenticationProvider 的 authenticate 在認證過程中又調用 DaoAuthenticationProvider 的 retrieveUser 方法獲取登錄認證所需的用戶信息。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);return loadedUser;
}
...
}
UserDetailsManager.loadUserByUsername()
DaoAuthenticationProvider 的 retrieveUser 方法 通過 UserDetailsService 來進一步獲取登錄認證所需的用戶信息。UserDetailsManager 接口繼承了 UserDetailsService 接口,框架默認提供了 InMemoryUserDetailsManager 和 JdbcUserDetailsManager 兩種用戶信息的獲取方式,當然 InMemoryUserDetailsManager 主要用於非正式環境,正式環境大多都是採用 JdbcUserDetailsManager,從數據庫獲取用戶信息,當然你也可以根據需要擴展其他的獲取方式。
DaoAuthenticationProvider 的大致實現:
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
List<UserDetails> users = loadUsersByUsername(username);
UserDetails user = users.get(0); // contains no GrantedAuthority[]
Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
...
List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
addCustomAuthorities(user.getUsername(), dbAuths);return createUserDetails(username, user, dbAuths);
}
InMemoryUserDetailsManager 的大致實現:
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
DaoAuthenticationProvider.additionalAuthenticationChecks()
獲取到用戶認證所需的信息之後,認證器會進行一些檢查譬如 preAuthenticationChecks 進行賬號狀態之類的前置檢查,然後調用 DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法驗證密碼合法性。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
...
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
...
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication()
登錄認證成功之後, AbstractUserDetailsAuthenticationProvider 的 createSuccessAuthentication 方法被調用, 返回一個 UsernamePasswordAuthenticationToken 對象。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
...
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
...
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractAuthenticationProcessingFilter.successfulAuthentication()
認證成功之後,繼續回到 AbstractAuthenticationProcessingFilter,執行 successfulAuthentication 方法,存放認證信息到上下文,最終決定登錄認證成功之後的操作。
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 將登錄認證信息放置到上下文,在授權階段從上下文獲取
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess()
登錄成功之後,調用 SavedRequestAwareAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法,最後根據配置再次發送授權請求 :
http://localhost:8881/auth/oauth/authorize?client_id=SampleClientId&redirect_uri=http://localhost:8882/login&response_type=code&state=xtDCY2
AuthorizationEndpoint.authorize()
根據路徑匹配 /oauth/authorize,AuthorizationEndpoint 的 authorize 接口被調用。
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();try {
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Store authorizationRequest AND an immutable Map of authorizationRequest in session
// which will be used to validate against in approveOrDeny()
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
}
DefaultOAuth2RequestFactory.createAuthorizationRequest()
DefaultOAuth2RequestFactory 的 createAuthorizationRequest 方法被調用,用來創建 AuthorizationRequest。
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
// 構造 AuthorizationRequest
String clientId = authorizationParameters.get(OAuth2Utils.CLIENT_ID);
String state = authorizationParameters.get(OAuth2Utils.STATE);
String redirectUri = authorizationParameters.get(OAuth2Utils.REDIRECT_URI);
Set<String> responseTypes = OAuth2Utils.parseParameterList(authorizationParameters.get(OAuth2Utils.RESPONSE_TYPE));
Set<String> scopes = extractScopes(authorizationParameters, clientId);
AuthorizationRequest request = new AuthorizationRequest(authorizationParameters,
Collections.<String, String> emptyMap(), clientId, scopes, null, null, false, state, redirectUri, responseTypes);
// 通過 ClientDetailsService 加載 ClientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
request.setResourceIdsAndAuthoritiesFromClientDetails(clientDetails);
return request;
}
ClientDetailsService.loadClientByClientId()
ClientDetailsService 的 loadClientByClientId 方法被調用,框架提供了 ClientDetailsService 的兩種實現 InMemoryClientDetailsService 和 JdbcClientDetailsService,分別對應從內存獲取和從數據庫獲取,當然你也可以根據需要定製其他獲取方式。
JdbcClientDetailsService 的大致實現,主要是通過 JdbcTemplate 獲取,需要設置一個 datasource。
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails details;
try {
details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
}
catch (EmptyResultDataAccessException e) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
InMemoryClientDetailsService 的大致實現,主要是從內存Store裏面取出信息。
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
ClientDetails details = clientDetailsStore.get(clientId);
if (details == null) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
AuthorizationEndpoint.authorize()
繼續回到 AuthorizationEndpoint 的 authorize 方法
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();try {
// 創建ClientDtails
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// 設置跳轉URL
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
authorizationRequest.setRedirectUri(resolvedRedirect);
// 驗證授權範圍
oauth2RequestValidator.validateScope(authorizationRequest, client);
// 檢查是否是自動完成授權還是轉到授權頁面讓用戶手動確認
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
// 如果是授權碼模式,且爲自動授權或已完成授權,直接返回授權結果
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal));
}
}
// Store authorizationRequest AND an immutable Map of authorizationRequest in session
// which will be used to validate against in approveOrDeny()
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
}
如果是需要手動授權,轉到授權頁面URL: /oauth/confirm_access 。
private ModelAndView getUserApprovalPageResponse(Map<String, Object> model,
AuthorizationRequest authorizationRequest, Authentication principal) {
if (logger.isDebugEnabled()) {
logger.debug("Loading user approval page: " + userApprovalPage);
}
model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
// 轉到授權頁面, URL /oauth/confirm_access
return new ModelAndView(userApprovalPage, model);
}
用戶手動授權頁面
AuthorizationEndpoint.approveOrDeny()
AuthorizationEndpoint 中 POST 請求的接口 /oauth/authorize 對應的 approveOrDeny 方法被調用 。
@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get(AUTHORIZATION_REQUEST_ATTR_NAME);
try {
Set<String> responseTypes = authorizationRequest.getResponseTypes();
authorizationRequest.setApprovalParameters(approvalParameters);
authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest, (Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
if (!authorizationRequest.isApproved()) {
// 用戶不許授權,拒絕訪問
return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
false, true, false);
}
// 用戶授權完成,跳轉到客戶端設定的重定向URL
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
}
}
用戶授權完成,跳轉到客戶端設定的重定向URL。
BasicAuthenticationFilter.doFilterInternal()
轉到客戶端重定向URL之後,BasicAuthenticationFilter 攔截到請求, doFilterInternal 方法被調用,攜帶信息在客戶端執行登錄認證。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
try {
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, tokens[1]);
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
chain.doFilter(request, response);
}
如上面代碼顯示,doFilterInternal 方法中客戶端登錄認證邏輯也走了一遍,詳細過程跟上面授權服務端的認證過程一般無二,這裏就不貼重複代碼,大致流程如下鏈接流所示:
ProviderManager.authenticate() -- > AbstractUserDetailsAuthenticationProvider.authenticate() --> DaoAuthenticationProvider.retrieveUser() --> ClientDetailsUserDetailsService.loadUserByUsername() --> AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication()
TokenEndpoint.postAccessToken()
認證成功之後,客戶端獲取了權限憑證,返回客戶端URL,被 OAuth2ClientAuthenticationProcessingFilter 攔截,然後攜帶授權憑證向授權服務器發起形如: http://localhost:8881/auth/oauth/token 的 Post 請求換取訪問 token,對應的是授權服務器的 TokenEndpoint 類的 postAccessToken 方法。
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 獲取之前的請求信息,並對token獲取請求信息進行校驗
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
...
// 生成 token 並返回給客戶端,客戶端就可攜帶此 token 向資源服務器獲取信息了
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);return getResponse(token);
}
TokenGranter.grant()
令牌的生成通過 TokenGranter 的 grant 方法來完成。根據授權方式的類型,分別有對應的 TokenGranter 實現,如我們使用的授權碼模式,對應的是 AuthorizationCodeTokenGranter。
AbstractTokenGranter.grant()
AuthorizationCodeTokenGranter 的父類 AbstractTokenGranter 的 grant 方法被調用。
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
DefaultTokenServices.createAccessToken()
DefaultTokenServices 的 createAccessToken 被調用,用來生成 token。
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 先從 Store 獲取,Sotre 類型有 InMemoryTokenStore、JdbcTokenStore、JwtTokenStore、RedisTokenStore 等
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// Only create a new refresh token if there wasn't an existing one associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in the case that the old access token expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
客戶端攜帶Token訪問資源
token 被生成後返回給了客戶端,客戶端攜帶此 token 發起形如: http://localhost:8881/auth/user 的請求獲取用戶信息。
OAuth2AuthenticationProcessingFilter 過濾器攔截請求,然後調用 OAuth2AuthenticationManager 的 authenticate 方法執行登錄流程。
OAuth2AuthenticationProcessingFilter.doFilter()
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
// 獲取並校驗 token 之後,然後攜帶 token 進行登錄
Authentication authentication = tokenExtractor.extract(request);
...
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
chain.doFilter(request, response);
}
OAuth2AuthenticationManager.authenticate()
OAuth2AuthenticationManager 的 authenticate 方法被調用,利用 token 執行登錄認證。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
認證成功之後,獲取目標接口數據,然後重定向了真正的訪問目標URL http://localhost:8882/securedPage,並信息獲取的數據信息。
訪問 http://localhost:8882/securedPage,返回結果如下:
訪問 http://localhost:8883/securedPage,返回結果如下:
另外,在客戶端訪問受保護的資源的時候,會被 OAuth2ClientAuthenticationProcessingFilter 過濾器攔截。
OAuth2ClientAuthenticationProcessingFilter 的主要作用是獲取 token 進行登錄認證。
此時可能會出現以下幾種情況:
1. 獲取不到之前保存的 token,或者 token 已經過期,此時會繼續判斷請求中是否攜帶從認證服務器獲取的授權碼。
2. 如果請求中也沒有認證服務器提供的授權碼,則會重定向到認證服務器的 /oauth/authorize,要求獲取授權碼。
3. 訪問認證服務器的授權請求URL /oauth/authorize 時,會重定向到認證服務器的統一認證登錄頁面,要求進行登錄。
4. 如果步驟2中,請求已經攜帶授權碼,則攜帶授權碼向認證服務器發起 /oauth/token 請求,申請分配訪問 token。
5. 使用之前保存的或者通過上面步驟重新獲取的 token 進行登錄認證,登錄成功返回一個 OAuth2Authentication 對象。
OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication()
訪問請求被過濾器 OAuth2ClientAuthenticationProcessingFilter 攔截,它繼承了 AbstractAuthenticationProcessingFilter,過濾器 AbstractAuthenticationProcessingFilter 的doFilter 方法被調用,其中OAuth2ClientAuthenticationProcessingFilter 的 attemptAuthentication 被調用進行登錄認證。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
OAuth2RestTemplate.getAccessToken()
OAuth2RestTemplate 的 getAccessToken 方法被調用,用來獲取訪問 token.
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
OAuth2AccessToken accessToken = context.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
try {
accessToken = acquireAccessToken(context);
}
catch (UserRedirectRequiredException e) {
...
}
}
return accessToken;
}
AuthorizationCodeAccessTokenProvider.obtainAccessToken()
接下來 AuthorizationCodeAccessTokenProvider 的 obtainAccessToken 方法被調用。
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
// 如果沒有攜帶權限憑證,則轉到授權URL,又因爲未登錄,所以轉到授權服務器登錄界面
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
// 繼續調用父類的方法獲取 token
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));
}
授權前流程
如果還沒有進行授權,就沒有攜帶權限憑證,則轉到授權URL,又因爲未登錄,所以轉到授權服務器登錄界面。
授權後流程
如果是授權成功之後,就可以使用攜帶的授權憑證換取訪問 token 了。
OAuth2AccessTokenSupport.retrieveToken()
AuthorizationCodeAccessTokenProvider 通過調用父類 OAuth2AccessTokenSupport 的 retrieveToken 方法進一步獲取。
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
try {
// Prepare headers and form before going into rest template call in case the URI is affected by the result
authenticationHandler.authenticateTokenRequest(resource, form, headers);
// Opportunity to customize form and headers
tokenRequestEnhancer.enhance(request, resource, form, headers);
final AccessTokenRequest copy = request;
final ResponseExtractor<OAuth2AccessToken> delegate = getResponseExtractor();
ResponseExtractor<OAuth2AccessToken> extractor = new ResponseExtractor<OAuth2AccessToken>() {
@Override
public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
if (response.getHeaders().containsKey("Set-Cookie")) {
copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
}
return delegate.extractData(response);
}
};
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
}
}
攜帶授權憑證訪問授權服務器的授權連接 http://localhost:8881/auth/oauth/token,以換取資源訪問 token,後續客戶端攜帶 token 訪問資源服務器。
TokenEndpoint.postAccessToken()
TokenEndpoint 中授權服務器的 token 獲取接口定義。
獲取到 token 返回給客戶端之後,客戶就可以使用 token 向資源服務器獲取資源了。
源碼下載
碼雲:https://gitee.com/liuge1988/spring-boot-demo.git
原作者:朝雨憶輕塵
原出處:https://www.cnblogs.com/xifengxiaoma/
版權所有,歡迎轉載,轉載請註明原文作者及出處。