一、OAuth 2 介紹
1、什麼是 OAuth 2?
- OAuth 是一個開放標準,該標準允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密資源(如頭像、照片、視頻等),而在這個過程中無須將用戶名和密碼提供給第三方應用。實現這一功能是通過提供一個令牌(token),而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。
- 每一個令牌授權一個特定的網站在特定的時段內訪問特定的資源。這樣,OAuth 讓用戶可以授權第三方網站靈活地訪問存儲在另外一些資源服務器的特定信息,而非所有內容。目前主流的 qq,微信等第三方授權登錄方式都是基於 OAuth2 實現的。
- OAuth 2 是 OAuth 協議的下一版本,但不向下兼容 OAuth 1.0。OAuth 2 關注客戶端開發者的簡易性,同時爲 Web 應用、桌面應用、移動設備、起居室設備提供專門的認證流程。
- 傳統的 Web 開發登錄認證一般都是基於 Session 的,但是在前後端分離的架構中繼續使用 Session 會有許多不便,因爲移動端(Android、iOS、微信小程序等)要麼不支持Cookie(微信小程序),要麼使用非常不便,對於這些問題,使用 OAuth 2 認證都能解決。
2、OAuth 2 角色
OAuth 2 標準中定義了以下幾種角色:
- 資源所有者(Resource Owner):即代表授權客戶端訪問本身資源信息的用戶,客戶端訪問用戶帳戶的權限僅限於用戶授權的“範圍”。
- 客戶端(Client):即代表意圖訪問受限資源的第三方應用。在訪問實現之前,它必須先經過用戶者授權,並且獲得的授權憑證將進一步由授權服務器進行驗證。
- 授權服務器(Authorization Server):授權服務器用來驗證用戶提供的信息是否正確,並返回一個令牌給第三方應用。
- 資源服務器(Resource Server):資源服務器是提供給用戶資源的服務器,例如頭像、照片、視頻等。
注意:一般來說,授權服務器和資源服務器可以是同一臺服務器。
3、OAuth 2 授權流程
下面是 OAuth 2 一個大致的授權流程圖:
- 步驟1:客戶端(第三方應用)向用戶請求授權。
- 步驟2:用戶單擊客戶端所呈現的服務授權頁面上的同意授權按鈕後,服務端返回一個授權許可憑證給客戶端。
- 步驟3:客戶端拿着授權許可憑證去授權服務器申請令牌。
- 步驟4:授權服務器驗證信息無誤後,發放令牌給客戶端。
- 步驟5:客戶端拿着令牌去資源服務器訪問資源。
- 步驟6:資源服務器驗證令牌無誤後開放資源。
4、OAuth 2 授權模式
OAuth 協議的授權模式共分爲 4 種,分別說明如下:
- 授權碼模式:授權碼模式(authorization code)是功能最完整、流程最嚴謹的授權模式。它的特點就是通過客戶端的服務器與授權服務器進行交互,國內常見的第三方平臺登錄功能基本 都是使用這種模式。
- 簡化模式:簡化模式不需要客戶端服務器參與,直接在瀏覽器中向授權服務器中請令牌,一般若網站是純靜態頁面,則可以採用這種方式。
- 密碼模式:密碼模式是用戶把用戶名密碼直接告訴客戶端,客戶端使用這些信息向授權服務器中請令牌。這需要用戶對客戶端高度信任,例如客戶端應用和服務提供商是同一家公司。
- 客戶端模式:客戶端模式是指客戶端使用自己的名義而不是用戶的名義向服務提供者申請授權。嚴格來說,客戶端模式並不能算作 OAuth 協議要解決的問題的一種解決方案,但是,對於開發者而言,在一些前後端分離應用或者爲移動端提供的認證授權服務器上使用這種模式還是非常方便的。
二、使用樣例
由於本樣例演示的是如何在前後端分離應用(或者爲移動端、微信小程序等)提供的認證服務器中如何搭建 OAuth 服務,因此主要介紹密碼模式。
1、添加依賴
由於 Spring Boot 中的 OAuth 協議是在 Spring Security 基礎上完成的。因此首先編輯 pom.xml,添加 Spring Security 以及 OAuth 依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
2、配置授權服務器
授權服務器和資源服務器可以是同一臺服務器,也可以是不同服務器,本案例中假設是同一臺服務器,通過不同的配置開啓授權服務器和資源服務器。
下面是授權服務器配置代碼。創建一個自定義類繼承自 AuthorizationServerConfigurerAdapter,完成對授權服務器的配置,然後通@EnableAuthorizationServer 註解開啓授權服務器:
注意:authorizedGrantTypes("password", "refresh_token") 表示 OAuth 2 中的授權模式爲“password”和“refresh_token”兩種。在標準的 OAuth 2 協議中,授權模式並不包括“refresh_token”,但是在 Spring Security 的實現中將其歸爲一種,因此如果需要實現 access_token 的刷新,就需要這樣一種授權模式。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 該對象用來支持 password 模式
@Autowired
AuthenticationManager authenticationManager;
// 該對象用來將令牌信息存儲到內存中
@Autowired(required = false)
TokenStore inMemoryTokenStore;
// 該對象將爲刷新token提供支持
@Autowired
UserDetailsService userDetailsService;
// 指定密碼的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用BCrypt強哈希函數加密方案(密鑰迭代次數默認爲10)
return new BCryptPasswordEncoder();
}
// 配置 password 授權模式
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password", "refresh_token") //授權模式爲password和refresh_token兩種
.accessTokenValiditySeconds(1800) // 配置access_token的過期時間
.resourceIds("rid") //配置資源id
.scopes("all")
.secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密後的密碼
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存儲(這裏存放在內存中)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 表示支持 client_id 和 client_secret 做登錄認證
security.allowFormAuthenticationForClients();
}
}
3、配置資源服務器
接下來配置資源服務器。自定義類繼承自 ResourceServerConfigurerAdapter,並添加 @EnableResourceServer 註解開啓資源服務器配置。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("rid") // 配置資源id,這裏的資源id和授權服務器中的資源id一致
.stateless(true); // 設置這些資源僅基於令牌認證
}
// 配置 URL 訪問權限
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}
4、配置 Security
這裏 Spring Security 的配置與傳統的 Security 大體相同,不同在於:
- 這裏多了兩個 Bean,這兩個 Bean 將注入授權服務器配置類中使用。
- 另外,這裏的 HttpSecurity 配置主要是配置“oauth/**”模式的 URL,這一類的請求直接放行。
注意:在這個 Spring Security 配置和上面的資源服務器配置中,都涉及到了 HttpSecurity。其中 Spring Security 中的配置優先級高於資源服務器中的配置,即請求地址先經過 Spring Security 的 HttpSecurity,再經過資源服務器的 HttpSecurity。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}
5、添加測試接口
接着在 Conctoller 中添加如下三個接口用於測試,它們分別需要 admin 角色、use 角色以及登錄後訪問。
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
6、開始測試
(1)啓動項目,首先通過 POST 請求獲取 token:
- 請求地址:oauth/token
- 請求參數:用戶名、密碼、授權模式、客戶端 id、scope、以及客戶端密碼
- 返回結果:access_token 表示獲取其它資源是要用的令牌,refresh_token 用來刷新令牌,expires_in 表示 access_token 過期時間。
(2)當 access_token 過期後,可以使用 refresh_token 重新獲取新的 access_token(前提是 access_token 未過期),這裏也是 POST 請求:
- 請求地址:oauth/token(不變)
- 請求參數:授權模式(變成了 refresh_token)、refresh_token、客戶端 id、以及客戶端密碼
- 返回結果:與獲取前面登錄獲取 token 返回的內容項一樣。不過每次請求,access_token 和 access_token有效期都會變化。
(3)訪問資源時,我們只需要攜帶上 access_token 參數即可:
(4)如果非法訪問一個資源,比如 admin 用戶訪問“user/hello”接口,結果如下:
附:將令牌保存到 Redis 緩存服務器上
在上面的樣例中,令牌(Access Token)是存儲在內存中,這種方式在單服務上可以體現出很好特效(即併發量不大,並且它在失敗的時候不會進行備份)
我們也可以將令牌保存到數據庫或者 Redis 緩存服務器上。使用這中方式,可以在多個服務之間實現令牌共享。下面我通過樣例演示如何將令牌存儲在 Redis 緩存服務器上,同時 Redis 具有過期等功能,很適合令牌的存儲。
1、增加依賴
(1)首先編輯 pom.xml 文件,在前面的基礎上增加 Redis 相關依賴:
<!-- spring security OAuth2 相關 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
(2)接着在 application.properties 中配置 Redis 連接信息:
# 基本連接信息配置
spring.redis.database=0
spring.redis.host=192.168.60.133
spring.redis.port=6379
spring.redis.password=123
# 連接池信息配置
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
2、修改授權服務器配置
最後我們修改授權服務器配置 AuthorizationServerConfig 中令牌保存相關代碼,將其改成保存到 Redis 上即可。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 該對象用來支持 password 模式
@Autowired
AuthenticationManager authenticationManager;
// 該對象用來將令牌信息存儲到Redis中
@Autowired
RedisConnectionFactory redisConnectionFactory;
// 該對象將爲刷新token提供支持
@Autowired
UserDetailsService userDetailsService;
// 指定密碼的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用BCrypt強哈希函數加密方案(密鑰迭代次數默認爲10)
return new BCryptPasswordEncoder();
}
// 配置 password 授權模式
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password", "refresh_token") //授權模式爲password和refresh_token兩種
.accessTokenValiditySeconds(1800) // 配置access_token的過期時間
.resourceIds("rid") //配置資源id
.scopes("all")
.secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密後的密碼
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) //配置令牌存放在Redis中
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 表示支持 client_id 和 client_secret 做登錄認證
security.allowFormAuthenticationForClients();
}
}
3、運行測試
啓動項目,再次通過 /oauth/token 接口獲取令牌。然後查看下 Redis 中的數據,可以發現令牌確實以及保存在 Redis 上了: