iT邦幫忙

2022 iThome 鐵人賽

DAY 20
1
Software Development

讓 C# 也可以很 Social - 在 .NET 6 用 C# 串接 LINE Services API 的取經之路系列 第 20

[Day 20] .NET 6 C# 與 Line Services API 開發 - Line Login API (一) 起手式

  • 分享至 

  • xImage
  •  
tags: .NET6 C#, LineBot, Line Messaging API, C#, dotnet core

[Day 20] 讓 C# 也能很 Social - .NET 6 C# 與 Line Services API 開發 - Line Login API (一)起手式

前言

Hello 大家好,從本篇開始要介紹的內容是 Line Login,而後面其它篇的內容則會圍繞在網頁串接 Line Login、Line Pay、LIFF 等功能,雖然 Line Bot 上的訊息非常好用,能夠做出相當多元化的服務內容,但還是基於 Line 框架 導致擴充性還是有其限制,畢竟不像製作網頁的時候 能隨心所欲的做任何編排及效果。所以若想要更多的網頁互動/呈現的特效,可能還是透過網頁來實作會是比較好的選擇,所以這篇的過程中,我們會一起製作後端的程式以及前端的畫面 ~

Line Login

Line Login 是基於 OAuth2OpenID 協議建立的認證系統,在自己開發的系統中串接此類的第三方認證,能夠讓使用者不用再經歷註冊的流程,也不用多記住一組帳號密碼,更保證了其用戶資料的安全性。

另外,就像第一篇提到的,Line 在台灣16歲-64歲網路人口的使用比例高達95.7%,提供了 Line Login 的功能就代表幾乎所有使用者都一定能選擇這個登入方式,CP值真的超高。

Line Login 使用範例

最早開始,第三方登入只有 google 與 facebook,不過近幾年將 line 作為第三方登入串接的商家是越來越多了,下面是兩家有串接 line login 的商家的例子。

UNIQLO

有許多商家會在使用者使用第三方登入後再要求使用者填寫資料並建立帳號,會多這一個建立帳號的流程的意義是因為其對於使用者資料的掌控仍然有一定的需求,因為透過 Line 取得的使用者資料有限,若建立一個帳號後再與 Line 綁定,則提供了很大的彈性。

Login Flow

以下是 Line Login 的整個流程

  1. 使用者送出登入請求後,web 跳轉至 Line 的登入頁面,其中會將 redirect uri & state(用於安全驗證) ...等等資訊放在 query parameter 送到 Line。
  2. 使用者登入成功並授權相關資訊後,Line 會將 authorization code 與 state 放在 query parameter 跳轉至一開始帶入的 redirect uri。
  3. (驗證完 state 與一開始帶入相同後) 將 authorization code 送至 Line,取回 Access Token...等等登入相關資訊
  4. 再將取得的 Access Token 送到 Line,即可取回使用者資訊。

Line Login Channel 建立

要使用 line login 服務與 messaging api 一樣需要建立 channel。

  • 首先選擇 Line Login Channel

  • 選擇後填寫基本資料,並按下建立。



  • 建立好 Login Channel 後其中,即可看到畫面中的資訊,其中 Channel ID、Channel Sceret 稍後會用到。

Channel 建立完後,就開始實作程式的內容

後端實作

我這邊的做法是在 login flow 中間插入我們的 server,負責與 line login api 的溝通。

基本 Controller & Service

在 Controllers 資料夾中新增 LineLoginController.cs

using LineBotMessage.Domain;
using LineBotMessage.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace LineBotMessage.Controllers
{
    [Route("api/[Controller]")]
    [ApiController]
    public class LineLoginController : ControllerBase
    {
        private readonly LineLoginService _lineLoginService;
        public LineLoginController()
        {
            _lineLoginService = new LineLoginService();
        }
    }
}

在 Domain 資料夾中新增 LineLoginService.cs

using System.Net.Http.Headers;
using System.Text;
using System.Web;
using LineBotMessage.Dtos;
using LineBotMessage.Providers;

namespace LineBotMessage.Domain
{
    public class LineLoginService
    {

        private static HttpClient client = new HttpClient();
        private readonly JsonProvider _jsonProvider = new JsonProvider();

        public LineLoginService()
        {
        }
    }
}

取得 Authorization URL 文件連結

Authorization url 就是開啟登入頁面的連結

  • 登入頁面

透過以下 url 與許多的 query parameter 組成,來達到不同的登入效果。

https://access.line.me/oauth2/v2.1/authorize

其中有以下五項必填的 query parameter

  1. response_type : 固定填 code
  2. client_id : login channel id
  3. redirect_uri : 登入成功後要跳轉的網址,也可以帶入自己的 query parameter
  4. state : 用來做安全性驗證用的字串,用於防止 cross-site request forgery,正確來說應該每次登入 request 都帶入隨機的 state,但本系列這邊就先略過了~
  5. scope : 取得的使用者資料範圍,詳細如下圖

