這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?

1.案例架構

因為 OAuth2 涉及到的東西比較多,網上的案例大多都是簡化的,對於很多初學者而言,簡化的案例看的人云裡霧裡,所以松哥這次想自己搭建一個完整的測試案例,在這個案例中,主要包括如下服務:

  • 第三方應用
  • 授權服務器
  • 資源服務器
  • 用戶

我用一個表格來給大家整理下:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?

就是說,我們常見的 OAuth2 授權碼模式登錄中,涉及到的各個角色,我都會自己提供,自己測試,這樣可以最大限度的讓小夥伴們瞭解到 OAuth2 的工作原理(文末可以下載案例源碼)

那我們首先來創建一個空的 Maven 父工程,創建好之後,裡邊什麼都不用加,也不用寫代碼。我們將在這個父工程中搭建這個子模塊。

首先我們搭建一個名為 auth-server 的授權服務,搭建的時候,選擇如下三個依賴:

  • web
  • spring cloud security
  • spirng cloud OAuth2


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


項目創建完成後,首先提供一個 Spring Security 的基本配置:

<code>@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("sang")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("admin")
.and()
.withUser("javaboy")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("user");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin();
}
}/<code>

在這段代碼中,為了代碼簡潔,我就不把 Spring Security 用戶存到數據庫中去了,直接存在內存中。

這裡我創建了一個名為 sang 的用戶,密碼是 123,角色是 admin。同時我還配置了一個表單登錄。

這段配置的目的,實際上就是配置用戶。例如你想用微信登錄第三方網站,在這個過程中,你得先登錄微信,登錄微信就要你的用戶名/密碼信息,那麼我們在這裡配置的,其實就是用戶的用戶名/密碼/角色信息。

基本的用戶信息配置完成後,接下來我們來配置授權服務器:

<code>@Configuration
public class AccessTokenConfig {
@Bean
TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
@EnableAuthorizationServer
@Configuration
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
ClientDetailsService clientDetailsService;

@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(60 * 60 * 2);
services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
return services;
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("javaboy")
.secret(new BCryptPasswordEncoder().encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code","refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

endpoints.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
@Bean
AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
}/<code>

這段代碼有點長,我來給大家挨個解釋:

  1. 首先我們提供了一個 TokenStore 的實例,這個是指你生成的 Token 要往哪裡存儲,我們可以存在 Redis 中,也可以存在內存中,也可以結合 JWT 等等,這裡,我們就先把它存在內存中,所以提供一個 InMemoryTokenStore 的實例即可。
  2. 接下來我們創建 AuthorizationServer 類繼承自 AuthorizationServerConfigurerAdapter,來對授權服務器做進一步的詳細配置,AuthorizationServer 類記得加上 @EnableAuthorizationServer 註解,表示開啟授權服務器的自動化配置。
  3. 在 AuthorizationServer 類中,我們其實主要重寫三個 configure 方法。
  4. AuthorizationServerSecurityConfigurer 用來配置令牌端點的安全約束,也就是這個端點誰能訪問,誰不能訪問。checkTokenAccess 是指一個 Token 校驗的端點,這個端點我們設置為可以直接訪問(在後面,當資源服務器收到 Token 之後,需要去校驗 Token 的合法性,就會訪問這個端點)。
  5. ClientDetailsServiceConfigurer 用來配置客戶端的詳細信息,在上篇文章中,松哥和大家講過,授權服務器要做兩方面的檢驗,一方面是校驗客戶端,另一方面則是校驗用戶,校驗用戶,我們前面已經配置了,這裡就是配置校驗客戶端。客戶端的信息我們可以存在數據庫中,這其實也是比較容易的,和用戶信息存到數據庫中類似,但是這裡為了簡化代碼,我還是將客戶端信息存在內存中,這裡我們分別配置了客戶端的 id,secret、資源 id、授權類型、授權範圍以及重定向 uri。授權類型我在上篇文章中和大家一共講了四種,四種之中不包含 refresh_token 這種類型,但是在實際操作中,refresh_token 也被算作一種。
  6. AuthorizationServerEndpointsConfigurer 這裡用來配置令牌的訪問端點和令牌服務。authorizationCodeServices用來配置授權碼的存儲,這裡我們是存在在內存中,tokenServices 用來配置令牌的存儲,即 access_token 的存儲位置,這裡我們也先存儲在內存中。有小夥伴會問,授權碼和令牌有什麼區別?授權碼是用來獲取令牌的,使用一次就失效,令牌則是用來獲取資源的,如果搞不清楚,建議重新閱讀上篇文章惡補一下:做微服務繞不過的 OAuth2,松哥也來和大家扯一扯
  7. tokenServices 這個 Bean 主要用來配置 Token 的一些基本信息,例如 Token 是否支持刷新、Token 的存儲位置、Token 的有效期以及刷新 Token 的有效期等等。Token 有效期這個好理解,刷新 Token 的有效期我說一下,當 Token 快要過期的時候,我們需要獲取一個新的 Token,在獲取新的 Token 時候,需要有一個憑證信息,這個憑證信息不是舊的 Token,而是另外一個 refresh_token,這個 refresh_token 也是有有效期的。

好了,如此之後,我們的授權服務器就算是配置完成了,接下來我們啟動授權服務器。

3.資源服務器搭建

接下來我們搭建一個資源服務器。大家網上看到的例子,資源服務器大多都是和授權服務器放在一起的,如果項目比較小的話,這樣做是沒問題的,但是如果是一個大項目,這種做法就不合適了。

資源服務器就是用來存放用戶的資源,例如你在微信上的圖像、openid 等信息,用戶從授權服務器上拿到 access_token 之後,接下來就可以通過 access_token 來資源服務器請求數據。

我們創建一個新的 Spring Boot 項目,叫做 user-server ,作為我們的資源服務器,創建時,添加如下依賴:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


項目創建成功之後,添加如下配置:

<code>@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
RemoteTokenServices tokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
services.setClientId("javaboy");
services.setClientSecret("123");
return services;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1").tokenServices(tokenServices());
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}/<code>

