微信授權就是這個原理,Spring Cloud OAuth2 授權碼模式
- 2019 年 11 月 7 日
- 筆記
上一篇文章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 認證。
本篇源碼微服務客戶端對應的源碼地址為: 點擊查看 github 源碼
相關閱讀
不要吝惜你的「推薦」呦
歡迎關注,不定期更新本系列和其他文章
古時的風箏
,進入公眾號可以加入交流群