1. 程式人生 > >這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登入流程?

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

昨天和小夥伴們介紹了 OAuth2 的基本概念,在講解 Spring Cloud Security OAuth2 之前,我還是先來通過實際程式碼來和小夥伴們把 OAuth2 中的各個授權模式走一遍,今天我們來看最常用也最複雜的授權碼模式。

本文我將通過一個完整的 Demo ,注意,是一個完整的 Demo,帶領小夥伴們把授權碼模式走一遍。

如果小夥伴們還沒有看上篇文章可以先看下,這有助於你理解本文中的一些概念:

  • 做微服務繞不過的 OAuth2,鬆哥也來和大家扯一扯

1.案例架構

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

  • 第三方應用
  • 授權伺服器
  • 資源伺服器
  • 使用者

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

專案 備註
auth-server 8080 授權伺服器
user-server 8081 資源伺服器
client-app 8082 第三方應用

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

注意:小夥伴們一定先看下上篇文章鬆哥所講的 OAuth2 授權碼模式登入流程,再來學習本文。

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

2.授權伺服器搭建

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

  • web
  • spring cloud security
  • spirng cloud OAuth2

專案建立完成後,首先提供一個 Spring Security 的基本配置:

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

在這段程式碼中,為了程式碼簡潔,我就不把 Spring Security 使用者存到資料庫中去了,直接存在記憶體中。

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

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

基本的使用者資訊配置完成後,接下來我們來配置授權伺服器:

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

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

  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 ,作為我們的資源伺服器,建立時,新增如下依賴:

專案建立成功之後,新增如下配置:

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

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

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

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

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

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

4.第三方應用搭建

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

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

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

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

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>江南一點雨</title>
</head>
<body>
你好,江南一點雨!

<a href="http://localhost:8080/oauth/authorize?client_id=javaboy&response_type=code&scope=all&redirect_uri=http://localhost:8082/index.html">第三方登入</a>

<h1 th:text="${msg}"></h1>
</body>
</html>

這是一段 Thymeleaf 模版,點選超連結就可以實現第三方登入,超連結的引數如下:

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

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

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

@Controller
public class HelloController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/index.html")
    public String hello(String code, Model model) {
        if (code != null) {
            MultiValueMap<String, 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,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";
    }
}

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

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

  1. 根據拿到的 code,去請求 http://localhost:8080/oauth/token 地址去獲取 Token,返回的資料結構如下:
{
    "access_token": "e7f223c4-7543-43c0-b5a6-5011743b5af4",
    "token_type": "bearer",
    "refresh_token": "aafc167b-a112-456e-bbd8-58cb56d915dd",
    "expires_in": 7199,
    "scope": "all"
}

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 頁面,結果如下:

然後我們點選 第三方登入 這個超連結,點完之後,會進入到授權伺服器的預設登入頁面:

接下來我們輸入在授權伺服器中配置的使用者資訊來登入,登入成功後,會看到如下頁面:

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

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

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

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

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

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

好了,關注微信公眾號江南一點雨,回覆 oauth2 下載本文完整案例。