Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
阿新 • • 發佈:2020-09-23
## **一. 前言**
本篇實戰案例基於 [youlai-mall](https://github.com/hxrui/youlai-mall) 專案。專案使用的是當前主流和最新版本的技術和解決方案,自己不會太多華麗的言辭去描述,只希望能勾起大家對程式設計的一點喜歡。所以有興趣的朋友可以進入 [github](https://github.com/hxrui/youlai-mall) | [碼雲](https://gitee.com/haoxr/youlai-mall)瞭解下專案明細 ,有興趣也可以一起研發。
微服務通過整合 Spirng Cloud Gateway、Spring Security OAuth2、JWT 實現微服務的統一認證授權。其中Spring Cloud Gateway作為OAuth2客戶端,其他微服務提供資源服務給閘道器,交由閘道器來做統一鑑權,所以這裡網關同樣也作為資源伺服器。
**溫馨提示**:微服務認證授權在整個系列算是比較有難度的,本篇同時從理論和實戰兩個角度出發,所以篇幅有些長,還需要往期文章搭建的環境基礎,希望大家可以耐心的研究下。
**往期系列文章**
1. [Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務 ](https://www.cnblogs.com/haoxianrui/p/13581881.html)
2. [Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心](https://www.cnblogs.com/haoxianrui/p/13584204.html)
3. [Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心](https://www.cnblogs.com/haoxianrui/p/13585125.html)
4. [Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API閘道器](https://www.cnblogs.com/haoxianrui/p/13608650.html)
5. [Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的呼叫](https://www.cnblogs.com/haoxianrui/p/13615592.html)
## **二. OAuth2和JWT概念及關係?**
### **1. 什麼是OAuth2?**
> OAuth 2.0 是目前最流行的授權機制,用來授權第三方應用,獲取使用者資料。
-- [【阮一峰】OAuth 2.0 的一個簡單解釋](http://www.ruanyifeng.com/blog/2019/04/oauth_design.html)
> QQ登入OAuth2.0:對於使用者相關的OpenAPI(例如獲取使用者資訊,動態同步,照片,日誌,分享等),為了保護使用者資料的安全和隱私,第三方網站訪問使用者資料前都需要顯式的向用戶徵求授權。 -- [【QQ登入】OAuth2.0開發文件](https://wiki.open.qq.com/wiki/%E3%80%90QQ%E7%99%BB%E5%BD%95%E3%80%91OAuth2.0%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3)
從上面定義可以理解OAuth2是一個授權協議,並且廣泛流行的應用。
下面通過“有道雲筆記”通過“QQ授權登入”的案例來分析QQ的OAuth2平臺的具體實現。
**流程分析:**
`
有道雲筆記客戶端 -> 選擇QQ授權登入 -> QQ認證授權成功返回access_token -> 有道雲筆記客戶端接收到access_token後進入有道雲筆記應用
`
流程關聯OAuth2的角色關聯如下:
```
(1)第三方應用程式(Third-party Application):案例中的"有道雲筆記"客戶端。
(2)HTTP服務提供商(HTTP Service):QQ
(3)資源所有者(Resource Owner):使用者
(4)使用者代理(User Agent): 比如瀏覽器,代替使用者去訪問這些資源。
(5)認證伺服器(Authorization Server):服務提供商專門用來處理認證的伺服器。案例中QQ提供的認證授權。
(6)資源伺服器(Resource server):即服務提供商存放使用者生成的資源的伺服器。它與認證伺服器,可以是同一臺伺服器,也可以是不同的伺服器。
這裡指客戶端拿到access_token要去訪問資源物件的伺服器,比如我們在有道雲裡的筆記。
```
### **2. 什麼是JWT?**
JWT(JSON Web Token)是令牌token的一個子集,首先在伺服器端身份認證通過後生成一個字串憑證並返回給客戶端,客戶端請求伺服器端時攜帶該token字串進行鑑權認證。
JWT是無狀態的。 除了包含簽名演算法、憑據過期時間之外,還可擴充套件新增額外資訊,比如使用者資訊等,所以無需將JWT儲存在伺服器端。相較於cookie/session機制中需要將使用者資訊儲存在伺服器端的session裡節省了記憶體開銷,使用者量越多越明顯。
JWT的結構如下:
![](https://i.loli.net/2020/09/19/3nfIPYcDQzm41lu.jpg)
看不明白沒關係,我先把[youlai-mall](https://github.com/hxrui/youlai-mall)認證通過後生成的access token(標準的JWT格式)放到[JWT官網](https://jwt.io/)進行解析成能看的定的格式。
![](https://i.loli.net/2020/09/19/8SuirOcdvGt3ACm.png)
JWT字串由Header(頭部)、Payload(負載)、Signature(簽名)三部分組成。
Header: JSON物件,用來描述JWT的元資料,alg屬性表示簽名的演算法,typ標識token的型別
Payload: JSON物件,用來存放實際需要傳遞的資料, 除了預設欄位,還可以在此自定義私有欄位
Signature: 對Header、Payload這兩部分進行簽名,簽名需要私鑰,為了防止資料被篡改
### **3. OAuth2和JWT關係?**
- OAuth2是一種認證授權的協議規範。
- JWT是基於token的安全認證協議的實現。
至於一定要給這二者沾點親帶點故的話。可以說OAuth2在認證成功生成的令牌access_token可以由JWT實現。
## **三. 認證伺服器**
認證伺服器落地 [youlai-mall](https://github.com/hxrui/youlai-mall) 的youlai-auth認證中心模組,完整程式碼地址: [github](https://github.com/hxrui/youlai-mall) | [碼雲](https://gitee.com/haoxr/youlai-mall)
### **1. pom依賴**
```
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.security
spring-security-oauth2-jose
```
### **2. 認證服務配置(AuthorizationServerConfig)**
``` java
/**
* 認證服務配置
*/
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private DataSource dataSource;
private AuthenticationManager authenticationManager;
private UserDetailsServiceImpl userDetailsService;
/**
* 客戶端資訊配置
*/
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
JdbcClientDetailsServiceImpl jdbcClientDetailsService = new JdbcClientDetailsServiceImpl(dataSource);
jdbcClientDetailsService.setFindClientDetailsSql(AuthConstants.FIND_CLIENT_DETAILS_SQL);
jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstants.SELECT_CLIENT_DETAILS_SQL);
clients.withClientDetails(jdbcClientDetailsService);
}
/**
* 配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService)
// refresh_token有兩種使用方式:重複使用(true)、非重複使用(false),預設為true
// 1.重複使用:access_token過期重新整理時, refresh token過期時間未改變,仍以初次生成的時間為準
// 2.非重複使用:access_token過期重新整理時, refresh_token過期時間延續,在refresh_token有效期內重新整理而無需失效再次登入
.reuseRefreshTokens(false);
}
/**
* 允許表單認證
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.allowFormAuthenticationForClients();
}
/**
* 使用非對稱加密演算法對token簽名
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 從classpath下的金鑰庫中獲取金鑰對(公鑰+私鑰)
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
new ClassPathResource("youlai.jks"), "123456".toCharArray());
KeyPair keyPair = factory.getKeyPair(
"youlai", "123456".toCharArray());
return keyPair;
}
/**
* JWT內容增強
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
Map map = new HashMap<>(2);
User user = (User) authentication.getUserAuthentication().getPrincipal();
map.put(AuthConstants.JWT_USER_ID_KEY, user.getId());
map.put(AuthConstants.JWT_CLIENT_ID_KEY, user.getClientId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
return accessToken;
};
}
}
```
AuthorizationServerConfig這個配置類是整個認證服務實現的核心。總結下來就是兩個關鍵點,客戶端資訊配置和access_token生成配置。
#### **2.1 客戶端資訊配置**
配置OAuth2認證允許接入的客戶端的資訊,因為接入OAuth2認證伺服器首先人家得認可你這個客戶端吧,就比如上面案例中的QQ的OAuth2認證伺服器認可“有道雲筆記”客戶端。
同理,我們需要把客戶端資訊配置在認證伺服器上來表示認證伺服器所認可的客戶端。一般可配置在認證伺服器的記憶體中,但是這樣很不方便管理擴充套件。所以實際最好配置在資料庫中的,提供視覺化介面對其進行管理,方便以後像PC端、APP端、小程式端等多端靈活接入。
Spring Security OAuth2官方提供的客戶端資訊表oauth_client_details
``` sql
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
```
新增一條客戶端資訊
``` sql
INSERT INTO `oauth_client_details` VALUES ('client', NULL, '123456', 'all', 'password,refresh_token', '', NULL, NULL, NULL, NULL, NULL);
```
![](https://i.loli.net/2020/09/20/8EisZPdSHLBWmYw.png)
#### **2.2 token生成配置**
專案使用JWT實現access_token,關於access_token生成步驟的配置如下:
**1. 生成金鑰庫**
使用JDK工具的keytool生成JKS金鑰庫(Java Key Store),並將youlai.jks放到resources目錄
`
keytool -genkey -alias youlai -keyalg RSA -keypass 123456 -keystore youlai.jks -storepass 123456
`
-genkey 生成金鑰
-alias 別名
-keyalg 金鑰演算法
-keypass 金鑰口令
-keystore 生成金鑰庫的儲存路徑和名稱
-storepass 金鑰庫口令
![](https://i.loli.net/2020/09/17/mMJLyHh1ix82AdE.png)
**2. JWT內容增強**
JWT負載資訊預設是固定的,如果想自定義新增一些額外資訊,需要實現TokenEnhancer的enhance方法將附加資訊新增到access_token中。
**3. JWT簽名**
JwtAccessTokenConverter是生成token的轉換器,可以實現指定token的生成方式(JWT)和對JWT進行簽名。
簽名實際上是生成一段標識(JWT的Signature部分)作為接收方驗證資訊是否被篡改的依據。原理部分請參考這篇的文章:[RSA加密、解密、簽名、驗籤的原理及方法](https://www.cnblogs.com/pcheng/p/9629621.html)
其中對JWT簽名有對稱和非對稱兩種方式:
> 對稱方式:認證伺服器和資源伺服器使用同一個金鑰進行加簽和驗籤 ,預設演算法HMAC
> 非對稱方式:認證伺服器使用私鑰加簽,資源伺服器使用公鑰驗籤,預設演算法RSA
非對稱方式相較於對稱方式更為安全,因為私鑰只有認證伺服器知道。
專案中使用RSA非對稱簽名方式,具體實現步驟如下:
(1). 從金鑰庫獲取金鑰對(金鑰+私鑰)
(2). 認證伺服器私鑰對token簽名
(3). 提供公鑰獲取介面供資源伺服器驗籤使用
**公鑰獲取介面**
```
/**
* RSA公鑰開放介面
*/
@RestController
@AllArgsConstructor
public class PublicKeyController {
private KeyPair keyPair;
@GetMapping("/rsa/publicKey")
public Map getKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
```
### **3. 安全配置(WebSecurityConfig)**
```
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.and()
.authorizeRequests().antMatchers("/rsa/publicKey").permitAll().anyRequest().authenticated()
.and()
.csrf().disable();
}
/**
* 如果不配置SpringBoot會自動配置一個AuthenticationManager,覆蓋掉記憶體中的使用者
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
```
安全配置主要是配置請求訪問許可權、定義認證管理器、密碼加密配置。
## **四. 資源伺服器**
資源伺服器落地 [youlai-mall](https://github.com/hxrui/youlai-mall) 的youlai-gateway微服務閘道器模組,完整程式碼地址: [github](https://github.com/hxrui/youlai-mall) | [碼雲](https://gitee.com/haoxr/youlai-mall)
上文有提到過閘道器這裡是擔任資源伺服器的角色,因為閘道器是微服務資源訪問的統一入口,所以在這裡做資源訪問的統一鑑權是再合適不過。
### **1. pom依賴**
``` xml
org.springframework.security
spring-security-oauth2-resource-server
org.springframework.security
spring-security-oauth2-jose
```
### **2. 配置檔案(youlai-gateway.yaml)**
``` yaml
spring:
security:
oauth2:
resourceserver:
jwt:
# 獲取JWT驗籤公鑰請求路徑
jwk-set-uri: 'http://localhost:9999/youlai-auth/rsa/publicKey'
redis:
database: 0
host: localhost
port: 6379
password:
cloud:
gateway:
discovery:
locator:
enabled: true # 啟用服務發現
lower-case-service-id: true
routes:
- id: youlai-auth
uri: lb://youlai-auth
predicates:
- Path=/youlai-auth/**
filters:
- StripPrefix=1
- id: youlai-admin
uri: lb://youlai-admin
predicates:
- Path=/youlai-admin/**
filters:
- StripPrefix=1
# 配置白名單路徑
white-list:
urls:
- "/youlai-auth/oauth/token"
- "/youlai-auth/rsa/publicKey"
```
### **3. 鑑權管理器**
鑑權管理器是作為資源伺服器驗證是否有權訪問資源的裁決者,核心部分的功能先已通過註釋形式進行說明,後面再對具體形式補充。
``` java
/**
* 鑑權管理器
*/
@Component
@AllArgsConstructor
@Slf4j
public class AuthorizationManager implements ReactiveAuthorizationManager {
private RedisTemplate redisTemplate;
private WhiteListConfig whiteListConfig;
@Override
public Mono check(Mono mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
String path = request.getURI().getPath();
PathMatcher pathMatcher = new AntPathMatcher();
// 白名單路徑直接放行
List whiteList = whiteListConfig.getUrls();
for (String ignoreUrl : whiteList) {
if (pathMatcher.match(ignoreUrl, path)) {
return Mono.just(new AuthorizationDecision(true));
}
}
// 對應跨域的預檢請求直接放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// token為空拒絕訪問
String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);
if (StrUtil.isBlank(token)) {
return Mono.just(new AuthorizationDecision(false));
}
// 快取取資源許可權角色關係列表
Map