OAuth2 登錄流程

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

原創 江南一點雨 江南一點雨 今天

今日干貨

剛剛發表

查看:66666回覆:666

公衆號後臺回覆 ssm,免費獲取松哥純手敲的 SSM 框架學習乾貨。

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

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

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

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 這個字符串,報錯信息如下:

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

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

好了,本文案例下載地址:https://github.com/lenve/oauth2-samples

如果小夥伴們覺得有用的話,記得點個在看鼓勵下松哥。

今日干貨

剛剛發表

查看:66666回覆:666

公衆號後臺回覆 ssm,免費獲取松哥純手敲的 SSM 框架學習乾貨。

微信掃一掃
關注該公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章