可能是全網最詳細的 Spring Cloud OAuth2 授權碼模式使用教程了,微信登入就是這個原理

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

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

上一篇文章可能是全網最詳細的 Spring Cloud OAuth2 單點登入使用教程了其實只介紹了使用 password 模式進行身份認證和單點登入。

本篇繼續來講 Spring Cloud OAuth2 的另外一種授權模式,那就是 授權碼模式,如果你整合過微信、微博登入的功能,那一定對這個模式有一定的瞭解。接下來,跟著本教程,保證你可以更加透徹的瞭解授權碼模式到底是怎麼一會事,而且可以順利的應用到你的專案中。

授權碼模式認證過程

授權碼模式的認證過程是這樣的:

1、使用者客戶端請求認證伺服器的認證介面,並附上回撥地址;

2、認證服務介面接收到認證請求後調整到自身的登入介面;

3、使用者輸入使用者名稱和密碼,點選確認,跳轉到授權、拒絕提示頁面(也可省略);

4、使用者點選授權或者預設授權後,跳轉到微服務客戶端的回撥地址,並傳入引數 code;

5、回撥地址一般是一個 RESTful 介面,此介面拿到 code 引數後,再次請求認證伺服器的 token 獲取介面,用來換取 access_token 等資訊;

6、獲取到 access_token 後,拿著 token 去請求各個微服務客戶端的介面。

注意上面所說的使用者客戶端可以理解為瀏覽器、app 端,微服務客戶端就是我們系統中的例如訂單服務、使用者服務等微服務,認證服務端就是用來做認證授權的服務,相對於認證服務端來說,各個業務微服務也可以稱作是它的客戶端。

認證服務端配置

認證服務端繼續用上一篇文章的配置,程式碼不需要任何改變,只需要在資料庫里加一條記錄,來支援新加的微服務客戶端的認證

我們要建立的客戶端的 client-id 為 code-client,client-secret 為 code-secret-8888,但是同樣需要加密,可以用如下程式碼獲取:

System.out.println(new BCryptPasswordEncoder().encode("code-secret-8888"));
複製程式碼

除了以上這兩個引數,要將 authorized_grant_types 設定為 authorization_code,refresh_token,web_server_redirect_uri 設定為回撥地址,稍後微服務客戶端會建立這個介面。

然後將這條記錄組織好插入資料庫中。

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
('code-client', '$2a$10$jENDQZRtqqdr6sXGQK.L0OBADGIpyhtaRfaRDTeLKI76I/Ir1FDn6', 'all',
'authorization_code,refresh_token', 'http://localhost:6102/client-authcode/login', null, 3600, 36000, null, true);
複製程式碼

建立授權模式的微服務

引入 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>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
複製程式碼

引入 okhttp 和 thymeleaf 是因為要做一個簡單的頁面並模擬正常的認證過程。

配置檔案 application.yml

spring:
application:
name: client-authcode
server:
port: 6102
servlet:
context-path: /client-authcode


security:
oauth2:
client:
client-id: code-client
client-secret: code-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
authorization:
check-token-access: http://localhost:6001/oauth/check_token
複製程式碼

**建立 resourceConfig **

@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);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login").permitAll();
}
}
複製程式碼

使用 jwt 作為 token 的儲存,注意允許 /login 介面無授權訪問,這個地址是認證的回撥地址,會返回 code 引數。

建立 application.java啟動類

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製程式碼

到這步可以先停一下了。我們把認證服務端和剛剛建立的認證客戶端啟動起來,就可以手工測試一下了。回撥介面不是還沒建立呢嗎,沒關係,我們權當那個地址現在就是為了接收 code 引數的。 **1、**在瀏覽器訪問 /oauth/authorize 授權介面,介面地址為:

http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login 
複製程式碼

注意 response_type 引數設定為 code,redirect_uri 設定為資料庫中插入的回撥地址。

**2、**輸入上面地址後,會自動跳轉到認證服務端的登入頁面,輸入使用者名稱、密碼,這裡使用者名稱是 admin,密碼是 123456

**3、**點選確定後,來到授權確認頁面,頁面上有 Authorize 和 Deny (授權和拒絕)兩個按鈕。可通過將 autoapprove 欄位設定為 0 來取消此頁面的展示,預設直接同意授權。

**4、**點選同意授權後,跳轉到了回撥地址,雖然是 404 ,但是我們只是為了拿到 code 引數,注意地址後面的 code 引數。

**5、**拿到這個 code 引數是為了向認證伺服器 /oauth/token 介面請求 access_token ,繼續用 REST Client 傳送請求,同樣的,你也可以用 postman 等工具測試。

注意 grant_type 引數設定為 authorization_code,code 就是上一步回撥地址中加上的,redirect_uri 仍然要帶上,回作為驗證條件,如果不帶或者與前面設定的不一致,會出現錯誤。

請求頭 Authorization ,仍然是 Basic + 空格 + base64(client_id:client_secret),可以通過 https://www.sojson.com/base64.html 網站線上做 base64 編碼。

code-client:code-secret-8888 通過 base64 編碼後結果為 Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==

POST http://localhost:6001/oauth/token?grant_type=authorization_code&client=code-client&code=BbCE34&redirect_uri=http://localhost:6102/client-authcode/login
Accept: */*
Cache-Control: no-cache
Authorization: Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==
複製程式碼

傳送請求後,返回的 json 內容如下:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MjYwMTMzMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2OWRmY2M4Yy1iZmZiLTRiNDItYTZhZi1hN2IzZWUyZjI1ZTMiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.WlgGnBkNdg2PwKqjbZWo6QmUmq0QluZLgIWJXaZahSU",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjY5ZGZjYzhjLWJmZmItNGI0Mi1hNmFmLWE3YjNlZTJmMjVlMyIsImV4cCI6MTU3MjYzMzczMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkNzk2OWRhMS04NTg4LTQ2YzMtYjdlNS1jMGM5NzcxNTM5Y2YiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.TEz0pQOhST9-ozdoJWm6cf1SoWvPC6W-5JW9yjZJXek",
"expires_in": 3599,
"scope": "all",
"jwt-ext": "JWT 擴充套件資訊",
"jti": "69dfcc8c-bffb-4b42-a6af-a7b3ee2f25e3"
}
複製程式碼

和上一篇文章 password 模式拿到的 token 內容是一致的,接下來的請求都需要帶上 access_token 。

**6、**把獲取到的 access_token 代入到下面的請求中 ${access_token} 的位置,就可以請求微服務中的需要授權訪問的介面了。

GET http://localhost:6102/client-authcode/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ${access_token}
複製程式碼

介面內容如下:

@org.springframework.web.bind.annotation.ResponseBody
@GetMapping(value = "get")
@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;
}
複製程式碼

經過以上的手工測試,證明此過程是通的,但是還沒有達到自動化。如果你整合過微信登入,那你一定知道我們在回撥地址中做了什麼,拿到返回的 code 引數去 token 介面換取 access_token 對不對,沒錯,思路都是一樣的,我們的回撥介面中同樣要拿 code 去換取 access_token。

為此,我做了一個簡單的頁面,並且在回撥介面中請求獲取 token 的介面。

建立簡單的登入頁面

在 resources 目錄下建立 templates 目錄,用來存放 thymeleaf 的模板,不做樣式,只做最簡單的演示,建立 index.html 模板,內容如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>古時的風箏-OAuth2 Client</title>
</head>
<body>
<div>
<a href="http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login">登入</a>
<span th:text="'當前認證使用者:' + ${username}"></span>
<span th:text="${accessToken}"></span>
</div>
</body>
</html>
複製程式碼

回撥介面及其他介面

@Slf4j
@Controller
public class CodeClientController {

/**
* 用來展示index.html 模板
* @return
*/

@GetMapping(value = "index")
public String index(){
return "index";
}

@GetMapping(value = "login")
public Object login(String code,Model model) {
String tokenUrl = "http://localhost:6001/oauth/token";
OkHttpClient httpClient = new OkHttpClient();
RequestBody body = new FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client", "code-client")
.add("redirect_uri","http://localhost:6102/client-authcode/login")
.add("code", code)
.build();

Request request = new Request.Builder()
.url(tokenUrl)
.post(body)
.addHeader("Authorization", "Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==")
.build();
try {
Response response = httpClient.newCall(request).execute();
String result = response.body().string();
ObjectMapper objectMapper = new ObjectMapper();
Map tokenMap = objectMapper.readValue(result,Map.class);
String accessToken = tokenMap.get("access_token").toString();
Claims claims = Jwts.parser()
.setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(accessToken)
.getBody();
String userName = claims.get("user_name").toString();
model.addAttribute("username", userName);
model.addAttribute("accessToken", result);
return "index";
} catch (Exception e) {
e.printStackTrace();
}
return null;
}


@org.springframework.web.bind.annotation.ResponseBody
@GetMapping(value = "get")
@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;
}
}
複製程式碼

其中 index() 方法是為了展示 thymeleaf 模板,login 方法就是回撥介面,這裡用了 okhttp3 用作介面請求,請求認證服務端的 /oauth/token 介面來換取 access_token,只是把我們手工測試的步驟自動化了。

訪問 index.html 頁面

我們假設這個頁面就是一個網站的首頁,未登入的使用者會在網站上看到登入按鈕,我們訪問這個頁面:http://localhost:6102/client-authcode/index,看到的頁面是這樣的

接下來,點選登入按鈕,通過上面的模板程式碼看出,點選後其實就是跳轉到了我們手工測試第一步訪問的那個地址,之後的操作和上面手工測試的是一致的,輸入使用者名稱密碼、點選同意授權。

接下來,頁面跳轉回回撥地址<http://localhost:6102/client-authcode/login?code=xxx 的時候,login 方法拿到 code 引數,開始構造 post 請求體,並把 Authorization 加入請求頭,然後請求 oauth/token 介面,最後將拿到的 token 和 通過 token 解析後的 username 返回給前端,最後呈現的效果如下:

最後,拿到 token 後的客戶端,就可以將 token 加入到請求頭後,去訪問需要授權的介面了。

結合上一篇文章,我們就實現了 password 和 授權碼兩種模式的 oauth2 認證。

補充說明

之前我在公眾號首發這篇文章的時候,有同學看完提了下面的問題。

請問下,我在做認證的服務端,我怎麼知道code對應的是哪個使用者啊?是哪一步做的關聯啊?

解釋如下:

可能有的同學沒搞清楚哪個是認證服務端,哪個是客戶端,也怪我專案名稱起的有點歧義,上圖示明瞭原始碼中的服務端和客戶端,大家可以對照著程式碼看。

上圖的步驟就是輸入使用者名稱和密碼的過程,這一步會生成 code,同時應該在認證服務端儲存這個 code 和 使用者的對應關係。

之後,跳轉到回撥地址,回撥地址拿著這個 code 去認證服務端請求 token,同時去查詢這個 code 對應的使用者(注意會有一個過期時間,比如 5 分鐘有效),然後和 token 一起被返回。

下面是返回 token 的 json 串,這個結構中可以有擴充套件屬性,可以加上使用者 id 或使用者名稱等資訊,比如微信登入就會返回 openId 和 unionId。

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MjYwMTMzMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2OWRmY2M4Yy1iZmZiLTRiNDItYTZhZi1hN2IzZWUyZjI1ZTMiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.WlgGnBkNdg2PwKqjbZWo6QmUmq0QluZLgIWJXaZahSU",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjY5ZGZjYzhjLWJmZmItNGI0Mi1hNmFmLWE3YjNlZTJmMjVlMyIsImV4cCI6MTU3MjYzMzczMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkNzk2OWRhMS04NTg4LTQ2YzMtYjdlNS1jMGM5NzcxNTM5Y2YiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.TEz0pQOhST9-ozdoJWm6cf1SoWvPC6W-5JW9yjZJXek",
"expires_in": 3599,
"scope": "all",
"jwt-ext": "JWT 擴充套件資訊",
"jti": "69dfcc8c-bffb-4b42-a6af-a7b3ee2f25e3"
}
複製程式碼

經過以上幾步就建立了使用者和 code 的關聯關係。

創作不易,請給我點贊吧,小小的贊,大大的暖,能給我帶來繼續創作的動力。不用客氣了,讚我吧!

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

昨天有不少同學看到文章關注了公眾號,而且進了交流群,群家族又擴大了,哈哈。

可能是全網最詳細的 Spring Cloud OAuth2 授權碼模式使用教程了,微信登入就是這個原理

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

相關文章