歡迎光臨
每天分享高質量文章

Re:從零開始的Spring Security OAuth2(一)

前言

今天來聊聊一個接口對接的場景,A廠家有一套HTTP接口需要提供給B廠家使用,由於是外網環境,所以需要有一套安全機制保障,這個時候oauth2就可以作為一個方案。

關於oauth2,其實是一個規範,本文重點講解spring對他進行的實現,如果你還不清楚授權服務器,資源服務器,認證授權等基礎概念,可以移步理解OAuth 2.0 – 阮一峰,這是一篇對於oauth2很好的科普文章。

需要對spring security有一定的配置使用經驗,用戶認證這一塊,spring security oauth2建立在spring security的基礎之上。第一篇文章主要是講解使用springboot搭建一個簡易的授權,資源服務器,在文末會給出具體代碼的github地址。後續文章會進行spring security oauth2的相關原始碼分析。java中的安全框架如shrio,已經有跟我學shiro – 開濤,非常成體系地,深入淺出地講解了apache的這個開源安全框架,但是spring security包括oauth2一直沒有成體系的文章,學習它們大多依賴於較少的官方文件,理解一下基本的使用配置;通過零散的博客,瞭解一下他人的使用經驗;打斷點,分析內部的工作流程;看原始碼中的接口設計,以及註釋,瞭解設計者的用意。spring的各個框架都運用了很多的設計樣式,在學習原始碼的過程中,也大概瞭解了一些套路。spring也在必要的地方添加了適當的註釋,避免了原始碼閱讀者對於一些細節設計的理解產生偏差,讓我更加感嘆,spring不僅僅是一個工具框架,更像是一個藝術品。


作者:老徐

原文地址:http://t.cn/Rp7xUro

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

概述

使用oauth2保護你的應用,可以分為簡易的分為三個步驟

  • 配置資源服務器

  • 配置認證服務器

  • 配置spring security

前兩點是oauth2的主體內容,但前面我已經描述過了,spring security oauth2是建立在spring security基礎之上的,所以有一些體系是公用的。

oauth2根據使用場景不同,分成了4種樣式

  • 授權碼樣式(authorization code)

  • 簡化樣式(implicit)

  • 密碼樣式(resource owner password credentials)

  • 客戶端樣式(client credentials)

本文重點講解接口對接中常使用的密碼樣式(以下簡稱password樣式)和客戶端樣式(以下簡稱client樣式)。授權碼樣式使用到了回呼地址,是最為複雜的方式,通常網站中經常出現的微博,qq第三方登錄,都會採用這個形式。簡化樣式不常用。

專案準備

主要的maven依賴如下


<dependency>
   <groupId>org.springframework.bootgroupId>


   <artifactId>spring-boot-starter-securityartifactId>
dependency>

<dependency>
   <groupId>org.springframework.security.oauthgroupId>
   <artifactId>spring-security-oauth2artifactId>
dependency>
<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
   <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

我們給自己先定個標的,要乾什麼事?既然說到保護應用,那必須得先有一些資源,我們創建一個endpoint作為提供給外部的接口:

@RestController
public class TestEndpoints {

   @GetMapping("/product/{id}")
   public String getProduct(@PathVariable String id) {
       //for debug
       Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
       return "product id : " + id;
   }

   @GetMapping("/order/{id}")
   public String getOrder(@PathVariable String id) {
       //for debug
       Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
       return "order id : " + id;
   }

}

暴露一個商品查詢接口,後續不做安全限制,一個訂單查詢接口,後續添加訪問控制。

配置資源服務器和授權服務器

由於是兩個oauth2的核心配置,我們放到一個配置類中。
為了方便下載代碼直接運行,我這裡將客戶端信息放到了記憶體中,生產中可以配置到資料庫中。token的儲存一般選擇使用redis,一是性能比較好,二是自動過期的機制,符合token的特性。

@Configuration
public class OAuth2ServerConfig {

   private static final String DEMO_RESOURCE_ID = "order";

   @Configuration
   @EnableResourceServer
   protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

       @Override
       public void configure(ResourceServerSecurityConfigurer resources) {
           resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
       }

       @Override
       public void configure(HttpSecurity http) throws Exception {
           // @formatter:off
           http
                   // Since we want the protected resources to be accessible in the UI as well we need
                   // session creation to be allowed (it's disabled by default in 2.0.6)
                   .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                   .and()
                   .requestMatchers().anyRequest()
                   .and()
                   .anonymous()
                   .and()
                   .authorizeRequests()
//                    .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
                   .antMatchers("/order/**").authenticated();//配置order訪問控制,必須認證過後才可以訪問
           // @formatter:on
       }
   }

   @Configuration
   @EnableAuthorizationServer
   protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

       @Autowired
       AuthenticationManager authenticationManager;
       @Autowired
       RedisConnectionFactory redisConnectionFactory;

       @Override
       public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
           //配置兩個客戶端,一個用於password認證一個用於client認證
           clients.inMemory().withClient("client_1")
                   .resourceIds(DEMO_RESOURCE_ID)
                   .authorizedGrantTypes("client_credentials", "refresh_token")
                   .scopes("select")
                   .authorities("client")
                   .secret("123456")
                   .and().withClient("client_2")
                   .resourceIds(DEMO_RESOURCE_ID)
                   .authorizedGrantTypes("password", "refresh_token")
                   .scopes("select")
                   .authorities("client")
                   .secret("123456");
       }

       @Override
       public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
           endpoints
                   .tokenStore(new RedisTokenStore(redisConnectionFactory))
                   .authenticationManager(authenticationManager);
       }

       @Override
       public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
           //允許表單認證
           oauthServer.allowFormAuthenticationForClients();
       }

   }

}

簡單說下spring security oauth2的認證思路。

  • client樣式,沒有用戶的概念,直接與認證服務器交互,用配置中的客戶端信息去申請accessToken,客戶端有自己的clientid,clientsecret對應於用戶的username,password,而客戶端也擁有自己的authorities,當採取client樣式認證時,對應的權限也就是客戶端自己的authorities。

  • password樣式,自己本身有一套用戶體系,在認證時需要帶上自己的用戶名和密碼,以及客戶端的clientid,clientsecret。此時,accessToken所包含的權限是用戶本身的權限,而不是客戶端的權限。

我對於兩種樣式的理解便是,如果你的系統已經有了一套用戶體系,每個用戶也有了一定的權限,可以採用password樣式;如果僅僅是接口的對接,不考慮用戶,則可以使用client樣式。

配置spring security

在spring security的版本迭代中,產生了多種配置方式,建造者樣式,配接器樣式等等設計樣式的使用,spring security內部的認證flow也是錯綜複雜,在我一開始學習ss也產生了不少困惑,總結了一下配置經驗:使用了springboot之後,spring security其實是有不少自動配置的,我們可以僅僅修改自己需要的那一部分,並且遵循一個原則,直接改寫最需要的那一部分。這一說法比較抽象,舉個例子。比如配置記憶體中的用戶認證器。有兩種配置方式

planA:

@Bean
protected UserDetailsService userDetailsService(){
   InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
   manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
   manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
   return manager;
}

planB:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.inMemoryAuthentication()
               .withUser("user_1").password("123456").authorities("USER")
               .and()
               .withUser("user_2").password("123456").authorities("USER");
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
      AuthenticationManager manager = super.authenticationManagerBean();
       return manager;
   }
}

你最終都能得到配置在記憶體中的兩個用戶,前者是直接替換掉了容器中的UserDetailsService,這麼做比較直觀;後者是替換了AuthenticationManager,當然你還會在SecurityConfiguration 複寫其他配置,這麼配置最終會由一個委托者去認證。如果你熟悉spring security,會知道AuthenticationManager和AuthenticationProvider以及UserDetailsService的關係,他們都是頂級的接口,實現類之間錯綜複雜的聚合關係…配置方式千差萬別,但理解清楚認證流程,知道各個實現類對應的職責才是掌握spring security的關鍵。

下麵給出我最終的配置:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

   @Bean
   @Override
   protected UserDetailsService userDetailsService(){
       InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
       manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
       manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
       return manager;
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       // @formatter:off
       http
           .requestMatchers().anyRequest()
           .and()
               .authorizeRequests()
               .antMatchers("/oauth/*").permitAll();
       // @formatter:on
   }
}

重點就是配置了一個UserDetailsService,和ClientDetailsService一樣,為了方便運行,使用記憶體中的用戶,實際專案中,一般使用的是資料庫儲存用戶,具體的實現類可以使用JdbcDaoImpl或者JdbcUserDetailsManager。

獲取token

進行如上配置之後,啟動springboot應用就可以發現多了一些自動創建的endpoints:

{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]
{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]}
{[/oauth/check_token]}
{[/oauth/error]}

重點關註一下/oauth/token,它是獲取的token的endpoint。啟動springboot應用之後,使用http工具訪問
password樣式:

http://localhost:8080/oauth/token?username=user_1&password;=123456&grant;_type=password&scope;=select&client;_id=client_2&client;_secret=123456

響應如下:
{"access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0","token_type":"bearer","refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb","expires_in":27036,"scope":"select"}

client樣式:
http://localhost:8080/oauth/token?grant_type=client_credentials&scope;=select&client;_id=client_1&client;_secret=123456

響應如下:
{"access_token":"56465b41-429d-436c-ad8d-613d476ff322","token_type":"bearer","expires_in":25074,"scope":"select"}

在配置中,我們已經配置了對order資源的保護,如果直接訪問:http://localhost:8080/order/1會得到這樣的響應:{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
(這樣的錯誤響應可以通過重寫配置來修改)

而對於未受保護的product資源http://localhost:8080/product/1則可以直接訪問,得到響應product id : 1

攜帶accessToken引數訪問受保護的資源:

使用password樣式獲得的token:http://localhost:8080/order/1?access_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0,得到了之前匿名訪問無法獲取的資源:order id : 1

使用client樣式獲得的token:http://localhost:8080/order/1?access_token=56465b41-429d-436c-ad8d-613d476ff322,同上的響應order id : 1

我們重點關註一下debug後,對資源訪問時系統記錄的用戶認證信息,可以看到如下的debug信息

password樣式:

client樣式:

和我們的配置是一致的,仔細看可以發現兩者的身份有些許的不同。想要查看更多的debug信息,可以選擇下載demo代碼自己查看,為了方便讀者除錯和驗證,我去除了很多複雜的特性,基本實現了一個最簡配置,涉及到資料庫的地方也儘量配置到了記憶體中,這點記住在實際使用時一定要修改。

到這兒,一個簡單的oauth2入門示例就完成了,一個簡單的配置教程。token的工作原理是什麼,它包含了哪些信息?spring內部如何對身份信息進行驗證?以及上述的配置到底影響了什麼?這些內容會放到後面的文章中去分析。

示例代碼下載

全部的代碼可以在我的github上進行下載,專案使用springboot+maven構建:
https://github.com/lexburner/oauth2-demo

赞(0)

分享創造快樂