## 前言
今天來聊聊一個接口對接的場景,A廠家有一套HTTP接口需要提供給B廠家使用,由于是外網環境,所以需要有一套安全機制保障,這個時候oauth2就可以作為一個方案。
關于oauth2,其實是一個規范,本文重點講解spring對他進行的實現。
需要對spring security有一定的配置使用經驗,用戶認證這一塊,spring security oauth2建立在spring security的基礎之上。第一篇文章主要是講解使用springboot搭建一個簡易的授權,資源服務器。后續文章會進行spring security oauth2的相關源碼分析。Java中的安全框架如shrio,已經有跟我學shiro - 開濤,非常成體系地,深入淺出地講解了apache的這個開源安全框架,但是spring security包括oauth2一直沒有成體系的文章,學習它們大多依賴于較少的官方文檔,理解一下基本的使用配置;通過零散的博客,了解一下他人的使用經驗;打斷點,分析內部的工作流程;看源碼中的接口設計,以及注釋,了解設計者的用意。spring的各個框架都運用了很多的設計模式,在學習源碼的過程中,也大概了解了一些套路。spring也在必要的地方添加了適當的注釋,避免了源碼閱讀者對于一些細節設計的理解產生偏差,讓我更加感嘆,spring不僅僅是一個工具框架,更像是一個藝術品。
## 概述
使用oauth2保護你的應用,可以分為簡易的分為三個步驟
* 配置資源服務器
* 配置認證服務器
* 配置spring security
前兩點是oauth2的主體內容,但前面我已經描述過了,spring security oauth2是建立在spring security基礎之上的,所以有一些體系是公用的。
oauth2根據使用場景不同,分成了4種模式
* 授權碼模式(authorization code)
* 簡化模式(implicit)
* 密碼模式(resource owner password credentials)
* 客戶端模式(client credentials)
本文重點講解接口對接中常使用的密碼模式(以下簡稱password模式)和客戶端模式(以下簡稱client模式)。授權碼模式使用到了回調地址,是最為復雜的方式,通常網站中經常出現的微博,qq第三方登錄,都會采用這個形式。簡化模式不常用。
## 項目準備
主要的maven依賴如下
~~~
<!-- 注意是starter,自動配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 不是starter,手動配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 將token存儲在redis中 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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,客戶端有自己的client_id,client_secret對應于用戶的username,password,而客戶端也擁有自己的authorities,當采取client模式認證時,對應的權限也就是客戶端自己的authorities。
* password模式,自己本身有一套用戶體系,在認證時需要帶上自己的用戶名和密碼,以及客戶端的client_id,client_secret。此時,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`
## 總結
到這兒,一個簡單的oauth2入門示例就完成了,一個簡單的配置教程。token的工作原理是什么,它包含了哪些信息?spring內部如何對身份信息進行驗證?以及上述的配置到底影響了什么?這些內容會放到后面的文章中去分析。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux