需要先說一下,松哥最近寫的教程,都是成系列的,有一些重複的東西寫來寫去就沒意思了,是以每一篇文章都預設大家已經懂了前面的内容了,是以下文有任何看不懂的地方,建議一定先看下相關系列:
「Spring Security 系列:」
- 挖一個大坑,Spring Security 開搞!
- 松哥手把手帶你入門 Spring Security,别再問密碼怎麼解密了
- 手把手教你定制 Spring Security 中的表單登入
- Spring Security 做前後端分離,咱就别做頁面跳轉了!統統 JSON 互動
- Spring Security 中的授權操作原來這麼簡單
- Spring Security 如何将使用者資料存入資料庫?
- Spring Security+Spring Data Jpa 強強聯手,安全管理隻有更簡單!
「OAuth2 系列:」
- 做微服務繞不過的 OAuth2,松哥也來和大家扯一扯
- 這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登入流程?
- 死磕 OAuth2,教練我要學全套的!
- OAuth2 令牌還能存入 Redis ?越玩越溜!
- 想讓 OAuth2 和 JWT 在一起愉快玩耍?請看松哥的表演
- 和大家分享一點微服務架構中的安全管理思路
好了,開始今天的正文。
單點登入是我們在分布式系統中很常見的一個需求。
分布式系統由多個不同的子系統組成,而我們在使用系統的時候,隻需要登入一次即可,這樣其他系統都認為使用者已經登入了,不用再去登入。前面和小夥伴們分享了 OAuth2+JWT 的登入方式,這種無狀态登入實際上天然的滿足單點登入的需求,可以參考:想讓 OAuth2 和 JWT 在一起愉快玩耍?請看松哥的表演。
當然大家也都知道,無狀态登入也是有弊端的。
是以今天松哥想和大家說一說 Spring Boot+OAuth2 做單點登入,利用 @EnableOAuth2Sso 注解快速實作單點登入功能。
松哥依然建議大家在閱讀本文時,先看看本系列前面的文章,這有助于更好的了解本文。
1.項目建立
前面的案例中,松哥一直都把授權伺服器和資源伺服器分開建立,今天這個案例,為了省事,我就把授權伺服器和資源伺服器搭建在一起(不過相信大家看了前面的文章,應該也能自己把這兩個伺服器拆分開)。
是以,今天我們一共需要三個服務:
項目 端口 描述 auth-server 1111 授權伺服器+資源伺服器 client1 1112 子系統1 client2 1113 子系統2
auth-server 用來扮演授權伺服器+資源伺服器的角色,client1 和 client2 則分别扮演子系統的角色,将來等 client1 登入成功之後,我們也就能通路 client2 了,這樣就能看出來單點登入的效果。
我們建立一個名為 oauth2-sso 的 Maven 項目作為父工程即可。
2.統一認證中心
接下來我們來搭建統一認證中心。
首先我們建立一個名為 auth-server 的 module,建立時添加如下依賴:

項目建立成功之後,這個子產品由于要扮演授權伺服器+資源伺服器的角色,是以我們先在這個項目的啟動類上添加 @EnableResourceServer 注解,表示這是一個資源伺服器:
@[email protected] class AuthServerApplication { public static void main(String[] args) { SpringApplication.run(AuthServerApplication.class, args); }}
接下來我們進行授權伺服器的配置,由于資源伺服器和授權伺服器合并在一起,是以授權伺服器的配置要省事很多:
@[email protected] class AuthServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired PasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("javaboy") .secret(passwordEncoder.encode("123")) .autoApprove(true) .redirectUris("http://localhost:1112/login", "http://localhost:1113/login") .scopes("user") .accessTokenValiditySeconds(7200) .authorizedGrantTypes("authorization_code"); }}
這裡我們隻需要簡單配置一下用戶端的資訊即可,這裡的配置很簡單,前面的文章也講過了,大家要是不懂,可以參考本系列前面的文章:這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登入流程?。
當然這裡為了簡便,用戶端的資訊配置是基于記憶體的,如果大家想将用戶端資訊存入資料庫中,也是可以的,參考:OAuth2 令牌還能存入 Redis ?越玩越溜!
接下來我們再來配置 Spring Security:
@[email protected](1)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/login.html", "/css/**", "/js/**", "/images/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers("/login") .antMatchers("/oauth/authorize") .and() .authorizeRequests().anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/login") .permitAll() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("sang") .password(passwordEncoder().encode("123")) .roles("admin"); }}
關于 Spring Security 的配置,如果小夥伴們不懂,可以看看松哥最近正在連載的 Spring Security 系列。
我這裡來大緻捋一下:
- 首先提供一個 BCryptPasswordEncoder 的執行個體,用來做密碼加解密用。
- 由于我自定義了登入頁面,是以在 WebSecurity 中對這些靜态資源方形。
- HttpSecurity 中,我們對認證相關的端點放行,同時配置一下登入頁面和登入接口。
- AuthenticationManagerBuilder 中提供一個基于記憶體的使用者(小夥伴們可以根據 Spring Security 系列第 7 篇文章自行調整為從資料庫加載)。
- 另外還有一個比較關鍵的地方,因為資源伺服器和授權伺服器在一起,是以我們需要一個 @Order 注解來提升 Spring Security 配置的優先級。
SecurityConfig 和 AuthServerConfig 都是授權伺服器需要提供的東西(如果小夥伴們想将授權伺服器和資源伺服器拆分,請留意這句話),接下來,我們還需要提供一個暴露使用者資訊的接口(如果将授權伺服器和資源伺服器分開,這個接口将由資源伺服器提供):
@RestControllerpublic class UserController { @GetMapping("/user") public Principal getCurrentUser(Principal principal) { return principal; }}
最後,我們在 application.properties 中配置一下項目端口:
server.port=1111
另外,松哥自己提前準備了一個登入頁面,如下:
将登入頁面相關的 html、css、js 等拷貝到 resources/static 目錄下:
這個頁面很簡單,就是一個登入表單而已,我把核心部分列出來:
使用者名
密碼
登入
注意一下 action 送出位址不要寫錯即可。
「文末可以下載下傳源碼。」
如此之後,我們的統一認證登入平台就算是 OK 了。
3.用戶端建立
接下來我們來建立一個用戶端項目,建立一個名為 client1 的 Spring Boot 項目,添加如下依賴:

項目建立成功之後,我們來配置一下 Spring Security:
@[email protected] class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and().csrf().disable(); }}
這段配置很簡單,就是說我們 client1 中所有的接口都需要認證之後才能通路,另外添加一個 @EnableOAuth2Sso 注解來開啟單點登入功能。
接下來我們在 client1 中再來提供一個測試接口:
@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication.getName() + Arrays.toString(authentication.getAuthorities().toArray()); }}
這個測試接口傳回目前登入使用者的姓名和角色資訊。
接下來我們需要在 client1 的 application.properties 中配置 oauth2 的相關資訊:
security.oauth2.client.client-secret=123security.oauth2.client.client-id=javaboysecurity.oauth2.client.user-authorization-uri=http://localhost:1111/oauth/authorizesecurity.oauth2.client.access-token-uri=http://localhost:1111/oauth/tokensecurity.oauth2.resource.user-info-uri=http://localhost:1111/userserver.port=1112server.servlet.session.cookie.name=s1
這裡的配置也比較熟悉,我們來看一下:
- client-secret 是用戶端密碼。
- client-id 是用戶端 id。
- user-authorization-uri 是使用者授權的端點。
- access-token-uri 是擷取令牌的端點。
- user-info-uri 是擷取使用者資訊的接口(從資源伺服器上擷取)。
- 最後再配置一下端口,然後給 cookie 取一個名字。
如此之後,我們的 client1 就算是配置完成了。
按照相同的方式,我們再來配置一個 client2,client2 和 client1 一模一樣,就是 cookie 的名字不同(随意取,不相同即可)。
4.測試
接下來,我們分别啟動 auth-server、client1 和 client2,首先我們嘗試去方式 client1 中的 hello 接口,這個時候會自動跳轉到統一認證中心:
然後輸入使用者名密碼進行登入。
登入成功之後,會自動跳轉回 client1 的 hello 接口,如下:
此時我們再去通路 client2 ,發現也不用登入了,直接就可以通路:
OK,如此之後,我們的單點登入就成功了。
5.流程解析
最後,我再來和小夥伴們把上面代碼的一個執行流程捋一捋:
- 首先我們去通路 client1 的 /hello 接口,但是這個接口是需要登入才能通路的,是以我們的請求被攔截下來,攔截下來之後,系統會給我們重定向到 client1 的 /login 接口,這是讓我們去登入。
- 當我們去通路 client1 的登入接口時,由于我們配置了 @EnableOAuth2Sso 注解,這個操作會再次被攔截下來,單點登入攔截器會根據我們在 application.properties 中的配置,自動發起請求去擷取授權碼:
- 在第二步發送的請求是請求 auth-server 服務上的東西,這次請求當然也避免不了要先登入,是以再次重定向到 auth-server 的登入頁面,也就是大家看到的統一認證中心。
- 在統一認真中心我們完成登入功能,登入完成之後,會繼續執行第二步的請求,這個時候就可以成功擷取到授權碼了。
- 擷取到授權碼之後,這個時候會重定向到我們 client1 的 login 頁面,但是實際上我們的 client1 其實是沒有登入頁面的,是以這個操作依然會被攔截,此時攔截到的位址包含有授權碼,拿着授權碼,在 OAuth2ClientAuthenticationProcessingFilter 類中向 auth-server 發起請求,就能拿到 access_token 了(參考:這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登入流程?)。
- 在第五步拿到 access_token 之後,接下來在向我們配置的 user-info-uri 位址發送請求,擷取登入使用者資訊,拿到使用者資訊之後,在 client1 上自己再走一遍 Spring Security 登入流程,這就 OK 了。
OK,本文和小夥伴們聊了一些 SpringBoot +OAuth2 單點登入的問題,完整案例下載下傳位址:https://github.com/lenve/oauth2-samples
如果小夥伴們覺得有用的話,記得點個在看鼓勵下松哥。