前言
今天來聊聊一個介面對接的場景,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