可能是全網最詳細的 Spring Cloud OAuth2 單點登入使用教程了

古時的風箏發表於2020-03-18

微信搜尋公眾號「古時的風箏」,一個不只有技術的技術公眾號。 Spring Cloud 系列文章已經完成,可以到 我的github 上檢視系列完整內容。

另外,還有本篇的姊妹篇 可能是全網最詳細的 Spring Cloud OAuth2 授權碼模式使用教程了,微信登入就是這個原理 ,這是兩種最常用的模式,大家可以一起學習。

OAuth 2 有四種授權模式,分別是授權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password credentials)、客戶端模式(client credentials),具體 OAuth2 是什麼,可以參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)

本文我們將使用授權碼模式和密碼模式兩種方式來實現使用者認證和授權管理。

OAuth2 其實是一個關於授權的網路標準,它制定了設計思路和執行流程,利用這個標準我們其實是可以自己實現 OAuth2 的認證過程的。今天要介紹的 spring-cloud-starter-oauth2 ,其實是 Spring Cloud 按照 OAuth2 的標準並結合 spring-security 封裝好的一個具體實現。

什麼情況下需要用 OAuth2

首先大家最熟悉的就是幾乎每個人都用過的,比如用微信登入、用 QQ 登入、用微博登入、用 Google 賬號登入、用 github 授權登入等等,這些都是典型的 OAuth2 使用場景。假設我們做了一個自己的服務平臺,如果不使用 OAuth2 登入方式,那麼我們需要使用者先完成註冊,然後用註冊號的賬號密碼或者用手機驗證碼登入。而使用了 OAuth2 之後,相信很多人使用過、甚至開發過公眾號網頁服務、小程式,當我們進入網頁、小程式介面,第一次使用就無需註冊,直接使用微信授權登入即可,大大提高了使用效率。因為每個人都有微訊號,有了微信就可以馬上使用第三方服務,這體驗不要太好了。而對於我們的服務來說,我們也不需要儲存使用者的密碼,只要儲存認證平臺返回的唯一ID 和使用者資訊即可。

以上是使用了 OAuth2 的授權碼模式,利用第三方的權威平臺實現使用者身份的認證。當然了,如果你的公司內部有很多個服務,可以專門提取出一個認證中心,這個認證中心就充當上面所說的權威認證平臺的角色,所有的服務都要到這個認證中心做認證。

這樣一說,發現沒,這其實就是個單點登入的功能。這就是另外一種使用場景,對於多服務的平臺,可以使用 OAuth2 實現服務的單點登入,只做一次登入,就可以在多個服務中自由穿行,當然僅限於授權範圍內的服務和介面。

實現統一認證功能

本篇先介紹密碼模式實現的單點登入,下一篇再繼續說授權碼模式。

在微服務橫行的今天,誰敢說自己手上沒幾個微服務。微服務減少了服務間的耦合,同時也在某些方面增加了系統的複雜度,比如說使用者認證。假設我們這裡實現了一個電商平臺,使用者看到的就是一個 APP 或者一個 web 站點,實際上背後是由多個獨立的服務構成的,比如使用者服務、訂單服務、產品服務等。使用者只要第一次輸入使用者名稱、密碼完成登入後,一段時間內,都可以任意訪問各個頁面,比如產品列表頁面、我的訂單頁面、我的關注等頁面。

我們可以想象一下,自然能夠想到,在請求各個服務、各個介面的時候,一定攜帶著什麼憑證,然後各個服務才知道請求介面的使用者是哪個,不然肯定有問題,那其實這裡面的憑證簡單來說就是一個 Token,標識使用者身份的 Token。

系統架構說明

認證中心:oauth2-auth-server,OAuth2 主要實現端,Token 的生成、重新整理、驗證都在認證中心完成。

訂單服務:oauth2-client-order-server,微服務之一,接收到請求後會到認證中心驗證。

使用者服務:oauth2-client-user-server,微服務之二,接收到請求後會到認證中心驗證。

客戶端:例如 APP 端、web 端 等終端

上圖描述了使用了 OAuth2 的客戶端與微服務間的請求過程。大致的過程就是客戶端用使用者名稱和密碼到認證服務端換取 token,返回給客戶端,客戶端拿著 token 去各個微服務請求資料介面,一般這個 token 是放到 header 中的。當微服務接到請求後,先要拿著 token 去認證服務端檢查 token 的合法性,如果合法,再根據使用者所屬的角色及具有的許可權動態的返回資料。

建立並配置認證服務端

配置最多的就是認證服務端,驗證賬號、密碼,儲存 token,檢查 token ,重新整理 token 等都是認證服務端的工作。

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>
複製程式碼

spring-cloud-starter-oauth2包含了 spring-cloud-starter-security,所以不用再單獨引入了。之所以引入 redis 包,是因為下面會介紹一種用 redis 儲存 token 的方式。

2、配置好 application.yml

將專案基本配置設定好,並加入有關 redis 的配置,稍後會用到。

spring:
application:
name: auth-server
redis:
database: 2
host: localhost
port: 32768
password: 1qaz@WSX
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
timeout: 100ms

server:
port: 6001

management:
endpoint:
health:
enabled: true
複製程式碼

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類。

這個類的重點就是宣告 PasswordEncoderAuthenticationManager兩個 Bean。稍後會用到。其中 BCryptPasswordEncoder是一個密碼加密工具類,它可以實現不可逆的加密,AuthenticationManager是為了實現 OAuth2 的 password 模式必須要指定的授權管理 Bean。

4、實現 UserDetailsService

如果你之前用過 Security 的話,那肯定對這個類很熟悉,它是實現使用者身份驗證的一種方式,也是最簡單方便的一種。另外還有結合 AuthenticationProvider的方式,有機會講 Security 的時候再展開來講吧。

UserDetailsService的核心就是 loadUserByUsername方法,它要接收一個字串引數,也就是傳過來的使用者名稱,返回一個 UserDetails物件。

@Slf4j
@Component(value = "kiteUserDetailsService")
public class KiteUserDetailsService implements UserDetailsService {


@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("usernameis:" + username);
// 查詢資料庫操作
if(!username.equals("admin")){
throw new UsernameNotFoundException("the user is not found");
}else{
// 使用者角色也應在資料庫中獲取
String role = "ROLE_ADMIN";
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(role));
// 線上環境應該通過使用者名稱查詢資料庫獲取加密後的密碼
String password = passwordEncoder.encode("123456");
return new org.springframework.security.core.userdetails.User(username,password, authorities);
}
}
}
複製程式碼

這裡為了做演示,把使用者名稱、密碼和所屬角色都寫在程式碼裡了,正式環境中,這裡應該是從資料庫或者其他地方根據使用者名稱將加密後的密碼及所屬角色查出來的。賬號 admin ,密碼 123456,稍後在換取 token 的時候會用到。並且給這個使用者設定 "ROLE_ADMIN" 角色。

5、OAuth2 配置檔案

建立一個配置檔案繼承自 AuthorizationServerConfigurerAdapter.

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

@Autowired
public PasswordEncoder passwordEncoder;

@Autowired
public UserDetailsService kiteUserDetailsService;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private TokenStore redisTokenStore;

@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/**
* redis token 方式
*/

endpoints.authenticationManager(authenticationManager)
.userDetailsService(kiteUserDetailsService)
.tokenStore(redisTokenStore);

}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("order-client")
.secret(passwordEncoder.encode("order-secret-8888"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.accessTokenValiditySeconds(3600)
.scopes("all")
.and()
.withClient("user-client")
.secret(passwordEncoder.encode("user-secret-8888"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.accessTokenValiditySeconds(3600)
.scopes("all");
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");
}
}
複製程式碼

有三個 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);
}
}
複製程式碼

ClientDetailsServiceConfigurer引數的重寫,在這裡定義各個端的約束條件。包括

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 中。

具體的做法如下:

  1. 在資料庫中增加表,並插入資料
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()之後的值。

  1. 然後在配置檔案 application.yml 中新增關於資料庫的配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false
username: root
password: password
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 9
複製程式碼

Spring Boot 2.0 之後預設使用 hikari 作為資料庫連線池。如果使用其他連線池需要引入相關包,然後對應的增加配置。

  1. 在 OAuth2 配置類(OAuth2Config)中增加 DataSource 的注入
@Autowired
private DataSource dataSource;
複製程式碼
  1. public void configure(ClientDetailsServiceConfigurer clients)重寫方法修改為如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
jcsb.passwordEncoder(passwordEncoder);
}
複製程式碼

還有一個重寫的方法 public void configure(AuthorizationServerSecurityConfigurer security),這個方法限制客戶端訪問認證介面的許可權。

security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");
複製程式碼

第一行程式碼是允許客戶端訪問 OAuth2 授權介面,否則請求 token 會返回 401。

第二行和第三行分別是允許已授權使用者訪問 checkToken 介面和獲取 token 介面。

完成之後,啟動專案,如果你用的是 IDEA 會在下方的 Mapping 視窗中看到 oauth2 相關的 RESTful 介面。

主要有如下幾個:

POST /oauth/authorize  授權碼模式認證授權介面
GET/POST /oauth/token 獲取 token 的介面
POST /oauth/check_token 檢查 token 合法性介面
複製程式碼

