iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
Modern Web

WebGIS入門學習 - 以Openlayers實作系列 第 23

Day 23. API登入權限控管機制 #4:前端套用帳號驗證與API權限管控

  • 分享至 

  • xImage
  •  

前言

今天是 API登入權限控管機制 系列最後一天,昨天已經建立好帶有權限的API了,今天我們就要來修改前端的功能!!

將登入機制導入並且修改介接的API,最後紀錄登入的資訊,這一部份就完成了!

今天的大綱

  1. 新增登入頁
  2. 輸入帳密登入後取得Token
  3. 修正登入後首頁的權限辨識
  4. 修正介接WebAPI的資訊
  5. 登入Log紀錄

1. 新增登入頁

創建一個登入頁 WebLogin.aspx (假設一律要登入才能使用),讓使用者輸入帳號密碼進行登入,使用aspx是因為可以讓帳號驗證function runat="server" code behind 寫在後端 "WebLogin.aspx.cs"

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebLogin.aspx.cs" Inherits="OLMap.WebLogin" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>OLMap Demo登入</title>
    <link rel="stylesheet" href="css/login.css">
</head>
<body class="login-page">
    <form id="form1" runat="server" defaultbutton="LoginBtn">
        <div class="center">
            <div class="title">
                OLMap Demo
            </div>
            <div class="line"></div>
            <div id="inputs">
                <table id="otherLogin">
                    <tr>
                        <td colspan="2" class="login-hint">請輸入帳號密碼</td>
                    </tr>
                    <tr>
                        <td>帳號</td>
                        <td>
                            <asp:TextBox ID="txt_user" placeholder="請輸入帳號" runat="server">
                            </asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" align="right">
                            <asp:RequiredFieldValidator ID="UserNameRequired" runat="server" ControlToValidate="txt_user" ErrorMessage="*帳號必填" ToolTip="必須提供使用者名稱。" ValidationGroup="Login2" CssClass="warning-text">
                            </asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <td>密碼</td>
                        <td>
                            <asp:TextBox ID="txt_pass" placeholder="請輸入密碼" runat="server" TextMode="Password"></asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" align="right">
                            <asp:RequiredFieldValidator ID="PasswordRequired" runat="server" ControlToValidate="txt_pass" ErrorMessage="*密碼必填" ToolTip="必須提供密碼。" ValidationGroup="Login2" CssClass="warning-text">
                            </asp:RequiredFieldValidator>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2">
                            <asp:Button ID="LoginBtn" Text="登入" CssClass="button-act" runat="server" ValidationGroup="Login2" OnClick="LoginBtn_Click" />
                        </td>
                    </tr>
                </table>
            </div>
        </div>
    </form>
</body>
</html>

登入頁樣式寫在 css/login.css ,給它一個漂漂亮亮的畫面。

html,body {
    position: relative;
    width: 100%;
    height: 100%;
    padding: 0;
    min-height: 600px;
    margin: 0;
    overflow: auto;
    font-family: microsoft jhenghei;
}
body {
    background: linear-gradient( 135deg, #2af598, #009efd);
    background-size: cover;
    background-position: center center;
    background-repeat: no-repeat;
}
table {
    width: 100%;
    margin-bottom: 40px;
}
table:last-child {
    margin-bottom: 0;
}
.center {
    position: absolute;
    left: 0;
    right: 0;
    margin-left: auto;
    margin-right: auto;
    top: 50%;
    transform: translate(0, -50%);
    width: 500px;
    background-color: rgba(0, 0, 0, 0.35);
    border-radius: 5px;
    padding-bottom: 30px;
}
#inputs {
    padding: 20px 50px;
    color: white;
}
#inputs input {
    border: none;
    background-color: rgba(0, 0, 0, 0.5);
    padding: 10px;
    border-radius: 5px;
    color: white;
    width: 100%;
    box-sizing: border-box;
    font-family: microsoft jhenghei;
}
::placeholder {
    /* Chrome, Firefox, Opera, Safari 10.1+ */
    color: rgba(255, 255, 255, 0.7);
    opacity: 1; /* Firefox */
}

:-ms-input-placeholder {
    /* Internet Explorer 10-11 */
    color: rgba(255, 255, 255, 0.7);
}

::-ms-input-placeholder {
    /* Microsoft Edge */
    color: rgba(255, 255, 255, 0.7);
}
.title {
    text-align: center;
    padding: 30px;
    padding-bottom: 10px;
    color: white;
    font-size: 30px;
    font-weight:800;
}
.line {
    width: 80%;
    height: 1px;
    background: linear-gradient(135deg, #00eaff, #0047ff);
    position: relative;
    left: 50%;
    margin-left: -40%;
}
.button-act {
    background: linear-gradient(135deg, #04befe, #4481eb);
    padding: 15px !important;
    font-size: 20px;
    cursor: pointer;
    box-shadow: 5px 5px 25px rgba(0, 0, 0, 0.2);
    transition: 0.5s;
}
.button-act:hover {
    box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
}
.warning-text {
    font-size: 14px;
    color: #fba503;
}
.login-hint {
    font-weight: bold;
    padding-bottom: 16px;
    opacity: 0.8;
    /*text-decoration: underline;*/
}

登入頁示意圖:
https://ithelp.ithome.com.tw/upload/images/20201001/20108631stkuTABZu8.png

2. 輸入帳密登入後取得Token

在登入頁輸入帳號密碼,經過驗證成功後,會回傳token並存入 session ,後續API都要靠這組token來進行驗證。
Web.config 新增API路徑網址

<appSettings>
	<add key="OLMapAPI" value="http://localhost/OLMapAPI"/>
</appSettings>

WebLogin.aspx.cs 新增頁面載入 Page_Load 事件,登出並刪除所有 session (含token和username)

protected void Page_Load(object sender, EventArgs e)
{
    FormsAuthentication.SignOut();
    Session["token"] = Session["username"] = null;
    Session.RemoveAll();
}

WebLogin.aspx.cs 新增 validatesApiUser() 函式驗證帳號密碼,若驗證成功則回傳true,將token存入session;驗證失敗回傳false。

public static bool validatesApiUser(string userInput, string passwordInput)
{
    string url = ConfigurationManager.AppSettings["OLMapAPI"] + "/api/Auth/validatesApiUser";

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    request.Method = "POST";
    request.ContentType = "application/json; charset=utf-8";

    using (var streamWriter = new StreamWriter(request.GetRequestStream()))
    {
        string json = "{\"userid\":\"" + userInput + "\",\"password\":\"" + passwordInput + "\"}";
        streamWriter.Write(json);
    }

    //API回傳的字串
    string responseStr = "";
    //發出Request
    HttpWebResponse res;
    try
    {
        res = (HttpWebResponse)request.GetResponse();
    }
    catch (WebException ex)
    {
        res = (HttpWebResponse)ex.Response;
    }
    StreamReader sr = new StreamReader(res.GetResponseStream(), Encoding.UTF8);
    responseStr = sr.ReadToEnd();

    JObject obj = (JObject)JsonConvert.DeserializeObject(responseStr);
    System.Web.HttpContext.Current.Session["token"] = Convert.ToString(obj["token"]);
    if (Convert.ToString(obj["token"]) == "")
    {
        return false;
    }
    else
    {
        return true;
    }
}

新增登入按鈕點擊事件 LoginBtn_Click(),當按下登入按鈕時執行 validatesApiUser(),塞回 isValid

  • 若驗證成功則 isValid = true,將 username 存入session,並將網頁導向 map.aspx
  • 若驗證成功則 isValid = false,則 alert 帳號密碼錯誤
protected void LoginBtn_Click(object sender, EventArgs e)
{
    bool isValid = false;
    string userInput = txt_user.Text.TrimEnd();
    string passwordInput = txt_pass.Text.TrimEnd();

    isValid = validatesApiUser(userInput, passwordInput);
    Session["username"] = userInput;

    if (isValid)
    {
        Response.Redirect("~/map.aspx");
    }
    else
    {
        Session["username"] = null;
        this.Page.Controls.Add(new LiteralControl("<script language='javascript'>alert('帳號密碼錯誤');</script>"));
    }
}

3. 修正登入後首頁的權限辨識

在首頁 map.aspx 將session存入 localStorage 方便存取,但是如果全部頁面都是ASP.NET架構,就不需要存在localStorage。

<script type="text/javascript">
	localStorage["token"] = "<%=Session["token"]%>";
    localStorage["username"] = "<%=Session["username"]%>";
</script>

修正 map.aspx.cs ,若沒有 Session 內沒有儲存 tokenusername 則跳轉至登入頁 WebLogin.aspx

protected void Page_Load(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(Session["token"]?.ToString()) || string.IsNullOrEmpty(Session["username"]?.ToString()))
    {
        Response.Redirect("~/WebLogin.aspx");
    }
}

Web.config 內將首頁改成 WebLogin.aspx,所有使用者都要先登入才可使用圖台。

<defaultDocument>
  <files>
    <add value="WebLogin.aspx" />
  </files>
</defaultDocument>

改完上述以後,可以順利登入並取得token存入session,亦可顯示圖台畫面。
但是若要使用介接本次的Web API的相關功能,則會產生以下錯誤:已拒絕此要求的授權,所以下面我們要更改介接API的 request 資訊。

https://ithelp.ithome.com.tw/upload/images/20201001/20108631hiATt8IEfN.png

4. 修正介接WebAPI的資訊

修正使用api各支的 header 要帶入token才可順利存取,以 getlayer() 為例,需在request header 加入 "Authorization" 參數,值為先前存入 localStorage 的token:

$.ajax({
    type: "GET",
    url: config_OLMapWebAPI + "/Layers/getLayerResource",
    headers: {
        "Authorization": localStorage["token"]
    },
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    success: function (d) {
        //var data = $.parseJSON(d.d);
        var data = d;
        console.log(data);
        var layerlisthtml = "";
        TOCArray = [];
        $.each(data, function (index, item) {
            loadLayer(item);
            if (typehasExtent.includes(item.DataType)) {
                layerlisthtml += '<div class="item"><div class="ui checkbox"><input type="checkbox" name="example" onclick="toc.toggleTocLayer(\'' + item.LayerID + '\', this)"><label></label></div><div class="content"><a class="header">' + item.LayerTitle + '</a><div class="description">' + item.DataType + '</div><img class="layerBtns info" src="images/TOCpage/info.png" title="點擊定位圖層" onclick="toc.zoomTocLayer(\'' + item.LayerID + '\')"></div></div>';
            } else {
                layerlisthtml += '<div class="item"><div class="ui checkbox"><input type="checkbox" name="example" onclick="toc.toggleTocLayer(\'' + item.LayerID + '\', this)"><label></label></div><div class="content"><a class="header">' + item.LayerTitle + '</a><div class="description">' + item.DataType + '</div></div></div>';
            }
            TOCArray.push(item);
        });
        $("#layerlist").html(layerlisthtml);
    },
    error: function (jqXHR, exception) {
        ajaxError(jqXHR, exception);
    }
});

可以由localStorage 看到token資訊,更改完成後即可順利介接API。
https://ithelp.ithome.com.tw/upload/images/20201001/20108631NH83dnK47u.png

接著,為了讓使用者辨別方便,在頁面上顯示 使用者的ID

<a style="float: right;color: white;margin-top: 20px;margin-right: 20px;">使用者:<span id="useridshow"></span></a>
$("#useridshow").html(localStorage["username"]);

https://ithelp.ithome.com.tw/upload/images/20201002/20108631QVBuU9eNfM.png

5. 登入Log紀錄

每一次的登入都應該有log紀錄,token的紀錄在超過有效期限後便會被清除,因此需要另外紀錄登入的使用者與時間資訊。
新增登入記錄資料表 dbo.CRM_Record ,包含登入類型、登入名稱、登入時間和備註。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CRM_Record](
	[item] [smallint] IDENTITY(1,1) NOT NULL,
	[LoginType] [smallint] NULL,
	[LoginName] [nvarchar](20) NULL,
	[AccessTime] [smalldatetime] NULL,
	[Remark] [nvarchar](100) NULL
) ON [PRIMARY]
GO

修改 OLMapAPI 裡建立token的api:createToken()
當建立token的同時記錄紀錄登入的資訊到 CRM_Record 資料表內。

private string createToken(string userid, string type)
{
    string salt = GetRandomString(10);
    string token = generateToken(userid, salt);
    delExpiredToken();
    string sqlstr = @"INSERT INTO sys_tokens (userid,type,token,ip,issuedOn,expiredOn,salt) Values (@userid,@type,@token,@ip,@issuedOn,@expiredOn,@salt)";
    // 新增紀錄登入資訊
    sqlstr += @"INSERT INTO CRM_Record (LoginName,AccessTime,Remark) Values (@userid,GETDATE(),'OLMap')";
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand(sqlstr, conn);
    conn.Open();
    cmd.Parameters.AddWithValue("@userid", userid);
    cmd.Parameters.AddWithValue("@type", type);
    cmd.Parameters.AddWithValue("@token", token);
    cmd.Parameters.AddWithValue("@ip", getClientIP());
    cmd.Parameters.AddWithValue("@issuedOn", DateTime.Now);
    cmd.Parameters.AddWithValue("@expiredOn", DateTime.Now.AddHours(8)); //8小時後刪除
    cmd.Parameters.AddWithValue("@salt", salt);
    SqlDataReader dr = cmd.ExecuteReader();
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
    return "OLMapAPI " + token;
}

小結

所有的權限控管機制功能都完成拉!!!加起來前後耗時4天的時間,鐵人賽到今天第23天,我個人覺得已經可以完成一個WebGIS的網頁了。
目前介紹的權限控管為最簡單的只分成有權限和無權限,而各位在真正建立一個專案時可以考慮:

  1. 一般使用者也可以查詢,登入後可使用的功能更多。
  2. 一般使用者可開一個永久的 Anonymous 的token,一樣走token驗證機制,只是他的權限是Anonymous。
  3. 不同角色有不同權限。
  4. 依照權限從資料庫撈出功能列表與顯示資訊,而非從前端隱藏。
  5. 驗證的機制應該更多層檢驗,而非只有token而已,可嘗試根據token鎖ip 等等的。

有很多很多東西都可以玩,在有各種想法以後就可以慢慢地去學習去實現,這大概就是做side project的樂趣吧!

明天回歸WebGIS的部分!我們來學怎麼在地圖上繪圖~


上一篇
Day 22. API登入權限控管機制 #3:Token編碼與加密設計
下一篇
Day 24. 如何在地圖上畫Pokemon #1:繪圖工具的建立
系列文
WebGIS入門學習 - 以Openlayers實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言