1.OAuth2 概念
-
OAuth2 其實是一個關於授權的網絡標准,它制定了設計思路和運行流程,利用這個標准我們其實是可以自己實現 OAuth2 的認證過程的。

OAuth 2 有四種授權模式:
-
授權碼模式(authorization code)
-
簡化模式(implicit)
-
密碼模式(resource owner password credentials)
-
客戶端模式(client credentials)
具體 OAuth2 是什么,可以參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
2.什么情況下需要用 OAuth2
例子:
首先大家最熟悉的就是幾乎每個人都用過的,比如用微信登錄、用 QQ 登錄、用微博登錄、用 Google 賬號登錄、用 github 授權登錄等等,這些都是典型的 OAuth2 使用場景。假設我們做了一個自己的服務平台,如果不使用 OAuth2 登錄方式,那么我們需要用戶先完成注冊,然后用注冊號的賬號密碼或者用手機驗證碼登錄。而使用了 OAuth2 之后,相信很多人使用過、甚至開發過公眾號網頁服務、小程序,當我們進入網頁、小程序界面,第一次使用就無需注冊,直接使用微信授權登錄即可,大大提高了使用效率。因為每個人都有微信號,有了微信就可以馬上使用第三方服務,這體驗不要太好了。而對於我們的服務來說,我們也不需要存儲用戶的密碼,只要存儲認證平台返回的唯一ID 和用戶信息即可。
以上是使用了 OAuth2 的授權碼模式,利用第三方的權威平台實現用戶身份的認證。當然了,如果你的公司內部有很多個服務,可以專門提取出一個認證中心,這個認證中心就充當上面所說的權威認證平台的角色,所有的服務都要到這個認證中心做認證
這樣一說,發現沒,這其實就是個單點登錄的功能。這就是另外一種使用場景,對於多服務的平台,可以使用 OAuth2 實現服務的單點登錄,只做一次登錄,就可以在多個服務中自由穿行,當然僅限於授權范圍內的服務和接口。
3.具體使用
OAuth2 其實是一個關於授權的網絡標准,它制定了設計思路和運行流程,利用這個標准我們其實是可以自己實現 OAuth2 的認證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,其實是 Spring Cloud 按照 OAuth2 的標准並結合 spring-security 封裝好的一個具體實現。
3.1 系統架構說明

-
認證服務:OAuth2 主要實現端,Token 的生成、刷新、驗證都在認證中心完成。
-
后台服務: 接收到請求后會到認證中心驗證
-
前端:認證服務、后台服務之間的聯調
上圖描述了使用了 前端與OAuth2 認證服務、微服務間的請求過程。大致的過程就是前端用用戶名和密碼到后台服務登錄,成功后后台服務到認證服務端換取 token,返回給前端,前端拿着 token 去各個微服務請求數據接口,一般這個 token 是放到 header 中的。當微服務接到請求后,先要拿着 token 去認證服務端檢查 token 的合法性,如果合法,再根據用戶所屬的角色及具有的權限動態的返回數據
3.2 創建並配置認證服務端
配置最多的就是認證服務端,驗證賬號、密碼,存儲 token,檢查 token ,刷新 token 等都是認證服務端的工作。
3.2.1 引入需要的maven包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
之所以引入 redis 包,是因為下面會介紹一種用 redis 存儲 token 的方式。
3.2.2 配置好 application.yml
spring:
application:
name: auth-server
redis:
database: 2
host: localhost
port: 6379
server:
port: 6001
management:
endpoint:
health:
enabled: true
3.2.3 spring security 基礎配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 允許匿名訪問所有接口 主要是 oauth 接口
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll();
}
}
使用@EnableWebSecurity注解修飾,並繼承自WebSecurityConfigurerAdapter類。
這個類的重點就是聲明 PasswordEncoder 和 AuthenticationManager兩個 Bean。稍后會用到。其中 BCryptPasswordEncoder是一個密碼加密工具類,它可以實現不可逆的加密,AuthenticationManager是為了實現 OAuth2 的 password 模式必須要指定的授權管理 Bean。
3.2.4 實現 UserDetailsService
UserDetailsService的核心就是 loadUserByUsername方法,它要接收一個字符串參數,也就是傳過來的用戶名,返回一個 UserDetails對象。
3.2.5 OAuth2 配置文件
創建一個配置文件繼承自 AuthorizationServerConfigurerAdapter
有三個 configure 方法的重寫。
AuthorizationServerEndpointsConfigurer參數的重寫
endpoints.authenticationManager(authenticationManager)
.userDetailsService(kiteUserDetailsService)
.tokenStore(redisTokenStore);
authenticationManage() 調用此方法才能支持 password 模式。
userDetailsService() 設置用戶驗證服務。
tokenStore() 指定 token 的存儲方式。
redisTokenStore Bean 的定義如下:
@Configuration
public class RedisTokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}
}
ClientId、Client-Secret:這兩個參數對應請求端定義的 cleint-id 和 client-secret
authorizedGrantTypes 可以包括如下幾種設置中的一種或多種:
-
authorization_code:授權碼類型。
-
implicit:隱式授權類型。
-
password:資源所有者(即用戶)密碼類型。
-
client_credentials:客戶端憑據(客戶端ID以及Key)類型。
-
refresh_token:通過以上授權獲得的刷新令牌來獲取新的令牌。
accessTokenValiditySeconds:token 的有效期
scopes:用來限制客戶端訪問的權限,在換取的 token 的時候會帶上 scope 參數,只有在 scopes 定義內的,才可以正常換取 token。
上面代碼中是使用 inMemory 方式存儲的,將配置保存到內存中,相當於硬編碼了。正式環境下的做法是持久化到數據庫中,比如 mysql 中。(優化認證服務有實例)
3.3.6 創建數據庫SpringCloud、user表、實體User、UserRepository
實體bean
jpa接口
public interface UserRepository extends JpaRepository<User, String> {
user表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`oauth_password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN');
INSERT INTO `user` VALUES ('2', 'Custon', '$2a$10$D3PEtxvJ.N9Ko6osFaO4SO/jYcC8v7RHP34gZNk5THMvX7H5g8/NS', 'ROLE_ADMIN');
SET FOREIGN_KEY_CHECKS = 1;
3.2.6 啟動認證服務
完成之后,啟動項目,如果你用的是 IDEA 會在下方的 Mapping 窗口中看到 oauth2 相關的 RESTful 接口。

主要有如下幾個:
POST /oauth/authorize 授權碼模式認證授權接口
GET/POST /oauth/token 獲取 token 的接口
POST /oauth/check_token 檢查 token 合法性接口
3.3 創建用戶客戶端項目
上面創建完成了認證服務端,下面開始創建一個客戶端,對應到我們系統中的業務相關的微服務。我們假設這個微服務項目是管理用戶相關數據的,所以叫做用戶客戶端。
3.3.1 引用相關的 maven 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3.2 application.yml 配置文件
spring:
application:
name: client-user
redis:
database: 2
host: localhost
port: 6379
server:
port: 6101
servlet:
context-path: /client-user
security:
oauth2:
client:
client-id: user-client
client-secret: user-secret-8888
user-authorization-uri: http://localhost:6001/oauth/authorize
access-token-uri: http://localhost:6001/oauth/token
resource:
id: user-client
user-info-uri: user-info
authorization:
check-token-access: http://localhost:6001/oauth/check_token
上面是常規配置信息以及 redis 配置,重點是下面的 security 的配置,這里的配置稍有不注意就會出現 401 或者其他問題。
client-id、client-secret 要和認證服務中的配置一致,如果是使用 inMemory 還是 jdbc 方式。
user-authorization-uri 是授權碼認證方式需要的,下一篇文章再說。
access-token-uri 是密碼模式需要用到的獲取 token 的接口。
authorization.check-token-access 也是關鍵信息,當此服務端接收到來自客戶端端的請求后,需要拿着請求中的 token 到認證服務端做 token 驗證,就是請求的這個接口.
3.3.3 資源配置文件
在 OAuth2 的概念里,所有的接口都被稱為資源,接口的權限也就是資源的權限,所以 Spring Security OAuth2 中提供了關於資源的注解 @EnableResourceServer,和 @EnableWebSecurity的作用類似。
因為使用的是 redis 作為 token 的存儲,所以需要特殊配置一下叫做 tokenService 的 Bean,通過這個 Bean 才能實現 token 的驗證。
3.3.4 最后,添加一個 RESTful 接口
@Slf4j
@RestController
public class UserController {
@GetMapping(value = "get")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object get(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName();
}
}
一個 RESTful 方法,只有當訪問用戶具有 ROLE_ADMIN 權限時才能訪問,否則返回 401 未授權。
通過 Authentication 參數或者 SecurityContextHolder.getContext().getAuthentication() 可以拿到授權信息進行查看。
3.4 測試
3.4.1 獲取token
http://localhost:6001/oauth/token?username=2&password=123456&grant_type=password&scope=all&client_id=user-client&client_secret=user-secret-8888

3.4.3 校驗token

接口地址 http://localhost:6001/oauth/check_token?token=5f861834-9c6f-4424-af1d-df35fefddee3
正常返回結果:
{
"active": true,
"exp": 1597915851,
"user_name": "2",
"authorities": [
"ROLE_ADMIN"
],
"client_id": "user-client",
"scope": [
"all"
]
}
校驗失敗結果:
{
"error": "invalid_token",
"error_description": "Token was not recognised"
}
3.4.3 獲取refresh_token

grant_type: refresh_token
refresh_token: 從獲取token里面取出
3.4.2 客戶端攜帶token訪問接口

http://localhost:6101/client-user/get
返回結果: “2” (登錄username)
token到了過期時間,再次訪問,返回結果
{
"error": "invalid_token",
"error_description": "f7520be0-fb2c-4386-9ffc-e64977314b2f"
}
3.5 優化方案
3.5.1 認證服務OAuth2Config的configure(ClientDetailsServiceConfigurer clients) 換成數據庫存儲
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//添加客戶端信息
//使用內存存儲OAuth客服端信息
clients.inMemory()
// client_id 客戶單ID
.withClient("order-client")
// client_secret 客戶單秘鑰
.secret(passwordEncoder.encode("order-secret-8888"))
// 該客戶端允許的授權類型,不同的類型,則獲取token的方式不一樣
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
// token 有效期
.accessTokenValiditySeconds(3600)
// 允許的授權范圍
.scopes("all")
.and()
.withClient("user-client")
.secret(passwordEncoder.encode("user-secret-8888"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.accessTokenValiditySeconds(3600)
.scopes("all");
}
把OAuth2Config.java文件的configure(ClientDetailsServiceConfigurer clients)替換成下面的
@Autowired
private DataSource dataSource;
/**
* jdbc配置
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
jcsb.passwordEncoder(passwordEncoder);
}
在application.yml添加數據庫連接
#數據庫連接
datasource:
url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
-
在數據庫中增加表,並插入數據
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
注意: client_secret 字段不能直接是 secret 的原始值,需要經過加密。因為是用的 BCryptPasswordEncoder,所以最終插入的值應該是經過 BCryptPasswordEncoder.encode()之后的值。
3.6 JWT替換 redisToke
上面 token 的存儲用的是 redis 的方案,Spring Security OAuth2 還提供了 jdbc 和 jwt 的支持,jdbc 的暫不考慮,現在來介紹用 JWT 的方式來實現 token 的存儲。
用 JWT 的方式就不用把 token 再存儲到服務端了,JWT 有自己特殊的加密方式,可以有效的防止數據被篡改,只要不把用戶密碼等關鍵信息放到 JWT 里就可以保證安全性。
3.6.1 認證服務端改造
3.6.1.1 添加 JwtConfig 配置類
JwtAccessTokenConverter是為了做 JWT 數據轉換,這樣做是因為 JWT 有自身獨特的數據格式。如果沒有了解過 JWT ,可以搜索一下先了解一下。
3.6.1.2 更改 OAuthConfig 配置類
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/**
* 普通 jwt 模式
*/
endpoints.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.userDetailsService(kiteUserDetailsService)
/**
* 支持 password 模式
*/
.authenticationManager(authenticationManager);
}
注入 JWT 相關的 Bean,然后修改 configure(final AuthorizationServerEndpointsConfigurer endpoints) 方法為 JWT 存儲模式。
3.6.2 改造用戶客戶端
3.6.2.1 修改 application.yml 配置文件
security:
oauth2:
client:
client-id: user-client
client-secret: user-secret-8888
user-authorization-uri: http://localhost:6001/oauth/authorize
access-token-uri: http://localhost:6001/oauth/token
resource:
jwt:
key-uri: http://localhost:6001/oauth/token_key
key-value: dev
注意認證服務端 JwtAccessTokenConverter設置的 SigningKey 要和配置文件中的 key-value 相同,不然會導致無法正常解碼 JWT ,導致驗證不通過。
3.6.2.2 ResourceServerConfig 類的配置
3.6.3 測試
跟上面一樣(這里就不重復了)
3.6.4 增強 JWT
如果我想在 JWT 中加入額外的字段(比方說用戶的其他信息)怎么辦呢,當然可以。spring security oauth2 提供了 TokenEnhancer 增強器。其實不光 JWT ,RedisToken 的方式同樣可以。
3.6.4.1 OAuthConfig 配置類修改
聲明一個增強器
public class JWTokenEnhancer implements TokenEnhancer {
通過 oAuth2Authentication 可以拿到用戶名等信息,通過這些我們可以在這里查詢數據庫或者緩存獲取更多的信息,而這些信息都可以作為 JWT 擴展信息加入其中。
在JwtTokenConfig.java 注入增強器 TokenEnhancer
@Configuration
public class JwtTokenConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("dev");
return accessTokenConverter;
}
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTokenEnhancer();
}
}
OAuthConfig.java 修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)方法
@Override
public void configure( final AuthorizationServerEndpointsConfigurer endpoints ) throws Exception{
/**
* jwt 增強模式
*/
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add( jwtTokenEnhancer );
enhancerList.add( jwtAccessTokenConverter );
enhancerChain.setTokenEnhancers( enhancerList );
endpoints.tokenStore( jwtTokenStore )
.userDetailsService( kiteUserDetailsService )
/**
* 支持 password 模式
*/
.authenticationManager( authenticationManager )
.tokenEnhancer( enhancerChain )
.accessTokenConverter( jwtAccessTokenConverter );
}
3.6.4.2 測試
再次請求 token ,返回內容中多了個剛剛加入的 jwt-ext 字段
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImE0NTUxZDllLWI3ZWQtNDc1OS1iMmYxLWYwYjliMjFjYTQyYyIsImV4cCI6MTU3MTc3NzU3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmNTI3ODJlOS0wOGRjLTQ2NGUtYmJhYy03OTMwNzYwYmZiZjciLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.UQMf140CG8U0eWh08nGlctpIye9iJ7p2i6NYHkGAwhY",
"expires_in": 3599,
"scope": "all",
"jwt-ext": "JWT 擴展信息",
"jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c"
}
3.6.4 用戶客戶端解析 JWT 數據
我們如果在 JWT 中加入了額外信息,這些信息我們可能會用到,而在接收到 JWT 格式的 token 之后,用戶客戶端要把 JWT 解析出來。
引入 JWT 包
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
加一個 RESTful 接口,在其中解析 JWT
同樣注意其中簽名的設置要與認證服務端相同
測試
用上一步的 token 請求上面的接口
返回內容如下:
{
"user_name": "admin",
"jwt-ext": "JWT 擴展信息",
"scope": [
"all"
],
"exp": 1571745178,
"authorities": [
"ROLE_ADMIN"
],
"jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c",
"client_id": "user-client"
}