建立使用者客戶端專案

上面建立完成了認證服務端,下面開始建立一個客戶端,對應到我們系統中的業務相關的微服務。我們假設這個微服務專案是管理使用者相關資料的,所以叫做使用者客戶端。

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>
複製程式碼

2、application.yml 配置檔案

spring:
application:
name: client-user
redis:
database: 2
host: localhost
port: 32768
password: 1qaz@WSX
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
timeout: 100ms
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、資源配置檔案

在 OAuth2 的概念裡,所有的介面都被稱為資源,介面的許可權也就是資源的許可權,所以 Spring Security OAuth2 中提供了關於資源的註解 @EnableResourceServer,和 @EnableWebSecurity的作用類似。

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Value("${security.oauth2.client.client-id}")
private String clientId;

@Value("${security.oauth2.client.client-secret}")
private String secret;

@Value("${security.oauth2.authorization.check-token-access}")
private String checkTokenEndpointUrl;

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}

@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setClientId(clientId);
tokenService.setClientSecret(secret);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
return tokenService;
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenService());
}
}
複製程式碼

因為使用的是 redis 作為 token 的儲存,所以需要特殊配置一下叫做 tokenService 的 Bean,通過這個 Bean 才能實現 token 的驗證。

4、最後,新增一個 RESTful 介面

@Slf4j
@RestController
public class UserController {

@GetMapping(value = "get")
//@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object get(Authentication authentication){
//Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.getCredentials();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
String token = details.getTokenValue();
return token;
}
}
複製程式碼

一個 RESTful 方法,只有當訪問使用者具有 ROLE_ADMIN 許可權時才能訪問,否則返回 401 未授權。

通過 Authentication 引數或者 SecurityContextHolder.getContext().getAuthentication() 可以拿到授權資訊進行檢視。

測試認證功能

1、啟動認證服務端,啟動埠為 6001

2、啟動使用者服務客戶端,啟動埠為6101

3、請求認證服務端獲取 token

我是用 REST Client 來做訪問請求的,請求格式如下:

POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
複製程式碼

假設我們們在一個 web 端使用,grant_type 是 password,表明這是使用 OAuth2 的密碼模式。

username=admin 和 password=123456 就相當於在 web 端登入介面輸入的使用者名稱和密碼,我們在認證服務端配置中固定了使用者名稱是 admin 、密碼是 123456,而線上環境中則應該通過查詢資料庫獲取。

scope=all 是許可權有關的,在認證服務的 OAuthConfig 中指定了 scope 為 all 。

Authorization 要加在請求頭中,格式為 Basic 空格 base64(clientId:clientSecret),這個微服務客戶端的 client-id 是 user-client,client-secret 是 user-secret-8888,將這兩個值通過冒號連線,並使用 base64 編碼(user-client:user-secret-8888)之後的值為 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,可以通過 https://www.sojson.com/base64.html 線上編碼獲取。

執行請求後,如果引數都正確的話,獲取到的返回內容如下,是一段 json 格式

{
"access_token": "9f958300-5005-46ea-9061-323c9e6c7a4d",
"token_type": "bearer",
"refresh_token": "0f5871f5-98f1-405e-848e-80f641bab72e",
"expires_in": 3599,
"scope": "all"
}
複製程式碼

access_token :  就是之後請求需要帶上的 token,也是本次請求的主要目的 token_type:為 bearer,這是 access token 最常用的一種形式 refresh_token:之後可以用這個值來換取新的 token,而不用輸入賬號密碼 expires_in:token 的過期時間(秒)

4、用獲取到的 token 請求資源介面

我們在使用者客戶端中定義了一個介面 http://localhost:6101/client-user/get,現在就拿著上一步獲取的 token 來請求這個介面。

GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ce334918-e666-455a-8ecd-8bd680415d84
複製程式碼

同樣需要請求頭 Authorization,格式為 bearer + 空格 + token,正常情況下根據介面的邏輯,會把 token 原樣返回。

5、token 過期後,用 refresh_token 換取 access_token

一般都會設定 access_token 的過期時間小於 refresh_token 的過期時間,以便在 access_token 過期後,不用使用者再次登入的情況下,獲取新的 access_token。

### 換取 access_token
POST http://localhost:6001/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
複製程式碼

grant_type 設定為 refresh_token。

refresh_token 設定為請求 token 時返回的 refresh_token 的值。

請求頭加入 Authorization,格式依然是 Basic + 空格 + base64(client-id:client-secret)

請求成功後會返回和請求 token 同樣的資料格式。

用 JWT 替換 redisToken

上面 token 的儲存用的是 redis 的方案,Spring Security OAuth2 還提供了 jdbc 和 jwt 的支援,jdbc 的暫不考慮,現在來介紹用 JWT 的方式來實現 token 的儲存。

用 JWT 的方式就不用把 token 再儲存到服務端了,JWT 有自己特殊的加密方式,可以有效的防止資料被篡改,只要不把使用者密碼等關鍵資訊放到 JWT 裡就可以保證安全性。

認證服務端改造

先把有關 redis 的配置去掉。

新增 JwtConfig 配置類
@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;
}
}
複製程式碼

JwtAccessTokenConverter是為了做 JWT 資料轉換,這樣做是因為 JWT 有自身獨特的資料格式。如果沒有了解過 JWT ,可以搜尋一下先了解一下。

更改 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 儲存模式。

改造使用者客戶端

修改 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 ,導致驗證不通過。

ResourceServerConfig 類的配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

accessTokenConverter.setSigningKey("dev");
accessTokenConverter.setVerifierKey("dev");
return accessTokenConverter;
}

@Autowired
private TokenStore jwtTokenStore;

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(jwtTokenStore);
}
}
複製程式碼
執行請求 token 介面的請求
POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
複製程式碼

返回結果如下:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJleHAiOjE1NzE3NzU4OTQsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjdkMjg4NDUtMmU2ZC00ZmRjLTg1OGYtMWNiY2RlNzI1ZmMyIiwiY2xpZW50X2lkIjoidXNlci1jbGllbnQifQ.vk_msYtbrAr93h5sK4wy6EC2_wRD_cD_UBS8O6eRziw",
"expires_in": 3599,
"scope": "all",
"jti": "8cca29af-ea77-4fe6-9fe1-327415dcd21d"
}
複製程式碼

我們已經看到返回的 token 是 JWT 格式了,到 JWT 線上解碼網站 jwt.io/ 或者 jwt.calebb.net/將 token 解碼看一下

看到了沒,user_name、client_id 等資訊都在其中。

拿著返回的 token 請求使用者客戶端介面
GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM
複製程式碼

增強 JWT

如果我想在 JWT 中加入額外的欄位(比方說使用者的其他資訊)怎麼辦呢,當然可以。spring security oauth2 提供了 TokenEnhancer 增強器。其實不光 JWT ,RedisToken 的方式同樣可以。

宣告一個增強器
public class JWTokenEnhancer implements TokenEnhancer {

@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
info.put("jwt-ext", "JWT 擴充套件資訊");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
複製程式碼

通過 oAuth2Authentication 可以拿到使用者名稱等資訊,通過這些我們可以在這裡查詢資料庫或者快取獲取更多的資訊,而這些資訊都可以作為 JWT 擴充套件資訊加入其中。

OAuthConfig 配置類修改

注入增強器

@Autowired
private TokenEnhancer jwtTokenEnhancer;

@Bean
public TokenEnhancer jwtTokenEnhancer(){
return new JWTokenEnhancer();
}
複製程式碼

修改 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 );
}
複製程式碼
再次請求 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"
}
複製程式碼

使用者客戶端解析 JWT 資料

我們如果在 JWT 中加入了額外資訊,這些資訊我們可能會用到,而在接收到 JWT 格式的 token 之後,使用者客戶端要把 JWT 解析出來。

引入 JWT 包
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
複製程式碼
加一個 RESTful 介面,在其中解析 JWT
@GetMapping(value = "jwt")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object jwtParser(Authentication authentication){
authentication.getCredentials();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
String jwtToken = details.getTokenValue();
Claims claims = Jwts.parser()
.setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(jwtToken)
.getBody();
return claims;
}
複製程式碼

同樣注意其中籤名的設定要與認證服務端相同。

用上一步的 token 請求上面的介面
### 解析 jwt
GET http://localhost:6101/client-user/jwt
Accept: */*
Cache-Control: no-cache
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ
複製程式碼

返回內容如下:

{
"user_name": "admin",
"jwt-ext": "JWT 擴充套件資訊",
"scope": [
"all"
],
"exp": 1571745178,
"authorities": [
"ROLE_ADMIN"
],
"jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c",
"client_id": "user-client"
}
複製程式碼

以上就是 password 模式的完整過程,原始碼放到了 github 上,有需要的可以去看一下。

創作不易,點贊是美德,還能給我創作的動力。不用客氣了,讚我!

微信搜尋公眾號「古時的風箏」,也可以直接掃下面二維碼。關注之後可加微信,與群裡小夥伴交流學習,另有阿里等大廠同學可以直接內推。

本文對應的原始碼:請點這裡檢視


可能是全網最詳細的 Spring Cloud OAuth2 單點登入使用教程了

相關文章