Gitlab OAuth2 Application簡明教程


1 概要流程說明

1.1 應用注冊

Gitlab允許用戶創建Applications, 這些Applications可以通過OAuth2授權來訪問Gitlab的相應資源。

在Gitlab中, Applications分兩種, 第一種是用戶級別的Application, 這一般通過用戶的Profile菜單進入創建:

第二種是系統級別的Application, 這一般只有管理員權限的人通過Admin菜單進入創建:

我們以系統級別的Application為例, 說明如何創建並注冊一個Application:

點擊“New Application”之后, 會進入要求輸入應用注冊信息頁面:

假設我們創建一個testapp應用,並在本機調試,那么我們填寫如下信息:

然后點擊提交(Submit), 將得到應用創建后的信息:

這個頁面的信息將由創建應用的管理員分發給相應的Application負責人, 后面, Application應用負責人將使用這些信息做認證。

注冊完成后, 我們可以看到應用現在只有0個Clients, 即還沒有任何實例作為這個Application的實例進行認證:

但不管怎么說, 應用注冊這一步我們算成功完成啦! Give Me Five~

1.2 應用授權

應用注冊成功后, 應用的實例就可以作為一個認證實體向Gitlab認證自己啦, 如果認證成功, 就可以獲取一個代表某個用戶權限的access token對Gitlab的資源進行訪問了。

GitLab as an OAuth2 client這篇幫助文檔其實說的就是這個過程, 但其實只是看, 不自己動動手很難搞清楚這個過程是怎么回事, 尤其是, 幫助文檔中沒有清楚的表達出Gitlab和Application實例雙方的交互流程,初看起來更是讓人百思不得其解。

下面我簡單通過圖例說明這個交互的過程,后面再通過工程實例代碼演示這個過程是如何實現的…

