Spring Security Oauth2 單點登錄案例實現和執行流程剖析

我已經試過了 教程很完美

 

 

              

Spring Security Oauth2

OAuth是一個關於授權的開放網絡標準,在全世界得到的廣泛的應用,目前是2.0的版本。OAuth2在“客戶端”與“服務提供商”之間,設置了一個授權層(authorization layer)。“客戶端”不能直接登錄“服務提供商”,只能登錄授權層,以此將用戶與客戶端分離。“客戶端”登錄需要獲取OAuth提供的令牌,否則將提示認證失敗而導致客戶端無法訪問服務。關於OAuth2這裏就不多作介紹了,網上資料詳盡。下面我們實現一個 整合 SpringBoot 、Spring Security OAuth2 來實現單點登錄功能的案例並對執行流程進行詳細的剖析。

案例實現

目錄

項目介紹

認證服務端 spring-oauth-server

添加依賴 pom.xml

配置文件 application.yml

啓動類

認證服務配置 AuthorizationServerConfigurerAdapter

安全配置 WebSecurityConfigurerAdapter

自定義登錄接口提供 LoginController 及頁面

受保護的接口 UserController 要求登錄認證。

客戶端實現

添加依賴 pom.xml

啓動類

安全配置 WebSecurityConfigurerAdapter

頁面配置

配置文件 application.yml

頁面文件 index securedPage

測試效果

執行流程剖析

源碼下載


項目介紹

這個單點登錄系統包括下面幾個模塊:

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/ 
版權所有,歡迎轉載,轉載請註明原文作者及出處。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章