Authorization Uri 範例

https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=1657502969&redirect_uri=https%3a%2f%2fcccf-61-63-154-173.jp.ngrok.io%2fprofile.html&state=1qazRTGFDY5ysg&scope=profile&openId

在 LineLoginSerice 新增以下 function & variable

private readonly string loginUrl = "https://access.line.me/oauth2/v2.1/authorize?response_type={0}&client_id={1}&redirect_uri={2}&state={3}&scope={4}";
private readonly string clientId = {channel ID};
private readonly string clientSecret = {channel secret};

// 回傳 line authorization url
public string GetLoginUrl(string redirectUrl)
{
    // 根據想要得到的資訊填寫 scope
    var scope = "profile&openId";
    // 這個 state 是隨便打的
    var state = "1qazRTGFDY5ysg";
    var uri = string.Format(loginUrl, "code", clientId, HttpUtility.UrlEncode(redirectUrl), state, scope);
    return uri;
}

在 LineLoginController 新增以下 API

// 取得 Line Login 網址
[HttpGet("Url")]
public string GetLoginUrl([FromQuery] string redirectUrl)
{
    return _lineLoginService.GetLoginUrl(redirectUrl);
}

Get Access Token 文件連結

使用前端 callback 帶回的 authToken 去取得 access token 等等登入資訊。

根據 Response 新增 Dtos/Login 資料夾,並新增 TokensResponseDto.cs

namespace LineBotMessage.Dtos
{
    public class TokensResponseDto
    {
        public string Access_token { get; set; }
        public string Token_type { get; set; }
        public string Refresh_token { get; set; }
        public int Expires_in { get; set; }
        public string Scope { get; set; }
        public string? Id_token { get; set; }
    }
}

在 LineLoginService 中新增以下 function

private readonly string tokenUrl = "https://api.line.me/oauth2/v2.1/token";


// 取得 access token 等資料
public async Task<TokensResponseDto> GetTokensByAuthToken(string authToken, string callbackUri)
{
    var formContent = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("grant_type", "authorization_code"),
        new KeyValuePair<string, string>("code", authToken),
        new KeyValuePair<string, string>("redirect_uri",callbackUri),
        new KeyValuePair<string, string>("client_id", clientId),
        new KeyValuePair<string, string>("client_secret", clientSecret),
    });

    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); //添加 accept header
    var response = await client.PostAsync(tokenUrl, formContent); // 送出 post request
    var dto = _jsonProvider.Deserialize<TokensResponseDto>(await response.Content.ReadAsStringAsync()); //將 json response 轉成 dto

    return dto;
}

在 LineLoginController 中新增 API

// 使用 authToken 取回登入資訊
[HttpGet("Tokens")]
public async Task<TokensResponseDto> GetTokensByAuthToken([FromQuery] string authToken, [FromQuery] string callbackUrl)
{
    return await _lineLoginService.GetTokensByAuthToken(authToken, callbackUrl);
}

Get User Profile By Access Token 文件連結

取得 user profile 的方式有兩種,一種是使用 access token,另一種是使用 id token,本篇先介紹 access token,下一篇再介紹使用 id token~

根據 response 新增 UserProfile class

namespace LineBotMessage.Dtos
{
    public class UserProfileDto
    {
        public string UserId { get; set; }
        public string DisplayName { get; set; }
        public string StatusMessage { get; set; }
        public string PictureUrl { get; set; }
    }
}

在 LineLoginService 新增以下 variable & function

private readonly string profileUrl = "https://api.line.me/v2/profile";


public async Task<UserProfileDto> GetUserProfileByAccessToken(string accessToken)
{
    //取得 UserProfile
    var request = new HttpRequestMessage(HttpMethod.Get, profileUrl);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    var response = await client.SendAsync(request);
    var profile = _jsonProvider.Deserialize<UserProfileDto>(await response.Content.ReadAsStringAsync());

    return profile;
}

在 LineLoginController 新增 API

// 使用 access token 取得 user profile
[HttpGet("Profile/{accessToken}")]
public async Task<UserProfileDto> GetUserProfileByAccessToken(string accessToken)
{
    return await _lineLoginService.GetUserProfileByAccessToken(accessToken);
}

後端 API 開好後,下一步就要來實作前端的內容~(我自己在寫的時候是兩邊同時做的,但因文章不好表達就分開來講)

前端實作

這次網頁的實作就以簡單為主,畢竟本系列的重點是在後端,所以這邊就不使用什麼網頁框架,直接用 html & jquery 來開發~