首先, 我們需要創建一個Web應用, 這個Web應用就是我們注冊為Gitlab上的那個Application, 當用戶初次訪問這個Application的時候(比如訪問http://your.application.host/), 我們需要獲得Gitlab上某個用戶的授權,以便代表這個用戶來訪問Gitlab上的資源並做一些事情, 所以,我們直接將用戶請求redirect到gitlab的某個url下, 這個url就是http://{your.gitlab.server}/oauth/authorize, 當然, 我們需要通過參數帶上一些必要的請求信息,以便gitlab可以決定給誰授權, 所以, 這個url后面一般需要帶上以下幾個參數:

  1. client_id, 也就是我們注冊Application成功后分發給你這個Application的Application Id;
  2. redirect_uri, 在注冊Application的時候我們自己提交的Callback url, 因為我們在本地調試, 所以其實就是http://localhost:8080/callback這個url, 如果是線上應用,一般直接在注冊的時候(或者之后Edit)輸入對應的域名標識的url;
  3. response_type=code, 固定字符串, 表示我們使用OAuth2的Authorization Code Grant授權模式;

當請求被重定向到以上gitlab的url的時候, gitlab會顯示如下類似的頁面要求當前已經登錄gitlab的用戶授權當前Application代表他/她來訪問Gitlab的各項資源:

不管用戶是點擊了“Authorize”同意授權還是“Deny”拒絕授權, gitlab都會將web請求重定向到Application注冊的時候提供的Callback url地址上(在這里是我們的http://localhost:8080/callback), 然后Application的對應這個url地址的Action就可以根據授權結果來決定后繼行為了。

如果用戶授權, 則Application會收到一個授權code, 使用這個授權code結合之前分配的Secret(即client secret)和一些其它必要信息,就可以訪問http://{your.gitlab.server}/oauth/token並從請求返回的響應(Response)中獲得一個AccessToken(當然,還有其他信息,比如Expire時間窗口有多長, RefreshToken,以及授權訪問的scope是什么等), 之后, Application就可以使用這個AccessToken並結合gitlab的API來訪問相應的資源(只要授權的這個用戶有權限訪問)。

2 工程實現舉例

我們使用Apache的OLTU這個庫來構建OAuth2交互流程,所以需要先把它加到項目依賴之中:

 <dependency>
        <groupId>org.apache.oltu.oauth2</groupId>
        <artifactId>org.apache.oltu.oauth2.client</artifactId>
        <version>1.0.1</version>
    </dependency>

 

簡單起見,我們使用SpringBoot構建一個Web應用oauth2-app-proto, 項目的POM大體看起來像這樣:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.keevol</groupId>
    <artifactId>oauth2-app-proto</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>oauth2-app-proto</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.oltu.oauth2</groupId>
            <artifactId>org.apache.oltu.oauth2.client</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

 

我們核心類就一個:

@Controller
public class IndexController {
    protected transient final Logger logger = LoggerFactory.getLogger(getClass());

    @Value("${oauth2.server.url}")
    private String gitlabServerUrl;
    @Value("${oauth2.server.url.authorize.path:/oauth/authorize}")
    private String authorizePath;
    @Value("${oauth2.server.url.token.path:/oauth/token}")
    private String tokenPath;

    @Value("${oauth2.client.id:260f7b273e13b9264b22bba3c5f1db53d55c8cb86fe3d02d7c11725ea52b0fe7}")
    private String clientId;
    @Value("${oauth2.client.secret:15cebdce7a671fee9a7929b5a2c2bca006bb5afa92b36352586ccb6089004765}")
    private String clientSecret;
    @Value("${oauth2.client.callback.url:http://localhost:8080/callback}")
    private String callbackUrl;

    @Autowired
    TokenRepository tokenRepository;

    OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());

    String currentUserMock = "yunshi";

    @PreDestroy
    public void cleanUp() {
        oAuthClient.shutdown();
    }

    @RequestMapping("/main")
    @ResponseBody
    public String main() {
        if (tokenRepository.getTokenOf(currentUserMock).isPresent()) {
            return "authorization is done, you are good to go with access token: " + tokenRepository.getTokenOf(currentUserMock).get();
        } else {
            return "no authority.";
        }
    }

    @RequestMapping("/")
    public String index(HttpServletRequest req, HttpServletResponse response) throws Throwable {
        if (tokenRepository.getTokenOf(currentUserMock).isPresent()) {
            logger.info("query user information with access token...");
            OAuthClientRequest bearerClientRequest = new OAuthBearerClientRequest(gitlabServerUrl + "/api/v3/user").setAccessToken(tokenRepository.getTokenOf(currentUserMock).get()).buildQueryMessage();
            OAuthResourceResponse resourceResponse = oAuthClient.resource(bearerClientRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);
            logger.info("had authorized, just query for user information...");
            logger.info("user information: " + resourceResponse.getBody());
            return "redirect:/main";
        } else {
            logger.info("first login, build oauth request >..");
            OAuthClientRequest request = OAuthClientRequest
                    .authorizationLocation(gitlabServerUrl + authorizePath)
                    .setClientId(clientId)
                    .setRedirectURI(callbackUrl)
                    .setResponseType("code")
                    .buildQueryMessage();

            String gitlabAuthUrl = request.getLocationUri();

            logger.info("redirect to : " + gitlabAuthUrl);
            return "redirect:" + gitlabAuthUrl;
        }
    }


    @RequestMapping("/callback")
    public String callback(@RequestParam(value = "code", required = false) String code,
                           @RequestParam(value = "error", required = false) String error,
                           @RequestParam(value = "error_description", required = false) String errorDescription) throws Throwable {

        if (StringUtils.hasLength(error)) {
            logger.error("authorization fails with error={} and error description={}", error, errorDescription);
        } else {
            logger.info("callback request receives with code={}", code);

            OAuthClientRequest request = OAuthClientRequest
                    .tokenLocation(gitlabServerUrl + tokenPath)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setClientId(clientId)
                    .setClientSecret(clientSecret)
                    .setRedirectURI(callbackUrl)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setCode(code)
                    .buildQueryMessage();

            logger.info("build authorize request with code:{} and client secret", code);

            OAuthJSONAccessTokenResponse response = oAuthClient.accessToken(request);
            String accessToken = response.getAccessToken();
            logger.info("access token got: {}", accessToken);

            // save access token for further use, then redirect user to another url in our own application.
            tokenRepository.store(currentUserMock, accessToken);
        }

        return "redirect:/main";
    }
    // ...
}

 

首先重點關注映射處理根路徑請求的index()方法,這個endpoint負責觸發授權行為, 你會看到,如果我們發現當前用戶在我們應用這邊沒有任何授權狀態(比如沒有相應的access token), 那么,代碼邏輯走else分支, 我們將構建一個要求gitlab服務器授權的請求,並將當前針對我們自己應用的web請求重定向到gitlab服務器的授權地址, 之后你就會看到以下的頁面:

gitlab的當前登錄用戶如果點擊Authorize同意授權之后, gitlab將會傳遞一個授權code給應用注冊時候提供的callback地址, 這樣, 請求就會輪轉重定向到這個callback地址, 這個callback地址,在我們的Controller實現中是對應callback()這個endpoint方法進行處理的。

如果用戶點擊Deny拒絕授權, 請求也同樣會重定向到callback地址,只不過傳遞的參數不是一個授權code, 而是一個error和error_description, 所以,你會看到我們的callback()方法聲明了三個非必須的參數。

未授權的處理很簡單,重定向到應用的某個公開頁面就可以了,這里不再細說。

應用的callback()處理方法收到授權code之后, 會使用這個授權code加上應用注冊時候配發的相應clientId和clientSecret等信息構建一個請求access token的oauth2請求,並發往gitlab服務器, 即這一段代碼:

  OAuthClientRequest request = OAuthClientRequest
                    .tokenLocation(gitlabServerUrl + tokenPath)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setClientId(clientId)
                    .setClientSecret(clientSecret)
                    .setRedirectURI(callbackUrl)
                    .setGrantType(GrantType.AUTHORIZATION_CODE)
                    .setCode(code)
                    .buildQueryMessage();

            logger.info("build authorize request with code:{} and client secret", code);

            OAuthJSONAccessTokenResponse response = oAuthClient.accessToken(request);

 

請求成功之后,我們就可以獲得相應的access token(以及對應refresh token, expires時間等), 之后,我們就可以使用這個access token暢通無阻的訪問授權用戶的各項gitlab服務器上的資源了。

我們的代碼實例中,只是先存下來,然后將用戶引導到一個無關的展示頁面(/main), 在實際應用中,可以考慮直接將用戶引導到授權之后有權限訪問的頁面。

到此, 基本上一個與Gitlab服務器正常交互的OAuth2處理流程就完成啦!

如果你再次去查看gitlab服務器上Application的列表, 會發現我們注冊的Application已經有1個Clients啦:

有咩有一點兒小興奮?

3 FAQs

3.1 Private Token與Access Token什么區別?

通過OAuth獲得的是Access Token,Access Token一般代替當前給與授權的用戶頒發給應用實體,拿到Access Token之后, 可以依據Access Token去獲取用戶信息,進而可以在用戶信息中拿到用戶個人的Private Token。

不管是Access Token還是Private Token, Gitlab API都支持作為API訪問的憑據。

在gitlab中, 頒發的access token一般是長度為64的字符串, 比如1f0af717251950dbd4d73154fdf0a474a5c5119adad999683f5b450c460726aa, 而private token則只是長度20的字符串, 比如SegfaScazyYyD_UG-n68

Gitlab用戶如果沒有登錄的話, 授權其實是不會進行的, gitlab會引導用戶到登陸頁面登錄, 只有登錄成功的用戶才會授權oauth2客戶端應用訪問。

3.2 “The redirect url included is not valid”錯誤

一般情況下, 出現這種錯誤信息是因為應用注冊的時候提供的callback地址跟我們在發起授權請求時候提供的callback地址不一致, 比如我們在注冊的時候提供callback地址為http://localhost:8080,而在發起授權請求時提供的callback地址卻是http://localhost:8080/callback

3.3 為啥沒有顯示授權頁面,而是引導我到了登錄頁面?

如果當前機器上無任何用戶登錄gitlab,當gitlab收到要求授權的請求時(即接收到重定向到http://your.gitlab.server/oauth/authorize的請求), gitlab會再次將請求重定向到gitlab的登錄頁面, 要求使用Application的用戶登錄Gitlab從而可以作為一個主體授權Application訪問:

用戶成功登陸后,則被gitlab引導進入Dashboard頁面:

這里有點兒不夠友好, 需要用戶重新訪問要求授權的Application的某個URL以便重新發起授權請求。

4 小結和補充

使用Controller來演示通過OAuth2與Gitlab服務器交互實際上只是為了簡化, 正常來說, 使用攔截器或者Filter來管理授權行為的觸發和認證才是比較合適的做法,尤其是封裝成一個spring-boot-starter-gitlab-oauth2之類的自動配置模塊, 可以大大簡化開發的復雜度提升集成效率, 也不需要應用研發用戶去了解以上流程交互細節(當然啦,從研發人員的角度, 還是了解這些細節比較好)。

授權成功后,我們其實可以拿到不止一個Access Token, 隨同的還有Refresh Token, Access Token的超時時間(Expires), 以及授權的范圍(scope), 各位客官可以根據請求選用。

有了以上與Gitlab服務器通過OAuth2集成授權的神功, 各位客官可以盡情的構建圍繞Gitlab的各種有趣的應用啦,你可以寫個小Robot來跟蹤issue並回復, 你也可以寫一個圍繞project自動配置各種資源的持續交付和運維平台, 就看你怎么玩啦,GL & HF


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM