Spring Security功能多,組件抽象程度高,配置方式多樣,導致了Spring Security強大且複雜的特性。Spring Security的學習成本幾乎是Spring家族中最高的,Spring Security的精良設計值得我們學習,但是結合實際複雜的業務場景,我們不但需要理解Spring Security的擴展方式還需要去理解一些組件的工作原理和流程(否則怎麼去繼承並改寫需要改寫的地方呢?),這又帶來了更高的門檻,因此,在決定使用Spring Security搭建整套安全體系(授權、認證、許可權、審計)之前還是需要考慮一下將來我們的業務會多複雜,我們徒手寫一套安全體系來的划算還是使用Spring Security更好。

短短的一篇文章不可能覆蓋Spring Security的方方面面,在最近的工作中會比較多接觸OAuth2,因此本文以這個維度來簡單闡述一下如果使用Spring Security搭建一套OAuth2授權&SSO架構。

OAuth2簡介

OAuth2.0是一套授權體系的開放標準,定義了四大角色:

  1. 資源擁有者,也就是用戶,由用於授予三方應用許可權
  2. 客戶端,也就是三方應用程序,在訪問用戶資源之前需要用戶授權
  3. 資源提供者,或者說資源伺服器,提供資源,需要實現Token和ClientID的校驗,以及做好相應的許可權控制
  4. 授權伺服器,驗證用戶身份,為客戶端頒發Token,並且維護管理ClientID、Token以及用戶

其中後三項都可以是獨立的程序,在本文的例子中我們會為這三者建立獨立的項目。OAuth2.0標準同時定義了四種授權模式,這裡介紹最常用的三種,也是後面會演示的三種(在之後的介紹中令牌=Token,碼=Code,可能會混合表達):

不管是哪種模式,通用流程如下:

  • 三方網站(或者說客戶端)需要先向授權伺服器去申請一套接入的ClientID+ClientSecret
  • 用任意一種模式拿到訪問Token(流程見下)
  • 拿著訪問Token去資源伺服器請求資源
  • 資源伺服器根據Token查詢到Token對應的許可權進行許可權控制

授權碼模式,最標準最安全的模式,適合和外部交互,流程是:

  • 三方網站客戶端轉到授權伺服器,上送ClientID,授權範圍Scope、重定向地址RedirectUri等信息
  • 用戶在授權伺服器進行登錄並且進行授權批准(授權批准這步可以配置為自動完成)
  • 授權完成後重定向回到之前客戶端提供的重定向地址,附上授權碼
  • 三方網站服務端通過授權碼+ClientID+ClientSecret去授權伺服器換取Token(Token含訪問Token和刷新Token,訪問Token過去後用刷新Token去獲得新的訪問Token)
  • 你可能會問這個模式為什麼這麼複雜,為什麼安全呢?因為我們不會對外暴露ClientSecret,不會對外暴露訪問Token,使用授權碼換取Token的過程是服務端進行,客戶端拿到的只是一次性的授權碼

密碼憑證模式,適合內部系統之間使用的模式(客戶端是自己人,客戶端需要拿到用戶帳號密碼),流程是:

  • 用戶提供帳號密碼給客戶端
  • 客戶端憑著用戶的帳號密碼,以及客戶端自己的ClientID+ClientSecret去授權伺服器換取Token

客戶端模式,適合內部服務端之間使用的模式:

  • 和用戶沒有關係,不是基於用戶的授權
  • 客戶端憑著自己的ClientID+ClientSecret去授權伺服器換取Token

下面,我們來搭建程序實際體會一下這幾種模式。

搭建授權伺服器

首先來創建一個父POM,內含三個模塊:

(省略,見源碼)

然後我們創建第一個模塊,資源伺服器pom如下:

(省略,見源碼)

這邊我們除了使用了Spring Cloud的OAuth2啟動器之外還使用數據訪問、Web等依賴,因為我們的資源伺服器需要使用資料庫來保存客戶端的信息、用戶信息等數據,我們同時也會使用thymeleaf來稍稍美化一下登錄頁面。

現在我們來創建一個配置文件application.yml:

server:

port: 8080spring: application: name: oauth2-server datasource: url: jdbc:mysql://localhost:3306/oauth?useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver

可以看到,我們會使用oauth資料庫,授權伺服器的埠是8080。

資料庫中我們需要初始化一些表:

  1. 用戶表users:存放用戶名密碼
  2. 授權表authorities:存放用戶對應的許可權
  3. 客戶端信息表oauth_client_details:存放客戶端的ID、密碼、許可權、允許訪問的資源伺服器ID以及允許使用的授權模式等信息
  4. 授權碼錶oauth_code:存放了授權碼
  5. 授權批准表oauth_approvals:存放了用戶授權第三方伺服器的批准情況

DDL見源碼

在之後演示的時候會看到這些表中的數據。這裡可以看到我們並沒有在資料庫中創建相應的表來存放訪問令牌、刷新令牌,這是因為我們之後的實現會把令牌信息使用JWT來傳輸,不會存放到資料庫中。基本上所有的這些表都是可以自己擴展的,只需要繼承實現Spring的一些既有類即可,這裡不做展開。

下面,我們創建一個最核心的類用於配置授權伺服器:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

@Configuration@EnableAuthorizationServerpublic class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired

private DataSource dataSource;

@Autowired private AuthenticationManager authenticationManager; /** * 代碼1 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.jdbc(dataSource);

} /** * 代碼2 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("permitAll()")

.allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance());

} /** * 代碼3 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();

tokenEnhancerChain.setTokenEnhancers(

Arrays.asList(tokenEnhancer(), jwtTokenEnhancer())); endpoints.approvalStore(approvalStore()) .authorizationCodeServices(authorizationCodeServices()) .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public AuthorizationCodeServices authorizationCodeServices() {

return new JdbcAuthorizationCodeServices(dataSource);

} @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtTokenEnhancer()); } @Bean public JdbcApprovalStore approvalStore() { return new JdbcApprovalStore(dataSource); }

@Bean

public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); } @Bean protected JwtAccessTokenConverter jwtTokenEnhancer() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray()); JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt")); return converter; } /** * 代碼4 */ @Configuration static class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("login").setViewName("login"); } }}

分析下這個類:

  1. 首先我們可以看到,我們需要通過註解@EnableAuthorizationServer來開啟授權伺服器
  2. 代碼片段1中,我們配置了使用資料庫來維護客戶端信息,當然在各種Demo中我們經常看到的是在內存中維護客戶端信息,通過配置直接寫死在這裡,對於實際的應用我們一般都會用資料庫來維護這個信息,甚至還會建立一套工作流來允許客戶端自己申請ClientID
  3. 代碼片段2中,針對授權伺服器的安全,我們幹了兩個事情,首先打開了驗證Token的訪問許可權(以便之後我們演示),然後允許ClientSecret明文方式保存並且可以通過表單提交(而不僅僅是Basic Auth方式提交),之後會演示到這個
  4. 代碼片段3中,我們幹了幾個事情:
    1. 配置我們的Token存放方式不是內存方式、不是資料庫方式、不是Redis方式而是JWT方式,JWT是Json Web Token縮寫也就是使用JSON數據格式包裝的Token,由.句號把整個JWT分隔為頭、數據體、簽名三部分,JWT保存Token雖然易於使用但是不是那麼安全,一般用於內部,並且需要走HTTPS+配置比較短的失效時間
    2. 配置了JWT Token的非對稱加密來進行簽名
    3. 配置了一個自定義的Token增強器,把更多信息放入Token中
    4. 配置了使用JDBC資料庫方式來保存用戶的授權批准記錄
  1. 代碼片段4中,我們配置了登錄頁面的視圖信息(其實可以獨立一個配置類更規範)

針對剛才的代碼,我們需要補充一些東西到資源目錄下,首先需要在資源目錄下創建一個templates文件夾然後創建一個login.html登錄模板:

(省略,見源碼)

然後,我們需要使用keytool工具生成密鑰,把密鑰文件jks保存到目錄下,然後還要導出一個公鑰留作以後使用。剛纔在代碼中我們還用到了一個自定義的Token增強器,實現如下:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Authentication userAuthentication = authentication.getUserAuthentication(); if (userAuthentication != null) { Object principal = authentication.getUserAuthentication().getPrincipal(); Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("userDetails", principal); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); } return accessToken; }}

這段代碼非常簡單,就是把用戶信息以userDetails這個Key存放到Token中去(如果授權模式是客戶端模式這段代碼無效,因為和用戶沒關係)。這是一個常見需求,默認情況下Token中只會有用戶名這樣的基本信息,我們往往需要把有關用戶的更多信息返回給客戶端(在實際應用中你可能會從資料庫或外部服務查詢更多的用戶信息加入到JWT Token中去),這個時候就可以自定義增強器來豐富Token的內容。

到此授權伺服器的核心配置已經完成,現在我們再來實現一下安全方面的配置:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/oauth/authorize") .permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login"); }}

這裡我們主要做了兩個事情:

  1. 配置用戶賬戶的認證方式,顯然,我們把用戶存在了資料庫中希望配置JDBC的方式,此外,我們還配置了使用BCryptPasswordEncoder加密來保存用戶的密碼(生產環境的用戶密碼肯定不能是明文保存)
  2. 開放/login和/oauth/authorize兩個路徑的匿名訪問,前者用於登錄,後者用於換授權碼,這兩個端點訪問的時候都在登錄之前

最後配置一個主程序:

package me.josephzhu.springsecurity101.cloud.oauth2.server;

@SpringBootApplicationpublic class OAuth2ServerApplication { public static void main(String[] args) { SpringApplication.run(OAuth2ServerApplication.class, args); }}

至此,授權伺服器的配置完成。

搭建資源伺服器

先來創建項目:

(省略,見源碼)

配置及其簡單,聲明資源服務埠8081

server:

port: 8081

還記得在資源文件夾下放我們之前通過密鑰導出的公鑰文件,類似:

-----BEGIN PUBLIC KEY-----

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmDmPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Zw5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1peh4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqkLciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul+QIDAQAB-----END PUBLIC KEY-----

先來創建一個可以匿名訪問的介面GET /hello:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

@RestControllerpublic class HelloController { @GetMapping("hello") public String hello() { return "Hello"; }}

再來創建一個需要登錄+授權才能訪問到的一些介面:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

@RestController@RequestMapping("user")public class UserController { @Autowired private TokenStore tokenStore; @PreAuthorize("hasAuthority(READ) or hasAuthority(WRITE)") @GetMapping("name") public String name(OAuth2Authentication authentication) { return authentication.getName(); } @PreAuthorize("hasAuthority(READ) or hasAuthority(WRITE)") @GetMapping public OAuth2Authentication read(OAuth2Authentication authentication) { return authentication; } @PreAuthorize("hasAuthority(WRITE)") @PostMapping public Object write(OAuth2Authentication authentication) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); OAuth2AccessToken accessToken = tokenStore.readAccessToken(details.getTokenValue()); return accessToken.getAdditionalInformation().getOrDefault("userDetails", null); }}

這裡我們配置了三個介面,並且通過@PreAuthorize在方法執行前進行許可權控制:

  1. GET /user/name介面讀寫許可權都可以訪問
  2. GET /user介面讀寫許可權都可以訪問,返回整個OAuth2Authentication
  3. POST /user介面只有寫許可權可以訪問,返回之前的CustomTokenEnhancer加入到Token中的額外信息,Key是userDetails,這裡也演示了使用TokenStore來解析Token的方式

下面我們來創建核心的資源伺服器配置類:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

@Configuration@EnableResourceServer@EnableGlobalMethodSecurity(prePostEnabled = true)public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { /** * 代碼1 * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("foo").tokenStore(tokenStore()); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean protected JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); Resource resource = new ClassPathResource("public.cert"); String publicKey = null; try { publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); } catch (IOException e) { e.printStackTrace(); } converter.setVerifierKey(publicKey); return converter; } /** * 代碼2 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/user/**").authenticated() .anyRequest().permitAll(); }}

這裡我們幹了四件事情:

  1. @EnableResourceServer啟用資源伺服器
  2. @EnableGlobalMethodSecurity(prePostEnabled = true)啟用方法註解方式來進行許可權控制
  3. 代碼1,聲明瞭資源伺服器的ID是foo,聲明瞭資源伺服器的TokenStore是JWT以及公鑰
  4. 代碼2,配置了除了/user路徑之外的請求可以匿名訪問

我們想一下,如果授權伺服器產生Token的話,資源伺服器必須是要有一種辦法來驗證Token的,如果是非JWT的方式,我們可以這麼辦:

  1. Token可以保存在資料庫或Redis中,資源伺服器和授權伺服器共享底層的TokenStore來驗證
  2. 資源伺服器可以使用RemoteTokenServices來從授權伺服器的/oauth/check_token端點進行Token校驗(還記得嗎,我們之前開放過這個埠)

現在我們使用的是不落地的JWT方式+非對稱加密,需要通過本地公鑰進行驗證,因此在這裡我們配置了公鑰的路徑。

最後創建一個啟動類:

package me.josephzhu.springsecurity101.cloud.oauth2.userservice;

@SpringBootApplicationpublic class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); }}

至此,資源伺服器配置完成,我們還在資源伺服器中分別建了兩個控制器,用於測試匿名訪問和收到資源伺服器許可權保護的資源。

初始化數據配置

現在我們來看一下如何配置資料庫實現:

  1. 兩個用戶,讀用戶reader具有讀許可權,寫用戶writer具有讀寫許可權
  2. 兩個許可權,讀和寫
  3. 三個客戶端:
    1. userservice1這個客戶端使用密碼憑證模式
    2. userservice2這個客戶端使用客戶端模式
    3. userservice3這個客戶端使用授權碼模式

首先是oauth_client_details表:

INSERT INTO `oauth_client_details` VALUES (userservice1, foo, 1234, FOO, password,refresh_token, , READ,WRITE, 7200, NULL, NULL, true);

INSERT INTO `oauth_client_details` VALUES (userservice2, foo, 1234, FOO, client_credentials,refresh_token, , READ,WRITE, 7200, NULL, NULL, true);

INSERT INTO `oauth_client_details` VALUES (userservice3, foo, 1234, FOO, authorization_code,refresh_token, baidu.com, READ,WRITE, 7200, NULL, NULL, false);

如之前所說,這裡配置了三條記錄:

  1. 它們能使用的資源ID都是foo,對應我們資源伺服器userservice的配置
  2. 它們的授權範圍都是FOO,可以拿到的許可權是讀寫(但對於用戶關聯的模式,最終拿到的許可權還取決於客戶端許可權和用戶許可權的交集)
  3. 通過grant_types欄位配置了支持的不同的授權模式,這裡我們為了便於測試觀察給三個客戶端各自配置了一個模式,你完全可以為一個客戶端配置支持OAuth2.0的那四種模式
  4. userservice1和2我們配置了用戶自動批准授權(不會彈出一個頁面要求用戶進行授權那種)

然後是authorities表,其中我們配置了兩條記錄,配置reader用戶具有讀許可權,writer用戶具有寫許可權:

INSERT INTO `authorities` VALUES (reader, READ);

INSERT INTO `authorities` VALUES (writer, READ,WRITE);

最後是users表配置了兩個用戶的賬戶名和密碼:

INSERT INTO `users` VALUES (reader, $2a$04$C6pPJvC1v6.enW6ZZxX.luTdpSI/1gcgTVN7LhvQV6l/AfmzNU/3i, 1);

INSERT INTO `users` VALUES (writer, $2a$04$M9t2oVs3/VIreBMocOujqOaB/oziWL0SnlWdt8hV4YnlhQrORA0fS, 1);

還記得嗎,密碼我們使用的是BCryptPasswordEncoder加密(準確說是哈希),可以使用一些在線工具進行哈希

演示三種授權模式

客戶端模式

POST請求地址:

localhost:8080/oauth/to

如下圖所示,直接可以拿到Token:

這裡注意到並沒有提供刷新令牌,刷新令牌用於避免訪問令牌失效後還需要用戶登錄,客戶端模式沒有用戶概念,沒有刷新令牌。我們把得到的Token粘貼到https://jwt.io/#debugger-io查看:

如果粘貼進去公鑰的話還可以看到Token簽名驗證成功:

也可以試一下,如果我們的授權伺服器沒有allowFormAuthenticationForClients的話,客戶端的憑證需要通過Basic Auth傳而不是Post過去:

還可以訪問授權伺服器來校驗Token:

http://localhost:8080/oauth/check_token?client_id=userservice1&client_secret=1234&token=...

得到如下結果:

密碼憑證模式

POST請求地址:

localhost:8080/oauth/to

得到如下圖結果:

再看下Token中的信息:

可以看到果然包含了我們TokenEnhancer加入的userDetails自定義信息。

授權碼模式

首先打開瀏覽器訪問地址:

localhost:8080/oauth/au

注意,我們客戶端跳轉地址需要和資料庫中配置的一致,百度的URL我們之前已經在資料庫中有配置了,訪問後頁面會跳轉到登錄界面,使用reader:reader登錄:

由於我們資料庫中設置的是禁用自動批准授權的模式,所以登錄後來到了批准界面:

點擊同意後可以看到資料庫中也會產生授權通過記錄:

然後我們可以看到瀏覽器轉到了百度並且提供給了我們授權碼:

baidu.com/?

資料庫中也記錄了授權碼:

然後POST訪問:localhost:8080/oauth/to

可以得到訪問令牌:

雖然userservice3客戶端可以有READ和WRITE許可權,但是我們登錄的用戶reader只有READ許可權,最後拿到的許可權只有READ

演示資源伺服器許可權控制

首先我們可以測試一下我們的安全配置,訪問/hello端點不需要認證可以匿名訪問:

訪問/user需要身份認證:

不管以哪種模式拿到訪問令牌,我們用具有讀許可權的訪問令牌GET訪問資源伺服器如下地址(請求頭加入Authorization: Bearer XXXXXXXXXX,其中XXXXXXXXXX代表Token):

http://localhost:8081/user/

可以得到如下結果:

以POST方式訪問localhost:8081/user/顯然是失敗的:

我們換一個具有讀寫許可權的令牌來試試:

果然可以成功,說明資源伺服器的許可權控制有效。

搭建客戶端程序

在之前,我們使用的是裸HTTP請求手動的方式來申請和使用令牌,最後我們來搭建一個OAuth客戶端程序自動實現這個過程:

(省略,見源碼)

配置文件如下:

server:

port: 8082 servlet: context-path: /uisecurity: oauth2: client: clientId: userservice3 clientSecret: 1234 accessTokenUri: http://localhost:8080/oauth/token userAuthorizationUri: http://localhost:8080/oauth/authorize scope: FOO resource: jwt: key-value: | (省略,見源碼)spring: thymeleaf: cache: false#logging:# level:# ROOT: DEBUG

客戶端項目埠8082,幾個需要說明的地方:

  1. 本地測試的時候一個坑就是我們需要配置context-path否則可能會出現客戶端和授權伺服器服務端Cookie幹擾導致CSRF防禦觸發的問題,這個問題出現後程序沒有任何錯誤日誌輸出,只有開啟DEBUG模式後才能看到DEBUG日誌裏有提示,這個問題非常難以排查,也不知道Spring為啥不把這個信息作為WARN級別
  2. 作為OAuth客戶端,我們需要配置OAuth服務端獲取令牌的地址以及授權(獲取授權碼)的地址,以及需要配置客戶端的ID和密碼,以及授權範圍
  3. 因為使用的是JWT Token,我們需要配置公鑰(當然,如果不在這裡直接配置公鑰的話也可以配置公鑰從授權伺服器服務端獲取)

首先實現MVC的配置:

package me.josephzhu.springsecurity101.cloud.auth.client;

@Configuration@EnableWebMvcpublic class WebMvcConfig implements WebMvcConfigurer { @Bean public RequestContextListener requestContextListener() { return new RequestContextListener(); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/") .setViewName("forward:/index"); registry.addViewController("/index"); }}

這裡做了兩個事情:

  1. 配置RequestContextListener用於啟用session scope的Bean
  2. 配置了index路徑的首頁Controller

然後實現安全方面的配置:

package me.josephzhu.springsecurity101.cloud.auth.client;

@Configuration@Order(200)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login**") .permitAll() .anyRequest() .authenticated(); }}

這裡我們實現的是/路徑和/login路徑允許訪問,其它路徑需要身份認證後才能訪問。

然後我們來創建一個控制器:

package me.josephzhu.springsecurity101.cloud.auth.client;

@RestControllerpublic class DemoController { @Autowired OAuth2RestTemplate restTemplate; @GetMapping("/securedPage") public ModelAndView securedPage(OAuth2Authentication authentication) { return new ModelAndView("securedPage").addObject("authentication", authentication); } @GetMapping("/remoteCall") public String remoteCall() { ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://localhost:8081/user/name", String.class); return responseEntity.getBody(); }}

這裡可以看到:

  1. 對於securedPage,我們把用戶信息作為模型傳入了視圖
  2. 我們引入了OAuth2RestTemplate,在登錄後就可以使用憑據直接從資源伺服器拿資源,不需要繁瑣的實現獲得訪問令牌,在請求頭裡加入訪問令牌的過程

在開始的時候我們定義了index頁面,模板如下:

(省略,見源碼)

現在又定義了securedPage頁面,模板如下:

(省略,見源碼)

接下去最關鍵的一步是啟用@EnableOAuth2Sso,這個註解包含了@EnableOAuth2Client:

package me.josephzhu.springsecurity101.cloud.auth.client;

@Configuration@EnableOAuth2Ssopublic class OAuthClientConfig { @Bean public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails details) { return new OAuth2RestTemplate(details, oAuth2ClientContext); }}

此外,我們這裡還定義了OAuth2RestTemplate,網上一些比較老的資料給出的是手動讀取配置文件來實現,最新版本已經可以自動注入OAuth2ProtectedResourceDetails。

最後是啟動類:

package me.josephzhu.springsecurity101.cloud.auth.client;

import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class OAuth2ClientApplication { public static void main(String[] args) { SpringApplication.run(OAuth2ClientApplication.class, args); }}

演示單點登錄

啟動客戶端項目,打開瀏覽器訪問localhost:8082/ui/secur

可以看到頁面自動轉到了授權伺服器的登錄頁面:

點擊登錄後出現如下錯誤:

顯然,之前我們資料庫中配置的redirect_uri是百度首頁,需要包含我們的客戶端地址,我們把欄位內容修改為4個地址:

https://baidu.com,http://localhost:8082/ui/login,http://localhost:8083/ui/login,http://localhost:8082/ui/remoteCall

刷新頁面,登錄成功:

我們再啟動另一個客戶端網站,埠改為8083,然後訪問同樣地址:

可以看到同樣是登錄狀態,SSO單點登錄測試成功,是不是很方便。

演示客戶端請求資源伺服器資源

最後,我們來訪問一下remoteCall介面:

可以看到輸出了用戶名,對應的資源伺服器服務端是:

@PreAuthorize("hasAuthority(READ) or hasAuthority(WRITE)")

@GetMapping("name")public String name(OAuth2Authentication authentication) { return authentication.getName();}

換一個用戶登錄試試:

總結

本文以OAuth 2.0這個維度來小窺了一下Spring Security的功能,介紹了OAuth 2.0的基本概念,體驗了三種常用模式,也使用Spring Security實現了OAuth 2.0的三個組件,客戶端、授權伺服器和資源伺服器,實現了資源伺服器的許可權控制,最後還使用客戶端測試了一下SSO和OAuth2RestTemplate使用,所有代碼見我的Github github.com/JosephZhu198,希望本文對你有用。


推薦閱讀:
查看原文 >>
相關文章