這段配置代碼很簡單,我簡單的說一下:

  1. tokenServices 我們配置了一個 RemoteTokenServices 的實例,這是因為資源服務器和授權服務器是分開的,資源服務器和授權服務器是放在一起的,就不需要配置 RemoteTokenServices 了。
  2. RemoteTokenServices 中我們配置了 access_token 的校驗地址、client_id、client_secret 這三個信息,當用戶來資源服務器請求資源時,會攜帶上一個 access_token,通過這裡的配置,就能夠校驗出 token 是否正確等。
  3. 最後配置一下資源的攔截規則,這就是 Spring Security 中的基本寫法,我就不再贅述。

接下來我們再來配置兩個測試接口:

<code>@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
}/<code>

如此之後,我們的資源服務器就算配置成功了。

4.第三方應用搭建

接下來搭建我們的第三方應用程序。

注意,第三方應用並非必須,下面所寫的代碼也可以用 POSTMAN 去測試,這個小夥伴們可以自行嘗試。

第三方應用就是一個普通的 Spring Boot 工程,創建時加入 Thymeleaf 依賴和 Web 依賴:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


在 resources/templates 目錄下,創建 index.html ,內容如下:

<code>



<title>江南一點雨/<title>


你好,江南一點雨!





/<code>

這是一段 Thymeleaf 模版,點擊超鏈接就可以實現第三方登錄,超鏈接的參數如下:

  • client_id 客戶端 ID,根據我們在授權服務器中的實際配置填寫。
  • response_type 表示響應類型,這裡是 code 表示響應一個授權碼。
  • redirect_uri 表示授權成功後的重定向地址,這裡表示回到第三方應用的首頁。
  • scope 表示授權範圍。

h1 標籤中的數據是來自資源服務器的,當授權服務器通過後,我們拿著 access_token 去資源服務器加載數據,加載到的數據就在 h1 標籤中顯示出來。

接下來我們來定義一個 HelloController:

<code>@Controller
public class HelloController {
@Autowired
RestTemplate restTemplate;

@GetMapping("/index.html")
public String hello(String code, Model model) {
if (code != null) {
MultiValueMap<string> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "javaboy");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code");
Map<string> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
String access_token = resp.get("access_token");
System.out.println(access_token);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + access_token);
HttpEntity<object> httpEntity = new HttpEntity<>(headers);
ResponseEntity<string> entity = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
model.addAttribute("msg", entity.getBody());
}
return "index";
}
}/<string>/<object>/<string>/<string>/<code>

在這個 HelloController 中,我們定義出 /index.html 的地址。

如果 code 不為 null,也就是如果是通過授權服務器重定向到這個地址來的,那麼我們做如下兩個操作:

  1. 根據拿到的 code,去請求 http://localhost:8080/oauth/token 地址去獲取 Token,返回的數據結構如下:
<code>{
"access_token": "e7f223c4-7543-43c0-b5a6-5011743b5af4",
"token_type": "bearer",

"refresh_token": "aafc167b-a112-456e-bbd8-58cb56d915dd",
"expires_in": 7199,
"scope": "all"
}/<code>

access_token 就是我們請求數據所需要的令牌,refresh_token 則是我們刷新 token 所需要的令牌,expires_in 表示 token 有效期還剩多久。

  1. 接下來,根據我們拿到的 access_token,去請求資源服務器,注意 access_token 通過請求頭傳遞,最後將資源服務器返回的數據放到 model 中。

這裡我只是舉一個簡單的例子,目的是和大家把這個流程走通,正常來說,access_token 我們可能需要一個定時任務去維護,不用每次請求頁面都去獲取,定期去獲取最新的 access_token 即可。後面的文章中,松哥還會繼續完善這個案例,到時候再來和大家解決這些細節問題。

OK,代碼寫完後,我們就可以啟動第三方應用開始測試了。

5.測試

接下來我們去測試。

首先我們去訪問 http://localhost:8082/index.html 頁面,結果如下:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


然後我們點擊 第三方登錄 這個超鏈接,點完之後,會進入到授權服務器的默認登錄頁面:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


接下來我們輸入在授權服務器中配置的用戶信息來登錄,登錄成功後,會看到如下頁面:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


在這個頁面中,我們可以看到一個提示,詢問是否授權 javaboy 這個用戶去訪問被保護的資源,我們選擇 approve(批准),然後點擊下方的 Authorize 按鈕,點完之後,頁面會自動跳轉回我的第三方應用中:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


大家注意,這個時候地址欄多了一個 code 參數,這就是授權服務器給出的授權碼,拿著這個授權碼,我們就可以去請求 access_token,授權碼使用一次就會失效。

同時大家注意到頁面多了一個 admin,這個 admin 就是從資源服務器請求到的數據。

當然,我們在授權服務器中配置了兩個用戶,大家也可以嘗試用 javaboy/123 這個用戶去登錄,因為這個用戶不具備 admin 角色,所以使用這個用戶將無法獲取到 admin 這個字符串,報錯信息如下:


這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?


這個小夥伴們可以自己去測試,我就不再演示了。

最後在說一句,這不是終極版,只是一個雛形,後面的文章,松哥再帶大家來繼續完善這個案例。

好了,關注我轉發文章後臺私信【源碼】即可免費下載本文完整案例。


作者:江南一點雨
鏈接:https://juejin.im/post/5e950b44e51d4546dc14af30


分享到:


相關文章: