Line Login using ASP.net MVC 5
前言
Line Login API目前沒有提供Javascript SDK,只有提供單純的HTTP Web API讓各種程式語言發出Request存取
雖然是可以自己完全打造純Javascript的登入流程,但是Line Login會需要搭配Channel secret (Client secret) 應用程式私鑰
這種東西就不方便直接曝露在前端Javascript了
我看網路上很多教學文章的Line Login都是Server端在處理作業,導頁來導頁去,沒有一個popup window範例,所以乾脆自己實作
而且我工作上也碰過Line登入頁不給你嵌入iframe,他們的網站有設定 X-Frame-Options 回應標頭
此時popup window的登入方式就是一個解決方案
使用popup window來實作第三方登入另一個好處是網頁不會導頁來導頁去,如果你的畫面裡剛好有表單、手風摺琴、頁籤這些UI元件,就是很好的組合
因為使用者填寫到一半的表單會因為導頁後,畫面上填寫的資料都被清空
前置作業
Line Developers Console網址:https://developers.line.biz/console/
Line登入應用程式的申請配置我懶得擷圖,請參考其他網友文章:[筆記]Line Login 使用MVC C# | 遇見零壹魔王- 點部落
要存取用戶email權限的話,應用程式設定請參考董大偉老師的文章:使用C#開發Linebot(30) – 使用LINE Login時取得用戶email
目前Line Login官方API文件量不多,難易度也還好,本文章大部份採用官方建議撰寫程式(資安上):Integrating LINE Login with your web app
2019-10-22 補充
不同Provider的Line Login Channel 即使前端用戶同一個人登入,取得的UserID會不一樣
但相同 Provider 底下的Line Login channel 前端用戶同一個人登入,則取得的UserID會相同,這特性和 FB Login App有點類似,要留意
撰寫程式碼
先看Controller,說明在註解裡
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace Web_GooglePeopleAPITest.Controllers
{
public class HomeController : Controller
{
//以下自行修改從WebConfig讀取
string redirect_uri = "https://localhost:44385/Home/AfterLineLogin";
string client_id = "165333****";
string client_secret = "8f38edb*******f36517430075d1";
/// <summary>
/// 主要Demo畫面
/// </summary>
/// <returns></returns>
public ActionResult DemoView()
{
return View();
}
/// <summary>
/// 產生新的LineLoginUrl
/// </summary>
/// <returns></returns>
public ActionResult GetLineLoginUrl()
{
if (Request.IsAjaxRequest()==false)
{
return Content("");
}
//只讓本機Ajax讀取LineLoginUrl
//state使用隨機字串比較安全
//每次Ajax Request都產生不同的state字串,避免駭客拿固定的state字串將網址掛載自己的釣魚網站獲取用戶的Line個資授權(CSRF攻擊)
string state = Guid.NewGuid().ToString();
TempData["state"] = state;//利用TempData被取出資料後即消失的特性,來防禦CSRF攻擊
//如果是ASP.net Form,就改成放入Session或Cookie,之後取出資料時再把Session或Cookie設為null刪除資料
string LineLoginUrl =
$@"https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&state={state}&scope={HttpUtility.UrlEncode("openid profile email")}";
//scope給openid是程式為了抓id_token用,設email則為了id_token的Payload裡才會有用戶的email資訊
return Content(LineLoginUrl);
}
/// <summary>
/// 使用者在Line網頁登入後的處理,接收Line傳遞過來的參數
/// </summary>
/// <param name="state"></param>
/// <param name="code"></param>
/// <param name="error"></param>
/// <param name="error_description"></param>
/// <returns></returns>
public ActionResult AfterLineLogin(string state, string code,string error,string error_description)
{
if (!string.IsNullOrEmpty(error))
{//用戶沒授權你的LineApp
ViewBag.error = error;
ViewBag.error_description = error_description;
return View();
}
if (TempData["state"] == null)
{//可能使用者停留Line登入頁面太久
return Content("頁面逾期");
}
if (Convert.ToString(TempData["state"]) != state)
{//使用者原先Request QueryString的TempData["state"]和Line導頁回來夾帶的state Querystring不一樣,可能是parameter tampering或CSRF攻擊
return Content("state驗證失敗");
}
if (Convert.ToString(TempData["state"])==state)
{//state字串驗證通過
//取得id_token和access_token:https://developers.line.biz/en/docs/line-login/web/integrate-line-login/#spy-getting-an-access-token
string issue_token_url = "https://api.line.me/oauth2/v2.1/token";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(issue_token_url);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
//必須透過ParseQueryString()來建立NameValueCollection物件,之後.ToString()才能轉換成queryString
NameValueCollection postParams = HttpUtility.ParseQueryString(string.Empty);
postParams.Add("grant_type", "authorization_code");
postParams.Add("code", code);
postParams.Add("redirect_uri", this.redirect_uri);
postParams.Add("client_id", this.client_id);
postParams.Add("client_secret", this.client_secret);
//要發送的字串轉為byte[]
byte[] byteArray = Encoding.UTF8.GetBytes(postParams.ToString());
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(byteArray, 0, byteArray.Length);
}//end using
//API回傳的字串
string responseStr = "";
//發出Request
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
{
responseStr = sr.ReadToEnd();
}//end using
}
LineLoginToken tokenObj = JsonConvert.DeserializeObject<LineLoginToken>(responseStr);
string id_token = tokenObj.id_token;
//方案總管>參考>右鍵>管理Nuget套件 搜尋 System.IdentityModel.Tokens.Jwt 來安裝
var jst = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(id_token);
LineUserProfile user = new LineUserProfile();
//↓自行決定要從id_token的Payload中抓什麼user資料
user.userId = jst.Payload.Sub;
user.displayName = jst.Payload["name"].ToString();
user.pictureUrl = jst.Payload["picture"].ToString();
if (jst.Payload.ContainsKey("email") && !string.IsNullOrEmpty(Convert.ToString(jst.Payload["email"])))
{//有包含email,使用者有授權email個資存取,並且用戶的email有值
user.email = jst.Payload["email"].ToString();
}
string access_token = tokenObj.access_token;
ViewBag.access_token = access_token;
#region 接下來是為了抓用戶的statusMessage狀態消息,如果你不想要可以省略不發出下面的Request
//Social API v2.1 Getting user profiles
//https://developers.line.biz/en/docs/social-api/getting-user-profiles/
//取回User Profile
string profile_url = "https://api.line.me/v2/profile";
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(profile_url);
req.Headers.Add("Authorization", "Bearer " + access_token);
req.Method = "GET";
//API回傳的字串
string resStr = "";
//發出Request
using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
{
using (StreamReader sr = new StreamReader(res.GetResponseStream(), Encoding.UTF8))
{
resStr = sr.ReadToEnd();
}//end using
}
LineUserProfile userProfile = JsonConvert.DeserializeObject<LineUserProfile>(resStr);
user.statusMessage = userProfile.statusMessage;//補上狀態訊息
#endregion
ViewBag.user = JsonConvert.SerializeObject(user,new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Formatting = Formatting.Indented
});
}//end if
return View();
}
/// <summary>
/// 徹銷Line Login,目前感覺不出差別在哪= =a,等待API改版
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult RevokeLineLoginUrl(string access_token)
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create("https://api.line.me/oauth2/v2.1/revoke");
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
//必須透過ParseQueryString()來建立NameValueCollection物件,之後.ToString()才能轉換成queryString
NameValueCollection postParams = HttpUtility.ParseQueryString(string.Empty);
postParams.Add("access_token", access_token);
postParams.Add("client_id", this.client_id);
postParams.Add("client_secret", this.client_secret);
//要發送的字串轉為byte[]
byte[] byteArray = Encoding.UTF8.GetBytes(postParams.ToString());
using (Stream reqStream = req.GetRequestStream())
{
reqStream.Write(byteArray, 0, byteArray.Length);
}//end using
//API回傳的字串
string responseStr = "";
//發出Request
using (HttpWebResponse response = (HttpWebResponse)req.GetResponse())
{
using (StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
{
responseStr = sr.ReadToEnd();
}//end using
}
return Content(responseStr);
}
}
public class LineLoginToken
{
public string access_token { get; set; }
public int expires_in { get; set; }
public string id_token { get; set; }
public string refresh_token { get; set; }
public string scope { get; set; }
public string token_type { get; set; }
}
public class LineUserProfile
{
public string userId { get; set; }
public string displayName { get; set; }
public string pictureUrl { get; set; }
public string statusMessage { get; set; }
public string email { get; set; }
}
}
要傳給Line網站的參數說明↓
↓DemoView.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Demo Line Login</title>
</head>
<body>
<a href="javascript:openPopupWindow()">
點我Line Login
</a>
|
<a href="javascript:revokeLineApp();">
點我撤銷Line的授權
</a>
<hr />
<div id="result"></div>
<!--引用jQuery-->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
let LineLoginUrl = "@Url.Action("GetLineLoginUrl","Home")";
let RevokeLineLoginUrl = "@Url.Action("RevokeLineLoginUrl","Home")";
</script>
<script type="text/javascript">
function openPopupWindow() {
$("#result").html("");//清空顯示結果
//另開popup window前,每次都取得新的LineLoginUrl(每次Url的state參數都不一樣)
$.ajax({
url: LineLoginUrl,
method: "get",
success: function (url) {
window.open(url, "_blank", "width=800px,height=600px");
}, error: function (xhr) {
console.log(xhr);
}
});
}
var access_token = "";
function revokeLineApp() {
if (access_token === "")
{
$("#result").html("請先登入Line");
} else {
$.ajax({
url: RevokeLineLoginUrl,
method: "post",
data: { access_token, access_token },
success: function (result) {
$("#result").html("已徹銷Line的授權<hr/>" + result);
}, error: function (xhr) {
console.log(xhr);
}
});
}
}
</script>
</body>
</html>
↓AfterLineLogin.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>AfterLineLogin</title>
</head>
<body>
@if (!string.IsNullOrEmpty(Convert.ToString(ViewBag.error)))
{
<h1>@ViewBag.error</h1>
<h2>@ViewBag.error_description</h2>
}
@if (!string.IsNullOrEmpty(Convert.ToString(ViewBag.user)))
{
<script type="text/javascript">
window.onload = function () {
let userObj = @Html.Raw(Convert.ToString(ViewBag.user));
window.opener.document.getElementById("result").innerHTML = JSON.stringify(userObj);
window.opener.access_token = "@Html.Raw(ViewBag.access_token)";
window.close();
}
</script>
}
</body>
</html>
程式執行結果
其實
Getting an access token (POST https://api.line.me/oauth2/v2.1/token):https://developers.line.biz/en/docs/line-login/web/integrate-line-login/#spy-getting-an-access-token
Social API v2.1 Get user profile:https://developers.line.biz/en/reference/social-api/#get-user-profile
↑ 兩邊同樣都可以取得userID(Line系統識別用戶的ID,不是讓別人加好友的那個ID)、name、pictureUrl
兩邊回傳的用戶個資差異在POST https://api.line.me/oauth2/v2.1/token 可以額外取得用戶email,Social API v2.1 Get user profile 則可以額外取得用戶的 "statusMessage" 狀態訊息
下圖是POST https://api.line.me/oauth2/v2.1/token 的回應內容
下圖是 Social API v2.1 Get user profile:https://developers.line.biz/en/reference/social-api/#get-user-profile 的回應內容
一些眉眉角角
Logging out users 請參考:https://developers.line.biz/en/docs/social-api/logging-out-users ,不過有沒有讓使用者登出,目前我感覺不出差異在哪,等待Line API改版看看
Line登入網址可以多加一個prompt=consent參數,例如:https://access.line.me/oauth2/v2.1/authorize?response_type=code&prompt=consent.....(略
↑ 如此使用者登入Line的時候,就會永遠顯示同意畫面
但是以上,我已試過就算你把使用者Logout,而且Line登入網址多加prompt=consent參數
如果使用者曾經授權過email讓你存取,他下次重新登入仍然不能變更是否授權email權限給你
↓目前官網我只找到使用者唯有在他修改過自己email後,他才能重新選擇是否要授權email給你的App存取
最終開發完成,當你的網站丟到PRD正式環境後,記得順便Published你的Line應用程式,如此redirect_uri裡設定的正式機DomainName才能生效,如果你忘記這回事也沒關係
因為當你遇到應用程式仍在Developing錯誤訊息時,就知道要把Line應用程式Published了XD
結語
Line API目前看來看去還無法取得用戶的電話、生日
個人覺得可以取得用戶個資數量的社群API:Google > Facebook > Line