網頁開發的話還是 VS Code 比較好用,沒安裝過的朋友可以先把它裝起來~ 原則上一直按下一步 就安裝完畢了。
VS Code安裝完成後,就可以準備進行開發了,不過為了方便後續開發的動作能夠順利進行,這邊要稍微介紹一個好用的套件

開發利器 - VS code Live Server

VS code 開發網頁時很方便的延伸模組 Live Server,可以方便又快速的建立本機伺服器。

安裝好 Live Server 後,VS code 右下角會有一個 Go Live 圖標,只要按下去就可以建立本機伺服器。

伺服器建立在 port 5001

若想調整伺服器建立的 port,則只需在 setting.json 中加入以下變數。

"liveServer.settings.port" : 5001


ngrok 設定

在介紹 login flow 時有提到,登入流程中有一個 callback 的機制是 Line 會透過我們帶入的 redirect uri 將 authorization code 送回來,要達成這個機制我們必須將網頁的伺服器位置使用 ngrok 建立與外部的連線,但免費的 ngrok authToken 一次只能啟動一個 ngrok 服務,但我們又需要同時建立兩個 poet 的連線怎麼辦? 這時就要去修改 ngrok 的設定檔了~

根據官網文件的描述,以下是各個作業系統中 ngrok configuration file 的預設位置。

  • Linux: "~/.config/ngrok/ngrok.yml"
  • MacOS (Darwin): "~/Library/Application Support/ngrok/ngrok.yml"
  • Windows: "%HOMEPATH%\AppData\Local\ngrok\ngrok.yml"

以下是 Configuration File 內容

我的 first tunnel 給網頁使用,監聽的 port 設成跟 Live Server port 一樣為 5001,second tunnel 則是原本 server 在跑的 port,帶入原本建立連線時輸入的位置即可。

version: "2"
authtoken: {你的 authToken}
tunnels:
  first: 
    addr: 5001
    proto: http
  second:
    addr: https://localhost:8080
    proto: http

輸入以下指令即可啟動 ngrok 服務,並根據 configuration file 建立通道

ngrok start --all

ngrok 啟動後的畫面,建立了兩個通道。

到這邊 vscode & live server & ngrok 都準備好了,就可以開始建立網頁頁面了~

HTML 內容

本篇要建立的頁面有兩張,一張是登入,一張是使用者資訊,網頁實作的部分我就不詳細說明了,直接貼上我的程式碼供各位參考,最下方也有 git 連結可直接下載。

  • 登入頁面 login.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>2022 iThome 鐵人賽 - 讓 C# 也能很 Social</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- jquery CDN include -->
    <script src="https://code.jquery.com/jquery-3.6.1.min.js"
        integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
    <!-- CSS include -->
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <!-- 最上方的 bar -->
    <div class="topnav">
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
        <a href="#">Line Pay</a>
    </div>
    <!-- 按鈕 -->
    <div class="container">
        <!-- 使用了三個 function,分別為:滑鼠移入時切換圖片、滑鼠移出時切換圖片、被點擊時送出登入請求 -->
        <input type="image" src="images/btn_login_base.png" onmousemove="hover(this)" onmouseout="unhover(this)"
            onclick="login()">
    </div>
    <script>
        let baseLoginApiUrl = 'https://localhost:8080/api/LineLogin/'
        // 登入 function
        function login() {
            // 向我們的伺服器送出 request,取得 Line Login 頁面網址。
            $.get({
                url: baseLoginApiUrl + 'Url' + "?redirectUrl=https://cccf-61-63-154-173.jp.ngrok.io/profile.html",
                contentType: "application/json;charset=utf-8",
                success: function (result) {
                    window.location = result; //取得網址並跳轉。
                },
            })
        }

        // 變更圖片 function
        function hover(element) {
            element.setAttribute('src', 'images/btn_login_hover.png');
        }
        function unhover(element) {
            element.setAttribute('src', 'images/btn_login_base.png');
        }
    </script>
</body>

</html>
  • 使用者資訊頁面(登入後的 redirect 頁面) profile.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>讓 C# 也能很 Social 用戶資料</title>
    <!-- jquery CDN incluer -->
    <script src="https://code.jquery.com/jquery-3.6.1.min.js"
        integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
    <!-- CSS include -->
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <div class="topnav">
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/login.html">Line Login</a>
        <a href="https://cccf-61-63-154-173.jp.ngrok.io/profile.html">User Profile</a>
        <a href="#">Line Pay</a>
    </div>
    <script>
        let baseLoginApiUrl = 'https://localhost:8080/api/LineLogin/';
        // 頁面載入時,就是登入後 Line 透過 callback 帶回資料
        // 此時開始取得使用者資料
        window.onload = function () {
            const params = new Proxy(new URLSearchParams(window.location.search), {
                get: (searchParams, prop) => searchParams.get(prop),
            });
            let code = params.code;
            let state = params.state;

            if (code == null || state == null) return;
            //省略驗證 state

            //取得 login info
            $.get({
                url: baseLoginApiUrl + `Tokens?authToken=${code}&callbackUrl=${window.location.toString().split("?")[0]}`,
                dataType: 'json',
                contentType: 'application/json',
                success: function (res) {
                    // 使用 access token 取回使用者資料
                    $.get({
                        url: baseLoginApiUrl + `Profile/${res.access_token}`,
                        dataType: 'json',
                        contentType: 'application/json',
                        success: function (res) {
                            $("#user_avatar").attr("src", res.pictureUrl);
                            $("#user_name").text('姓名 : ' + res.displayName);
                            $("#user_id").text('使用者ID : ' + res.userId);
                            $("#user_status").text('狀態文字 : ' + res.statusMessage);
                        }
                    });
                },
            })
        }
    </script>
    <center>
        <div class="container">
            <a><img id="user_avatar" src=""></image></a>
            <a><a id="user_name">姓名 : </a></a>
            <a><a id="user_id">使用者ID : </a></a>
            <a><a id="user_status"> </a></a>
        </div>
    </center>
</body>
</html>
  • Style.css
* {
    box-sizing: border-box;
    font-family: Arial, Helvetica, sans-serif;
}

body {
    margin: 0;
    font-family: Arial, Helvetica, sans-serif;
}

/* Style the top navigation bar */
.topnav {
    overflow: hidden;
    background-color: #333;
    height: 10%;
}

/* Style the topnav links */
.topnav a {
    float: left;
    display: block;
    color: #f2f2f2;
    text-align: center;
    padding: 14px 16px;
    text-decoration: none;
}

/* Change color on hover */
.topnav a:hover {
    background-color: #ddd;
    color: black;
}

/* Style the content */
.container {
    background-color: #ffffff;
    padding: 10px;
    width: 100%;
    height: 80%;
    align-items: center;
    /* Should be removed. Only for demonstration */
}

/* Style the footer */
.footer {
    background-color: #f1f1f1;
    padding: 10px;
    height: 10%;
}

.container input[type=image] {
    width: 150px;
    height: auto;
}

#user_avatar {
    width: 150px;
    height: auto;
    border-radius: 50%;
}

.container a {
    display: block;
}
  • 專案結構

  • 頁面製作完成後需要到 Login Channel 中註冊 callback url。

    最多可以註冊到 1000 個 callback url,只需換行即可,只有已註冊的 url 才能作為 redirect url 傳給 line。

測試

伺服器與前端頁面都跑起來後就進行流程的實測

  1. 登入畫面 login.html

  2. 跳轉至 Line 登入畫面

  3. 登入成功後 Redirect 至 profile.html 並帶出使用者資料。

結語

這篇完成了基本的 Line Login 功能,透過了內建的功能,讓我們的網頁程式能夠很輕鬆地取得使用者資料。
在使用者透過Line LIFF Browser來開啟網頁時,就不用像之前另開視窗般地重覆登入 ~ 而是無縫接軌般的運作
這部份,在使用者體驗上 則是大大的加分 ~~~~
舉個簡單的例子,https://liff.line.me/1657272498-L5vlOQpl 供大家參考。(*要從LINE點擊網址,開啟LIFF 才看的出效果喔)

不過,提醒一下 ~
使用者資料隱私安全這一部份一直以來都是很重要的,所以 Line 有提供一份資安項目的檢查清單 security checklist 提供給大家參考,裏面列出對資安要注意的項目,如 access token的設定值,url轉址的設定,參數的檢核…等等。
確認這些項目無誤,也算對在網路上傳遞的資料多了一層的保障,有興趣的朋友們可以再自行參考囉~

回到這篇文章的內容,因為同時有前端、後端的內容,其實訊息量也算不少 ~
這邊先一一提供給大家參考,日後各位可以再慢慢的消化~~有任何疑問歡迎留言告知。

下一篇會介紹使用 ID Token 的方式取的使用者資料,再看看還需要補充什麼內容就下一篇再決定囉,撒花 ~
各位下一篇見, 81 ~

腦經急轉彎

  • 登入時帶入 authorization url 的 state 參數應該要是隨機的才正確,為什麼呢?要怎麼做?

範例程式碼

如果想要參考今天範例程式碼的部份,下面是 Git Repo 連結,方便大家參考。


上一篇
[Day 19] .NET 6 C# 與 Line Services API 開發 - Rich Menu 製作(四) 總整理
下一篇
[Day 21] .NET 6 C# 與 Line Services API 開發 - Line Login API (二) 透過 ID Token 取得 User Profile
系列文
讓 C# 也可以很 Social - 在 .NET 6 用 C# 串接 LINE Services API 的取經之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言