天天看點

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

單點登入(SingleSignOn,SSO),就是通過使用者的一次性鑒别登入。當使用者在身份認證伺服器上登入一次以後,即可獲得通路單點登入系統中其他關聯系統和應用軟體的權限,同時這種實作是不需要管理者對使用者的登入狀态或其他資訊進行修改的,這意味着在多個應用系統中,使用者隻需一次登入就可以通路所有互相信任的應用系統。

随着企業各系統越來越多,如辦公自動化(OA)系統,财務管理系統,檔案管理系統,資訊查詢系統等,登入問題就變得愈發重要。要記錄那麼多的使用者名和密碼,實在不是一件容易的事兒。而為了便于記憶,很多人都在不同的站點使用相同的使用者名和密碼,雖然這樣可以減少負擔,但是同時也降低了安全性,而且使用不同的站點同樣要進行多次登入。基于以上原因,為使用者提供一個暢通的登入通道變得十分重要。

單點登入(SingleSign-On,SSO)是一種幫助使用者快捷通路網絡中多個站點的安全通信技術。單點登入系統基于一種安全的通信協定,該協定通過多個系統之間的使用者身份資訊的交換來實作單點登入。使用單點登入系統時,使用者隻需要登入一次,就可以通路多個系統,不需要記憶多個密碼密碼。單點登入使使用者可以快速通路網絡,進而提高工作效率,同時也能幫助提高系統的安全性。

OAUTH協定為使用者資源的授權提供了一個安全的、開放而又簡易的标準。與以往的授權方式不同之處是OAUTH的授權不會使第三方觸及到使用者的帳号資訊(如使用者名與密碼),即第三方無需使用使用者的使用者名與密碼就可以申請獲得該使用者資源的授權,是以OAUTH是安全的。OAuth是Open Authorization的簡寫。

雖然OAuth2一開始是用來允許使用者授權第三方應用通路其資源的一種協定,并不是用來做單點登入的,但是我們可以用其特性,來變相的實作單點登入,其中就要用到其授權碼模式(authorization code),并且,token生成使用JWT。

下面我們建立一套工程,包含授權平台、OA-綜合辦公平台、CRM-移動營銷平台,來模拟單點登入過程,闡述其配置進行,并針對其原理,進行深度剖析。

授權平台

pom

最終的pom依賴如下:

<?xml version="1.0" encoding="UTF-8"?>4.0.0        spring-security-oauth2-sso-sample        org.xbdframework.sample1.0.0-SNAPSHOTorg.xbdframework.sample    sso-auth-server    0.0.1-SNAPSHOTsso-auth-serverDemo project for Spring Bootorg.springframework.boot            spring-boot-starter-thymeleaf        org.springframework.boot            spring-boot-starter-web        org.springframework.boot            spring-boot-starter-security        org.springframework.security.oauth.boot            spring-security-oauth2-autoconfigure        org.springframework.boot            spring-boot-starter-test            testorg.springframework.boot                spring-boot-maven-plugin            
           

請注意,spring-security-oauth2-autoconfigure依賴必不可少,這是SpringBoot工程,而不是SpringCloud工程。SpringCloud的話,引入oauth2 starter即可。

EnableAuthorizationServer

授權伺服器配置如下:

package org.xbdframework.sample.sso.authserver.confg;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;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.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;@[email protected] class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {    @Autowired    private PasswordEncoder passwordEncoder;    @Override    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {        security.allowFormAuthenticationForClients()                .tokenKeyAccess("isAuthenticated()");    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {        clients.withClientDetails(a1());    }    @Bean    public ClientDetailsService a1() throws Exception {        return new InMemoryClientDetailsServiceBuilder()                // client oa application                .withClient("oa")                .secret(passwordEncoder.encode("oa_secret"))                .scopes("all")                .authorizedGrantTypes("authorization_code", "refresh_token")                .redirectUris("http://localhost:8080/oa/login", "http://www.baidu.com")                .accessTokenValiditySeconds(7200)                .autoApprove(true)                .and()                // client crm application                .withClient("crm")                .secret(passwordEncoder.encode("crm_secret"))                .scopes("all")                .authorizedGrantTypes("authorization_code", "refresh_token")                .redirectUris("http://localhost:8090/crm/login")                .accessTokenValiditySeconds(7200)                .autoApprove(true)                .and()                .build();    }    @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints.accessTokenConverter(jwtAccessTokenConverter())                .tokenStore(jwtTokenStore());    }    @Bean    public JwtTokenStore jwtTokenStore() {        return new JwtTokenStore(jwtAccessTokenConverter());    }    @Bean    public JwtAccessTokenConverter jwtAccessTokenConverter() {        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();        jwtAccessTokenConverter.setSigningKey("123456");        return jwtAccessTokenConverter;    }}
           

WebSecurityConfiguration

Spring Security 配置如下:

package org.xbdframework.sample.sso.authserver.confg;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;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.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.provisioning.InMemoryUserDetailsManager;import java.util.ArrayList;import java.util.Collection;import java.util.List;@Configurationpublic class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());    }    @Override    public void configure(WebSecurity web) throws Exception {        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http.formLogin()                .loginPage("/login")                .and()                .authorizeRequests()                .antMatchers("/login").permitAll()                .anyRequest()                .authenticated()                .and().csrf().disable().cors();    }    @Bean    @Override    public UserDetailsService userDetailsServiceBean() {        Collection users = buildUsers();        return new InMemoryUserDetailsManager(users);    }    private Collection buildUsers() {        String password = passwordEncoder().encode("123456");        List users = new ArrayList<>();        UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();        UserDetails user_user1 = User.withUsername("user 1").password(password).authorities("USER").build();        users.add(user_admin);        users.add(user_user1);        return users;    }    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    @Bean    @Override    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }}
           

至于後續的建立登入頁面、首頁等,比較簡單,不再贅述,想檢視具體代碼請參考文末的工程連結,下載下傳後自行檢視。

OA-綜合辦公平台

pom

最終的pom依賴如下:

<?xml version="1.0" encoding="UTF-8"?>4.0.0        spring-security-oauth2-sso-sample        org.xbdframework.sample1.0.0-SNAPSHOTorg.xbdframework.sample    sso-oa    0.0.1-SNAPSHOTsso-oaDemo project for Spring Bootorg.springframework.boot            spring-boot-starter-oauth2-client        org.springframework.boot            spring-boot-starter-security        org.springframework.security.oauth.boot            spring-security-oauth2-autoconfigure        org.springframework.boot            spring-boot-starter-thymeleaf        org.thymeleaf.extras            thymeleaf-extras-springsecurity5        org.springframework.boot            spring-boot-starter-web        org.springframework.boot            spring-boot-starter-test            testorg.springframework.boot                spring-boot-maven-plugin            
           

WebSecurityConfiguration

Spring Security配置如下:

package org.xbdframework.sample.sso.oa.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.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@[email protected] class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    public void configure(WebSecurity web) throws Exception {        super.configure(web);    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http.logout()                .and()                .authorizeRequests()                .anyRequest().authenticated()                .and()                .csrf().disable();    }}
           

特别注意,一定不要忘記@EnableOAuth2Sso注解,這是單點登入相關自動化配置的入口。

CRM-移動營銷平台

相關配置同OA平台,不再贅述。

單點登入過程

系統都建立好以後,我們依次啟動授權系統(8888)、OA平台(8080)、CRM平台(8090)。通路OA平台http://localhost:8080/oa/system/profile。此時,浏覽器會重定向到授權系統登入頁面,需要登入。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

此時,我們檢視Network标簽,檢視其通路路徑,即可看到确實是先跳轉到了商品系統的登入頁面(http://localhost:8080/oa/login),然後又重定向到授權系統的授權連結,由于未登入,是以最後又重定向到授權系統的登入界面,如圖:

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

使用admin/123456進行登入,成功的跳轉到了OA平台的profile頁面。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

可以看到,admin使用者已成功登入。此時,再次檢視Network标簽。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

在成功登入之後,授權系統重定向到OA平台配置的回調位址(http://localhost:8080/oa/login),與此同時,攜帶了兩個參數code和state。最最重要的一個,便是code(state參數是防止CSRF攻擊而設定的,此處不談)。用戶端可根據此code,通路授權系統token接口(/oauth/token),申請token。申請成功後,重定向到OA平台配置的回調位址(http://localhost:8080/oa/login)。

然後,我們點選“CRM-移動營銷平台”(亦可直接在浏覽器輸入位址通路,效果是一樣的。CRM平台的通路位址為:http://localhost:8090/crm/sysmtem/profile),此時,我們并不需要登入,即可直接通路該頁面。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

再次檢視Network标簽。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

可以看到,與第一次通路OA平台相同,浏覽器先重定向到CRM平台的登入頁面,然後又重定向到授權系統的授權連結,最後直接就重新重定向到CRM平台的登入頁面,而不同的是,此次通路并不需要再次重定向到授權系統進行登入,而是成功通路授權系統的授權接口,并攜帶着code重定向到CRM平台的回調路徑。然後架構依據此code,再次通路授權的token接口(/oauth/token),順利拿到了token,可正常通路受保護的資源。

為什麼通路第二次無需登入,就直接拿到了第一次登入的使用者資訊了呢,它是怎麼拿到的,而且不至于發生錯亂呢?

這還是歸功于Spring Security。Spring Security第一個Filter,便是SecurityContextPersistenceFilter。作何用處呢?從字面意思了解,便是安全上下文持久化Filter,即存儲已認證成功的使用者資訊。如遇該使用者請求後續通路,則可直接取出并使用。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

其重點就在于HttpSessionSecurityContextRepository類的loadContext。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

再來看一下readSecurityContextFromSession方法是如何擷取SecurityContext的。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

Spring Security在認證成功後,會向Session中寫入一些屬性,而key值即為SPRING_SECURITY_CONTEXT。後續可根據Request請求中的Session,擷取此資訊,其中有登入使用者、權限等資訊。

基于同一浏覽器通路同一網站,其JSESSIONID固定。是以當通路OA平台時,浏覽器會重定向到授權平台,此時會生成一個JSESSIONID,以辨別目前登入使用者,然後再重定向回OA平台回調位址;當再通路CRM平台時,一樣會重定向到授權平台,此時授權平台根據此前生成的唯一JSESSIONID,可直接擷取上一次登入使用者的資訊。這一節作者也是查找了好久,才找到這裡,真是不容易!

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼
oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

如圖所示,OA平台、CRM平台,在通路授權平台時,為同一個JSESSIONID。

當使用者通路伺服器的時候會為每一個使用者開啟一個session,浏覽器正是基于JSESSIONID,來判斷這個SESSION到底屬于哪個使用者。即JSESSIONID就是用來判斷目前使用者對應于哪個SESSION。換句話說,伺服器識别SESSION的方法是通過JSESSIONID來告訴伺服器該用戶端的SESSION在記憶體的什麼地方。

用戶端

我們來分析一下用戶端是如何觸發一系列請求的。

在前文中說過一個注解非常重要,就是@EnableOAuth2Sso。依托此注解,架構自動注冊了OAuth2ClientAuthenticationProcessingFilter執行個體。從名字即能看出作何用處。其中,重要邏輯如下:

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

第二部分沒什麼說的,重點在于第一部分,從OAuth2RestTemplate中擷取token。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

可以看到,架構先從OAuth2ClientContext中擷取緩存的token,如沒有,再調用acquireAccessToken方法進行擷取。如果發生UserRedirectRequiredException異常,則抛出。記着這裡抛出的這個異常,就是由于此,才會觸發後續一系列的授權、登入等重定向。

先來說一下acquireAccessToken方法。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

其它的都不重要,重點一就在于accessTokenProvider.obtainAccessToken這一句話,而accessTokenProvider不是别的,正是AuthorizationCodeAccessTokenProvider。重點二,一旦擷取了token,即緩存到OAuth2ClientContext中。是以,後續請求可直接從OAuth2ClientContext中擷取token,就是這個原因。

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

在第一次通路OA平台時時,由于沒有登入,也沒有申請授權,是以沒有code、沒有state。是以,架構會生成一個UserRedirectRequiredException并傳回,進而被OAuth2RestTemplate的getAccessToken方法捕獲并抛出。

那麼,抛出的異常架構怎麼處理的,觸發了重定向呢?

答案就是OAuth2ClientContextFilter,在後續filter過程中,會觸發重定向,邏輯如下:

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

而重定向傳回回調url之後,跟通路profile頁面跳轉到用戶端應用自己的登入頁面一樣,都是/login,而剛好被OAuth2ClientAuthenticationProcessingFilter所攔截,其攔截路徑,就是/login。不同的是,前者是後者一系列操作後的後續操作,即通路profile頁面跳轉到用戶端應用自己的登入頁面,被OAuth2ClientAuthenticationProcessingFilter攔截,進而發生UserRedirectRequiredException異常,重定向到授權服務申請授權,申請成功後又重定向到登入頁面,進而成功根據code擷取到token。

下面再說明一下AccessTokenRequest對象和OAuth2ClientContext對象,這兩個bean的聲明如下:

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

可以看到,AccessTokenRequest對象scope為request,針對每個HTTP的request請求有效,也就是說,在一次HTTP請求中,每個Bean定義對應一個執行個體。而OAuth2ClientContext對象的scope為session,針對每個HTTP的Session有效,即在一個HTTP Session中,每個Bean定義對應一個執行個體。這樣,便把不同請求、不同使用者給區分開了。

UML時序圖

整個授權過程的時序圖如下:

oauth2 單點登入_Spring Security Oauth2 單點登入配置及原理深度剖析授權平台OA-綜合辦公平台CRM-移動營銷平台單點登入過程用戶端UML時序圖源碼

最後,再次感歎一下,Spring Security OAuth2架構真的厲害!

源碼

附上源碼,以供參考,歡迎star、fork!

github

https://github.com/liuminglei/spring-security-oauth2-sso-sample.git

gitee

https://gitee.com/xbd521/spring-security-oauth2-sso-sample.git