iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
1
Modern Web

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

Day 21. API登入權限控管機制 #2:帳號驗證與token回傳流程建立

  • 分享至 

  • xImage
  •  

接續昨天的,前言什麼的不重要,也沒有人想看!

今天的大綱

  1. 驗證帳號判斷是否可使用API,並回傳token
  2. Message Handler:NLog 記錄 WebAPI 的日誌 (Optional)
  3. Message Handler:Request Header 資訊獲取與token驗證
  4. 新增filter並註冊

1. 驗證帳號判斷是否可使用API,並回傳token

昨天的專案我們已經完成了基本的帳號的Function和API的建立,可以方便管理者管理使用者帳戶的新增、編輯、刪除,而今天我們就要來建置,當使用者驗證完成帳號後,會回傳一組token回去,利用此組token可以在一定時間內在權限內暢行無阻 (?
他就像是 臨時的身分證,有了這張證,當每次來跟我要東西的時候,都會先去對對看身分證是否符合本人,若符合就給你資訊。

STEP1. 首先我們要先建立Model:新增 Models/auth/loginInfo.cs 類別檔。

  • loginInfo:登入輸入資訊
  • tokenObj:權限設定輸入資訊
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace OLMapAPI.Models.auth
{
    public class loginInfo
    {
        public string userid;
        public string password;
        public loginInfo(string _userid, string _password)
        {
            userid = _userid;
            password = _password;
        }
        public loginInfo() { }
    }

    /// <summary>
    /// 權限設定input
    /// </summary> 
    public class tokenObj
    {
        public string status;
        public string token;
        public tokenObj(string _status, string _token)
        {
            status = _status;
            token = _token;
        }
        public tokenObj() { }
    }
}

STEP2. 建立相對應的function:建立 Infrastructure/auth/authFunc.cs 授權相關程式碼檔。
整體驗證步驟如下:

  1. 使用者輸入帳號密碼,傳入API
  2. 使用 validatesApiUser() 驗證帳號密碼
    1. 利用帳號從 sys_apiUser 資料表檢查該帳號是否有被lock。
      • 若沒被lock住,則進行帳號的驗證。
      • 或有被lock住,則回傳錯誤訊息。
    2. 將回傳的 aspnetUserId 根據昨天建立的功能去new一個 UserManager() 物件來撈取 user
    3. 將user資料和輸入的密碼使用 CheckPassword() 來檢查是否正確。
    4. 若正確則執行 createToken() ,建立並回傳 tokenObj 資訊。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.Identity;
using OLMapAPI.Models;
using System.Data;
using System.Configuration;
using System.Web.UI.WebControls;
using System.Data.SqlClient;
using OLMapAPI.Models.auth;

namespace OLMapAPI.Infrastructure.auth
{
    public class authFunc
    {
        /// <summary>
        /// apiuser-驗證帳號
        /// </summary>
        public tokenObj validatesApiUser(loginInfo login)
        {
            string sqlstr = @"SELECT userId,aspnetUserId,aspnetPassword FROM sys_apiUser where lockYN='0' and userId=@userId";
            SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
            SqlCommand cmd = new SqlCommand(sqlstr, conn);
            conn.Open();
            cmd.Parameters.AddWithValue("@userId", login.userid);
            SqlDataReader dr = cmd.ExecuteReader();
            string userId = "";
            string aspnetUserId = "";
            while (dr.Read())
            {
                userId = dr["userId"].ToString();
                aspnetUserId = dr["aspnetUserId"].ToString();
            }
            dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();

            if (userId != "" && aspnetUserId != "" && login.password != "")
            {
                var manager = new UserManager();
                ApplicationUser user = manager.FindByName(aspnetUserId);
                bool YN = manager.CheckPassword(user, "apiUser@_" + login.password);
                if (YN)
                {
                    string token = createToken(aspnetUserId, "apiuser");
                    tokenObj tokenobj = new tokenObj("success", token);
                    return tokenobj;
                }
                else
                {
                    tokenObj error = new tokenObj("error-no dotnet user", "");
                    return error;
                }
            }
            else
            {
                tokenObj error = new tokenObj("error-no user", "");
                return error;
            }
        }
        // token相關功能
    }
}

STEP3. 接續STEP2進行token相關功能的建置。

  1. 首先一進來就先run delExpiredToken(),將資料表內所有超過有效期限的token都刪除。
  2. 接著利用 createToken() 來進行token的建立。
    • 這支呼叫了 generateToken() 進行建立。
    • 今天的token先以亂數 GetRandomString() 作預設,明天會再講解token的加密設計。
    • 利用 getClientIP() 去撈取ip資訊。
    • 將建好的token寫入 sys_tokens 資料表,並設計有效期限只有8小時 (可自行設定)。
    • 回傳token。
  3. 每次執行api都要帶token,建置 validatesToken() 來驗證其有效性,並回傳 userid
// token相關功能
private string createToken(string userid, string type)
{
    string token = generateToken();
    delExpiredToken();
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand("INSERT INTO sys_tokens (userid,type,token,ip,issuedOn,expiredOn) Values (@userid,@type,@token,@ip,@issuedOn,@expiredOn)", 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小時後刪除
    SqlDataReader dr = cmd.ExecuteReader();
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
    return token;
}

private void delExpiredToken()
{
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand("DELETE sys_tokens Where GETDATE() > expiredOn", conn);
    conn.Open();
    SqlDataReader dr = cmd.ExecuteReader();
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
}

protected string generateToken()
{
    return GetRandomString(10);
}

public static string GetRandomString(int length)
{
    Random r = new Random();
    string code = "";

    for (int i = 0; i < length; ++i)
        switch (r.Next(0, 3))
        {
            case 0: code += r.Next(0, 10); break;
            case 1: code += (char)r.Next(65, 91); break;
            case 2: code += (char)r.Next(97, 122); break;
        }
    return code;
}

protected string getClientIP()
{
    //判所client端是否有設定代理伺服器
    //if (HttpContext.Current.Request.ServerVariables["HTTP_VIA"] == null)
    //    return HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"].ToString();
    //else
    //    return HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString();
    string VisitorsIPAddr = string.Empty;
    if (HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null)
    {
        VisitorsIPAddr = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString();
    }
    else if (HttpContext.Current.Request.UserHostAddress.Length != 0)
    {
        VisitorsIPAddr = HttpContext.Current.Request.UserHostAddress;
    }
    return VisitorsIPAddr;
}

/// <summary>
/// 驗證token
/// </summary>
public string validatesToken(string token)
{
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString);
    SqlCommand cmd = new SqlCommand("select userid from sys_tokens where token=@token and  GETDATE()<=expiredOn", conn);
    conn.Open();
    cmd.Parameters.AddWithValue("@token", token);
    SqlDataReader dr = cmd.ExecuteReader();
    string userid = "";
    while (dr.Read())
    {
        userid = dr["userid"].ToString();
    }
    dr.Close(); dr.Dispose(); conn.Close(); conn.Dispose();
    return userid;
}

STEP4. 新增Controller:新增 Authcontroller.cs 頁面。
就只有一個API,validatesApiUser() 使用已註冊的帳號密碼取得token,而且這隻API不用經過權限驗證。
這隻就是用來取得Token的,所以介接這支API不需要輸入token。

using OLMapAPI.Infrastructure.auth;
using OLMapAPI.Models.auth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace OLMapAPI.Controllers
{
    /// <summary>
    /// VS驗證
    /// </summary>
    //[Authorize]
    [RoutePrefix("api/Auth")]
    public class AuthController : ApiController
    {
        /// <summary>
        /// 由帳號密碼返回token
        /// </summary>
        /// <remarks>使用已註冊的帳號密碼取得token</remarks>
        /// <response code="200">OK</response>
        /// <response code="400">Not found</response>
        //介接api用的帳號,僅提供token返回,不提供token取得資訊,該token僅可使用一般API
        [Route("validatesApiUser")]
        [HttpPost, AllowAnonymous]
        public tokenObj validatesApiUser(loginInfo login)
        {
            authFunc auth = new authFunc();
            tokenObj token = auth.validatesApiUser(login);
            return token;
        }
    }
}

2. Message Handler:NLog 記錄 WebAPI 的日誌 (Optional)

這邊先穿插一段用來記錄API日誌的套件NLog,使用NuGet安裝NLog套件

NLog is a flexible and free logging platform for various .NET platforms, including .NET standard. NLog makes it easy to write to several targets. (database, file, console) and change the logging configuration on-the-fly.

因為NLog講下去也是落落長,在這邊就不太詳細說明,請參考:

新增 NLog.config 裡面紀錄儲存路徑、格式等相關設定;這部分是直接參考上述第一篇。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true">      

  <!--[變數] 文字樣板 -->
  <variable name="Layout" value="${longdate} | ${level:uppercase=true} | ${logger} | ${message} ${newline}"/>
  <variable name="LayoutFatal" value="${longdate} | ${level:uppercase=true} | ${logger} | ${message} | ${exception:format=tostring} ${newline}"/>

  <!--[變數] 檔案位置 -->
  <variable name="LogTxtDir" value="${basedir}/App_Data/Logs/${shortdate}/"/>
  <variable name="LogTxtLocation" value="${LogTxtDir}/${logger}.log"/>
  <variable name="LogTxtLocationFatal" value="${LogTxtDir}/FatalFile.log"/>

  <!--[設定] 寫入目標-->
  <targets>
    <target name="File" xsi:type="File" fileName="${LogTxtLocation}" layout="${Layout}"
            encoding="utf-8" maxArchiveFiles="30" archiveNumbering="Sequence"
            archiveAboveSize="1048576" archiveFileName="${LogTxtDir}/${logger}.log{#######}" />
    <target name="FileFatal" xsi:type="File" fileName="${LogTxtLocationFatal}" layout="${LayoutFatal}"
            encoding="utf-8" maxArchiveFiles="30" archiveNumbering="Sequence"
            archiveAboveSize="1048576" archiveFileName="${LogTxtDir}/FatalFile.log{#######}" />
    <target name="EventLog" xsi:type="EventLog" source="NLogLogger" log="Application"
            layout="${date}| ${level} | ${message}"/>
  </targets>

  <!--[設定] 紀錄規則-->
  <rules>
    <logger name="*" levels="Trace,Debug,Info,Warn" writeTo="File" />
    <logger name="*" levels="Error,Fatal" writeTo="FileFatal" />
    <logger name="*" levels="Fatal" writeTo="EventLog" />
  </rules>

</nlog>

接下來,新增 MessageHandler/LogMessageHandler.cs 程式碼檔,這邊就直接看程式碼吧。

using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web;
using System.Collections.Generic;
using NLog;
using OLMapAPI.Infrastructure.auth;

namespace OLMapAPI.MessageHandler
{
    public class actionlog : ILog
    {
        private static Logger logger = NLog.LogManager.GetCurrentClassLogger();
        public void Save(string logContent)
        {
            // 原本 Nlog 的寫法
            logger.Info(logContent);
            //// 擷取自訂訊息
            //List<string> singleAttribute = logContent.Split(',').ToList<string>();
            //string name = singleAttribute[0].Trim(new Char[] { '{', '}' }).Split(new char[] { ':' }, 2)[1];
            //string ip = singleAttribute[1].Trim(new Char[] { '{', '}' }).Split(new char[] { ':' }, 2)[1];
            //// 紀錄自訂訊息
            //logger.LogExt(LogLevel.Info, logContent, name, "UserName"); //userid
        }
    }

    public class actionlogISerializer : ISerializer
    {
        public string Serialize<T>(RequestLogInfo info)
        {
            //return info.IpAddress + "|" + info.RequestTime + "|" + info.Signature + "|" + info.HttpMethod + "|" + info.UrlAccessed + "|" + info.BodyContent;
            return "{userid:" + info.Signature + "},{ip:" + info.IpAddress + "},{method:" + info.HttpMethod + "},{time:" + info.RequestTime.ToString("yyyy/MM/dd HH:mm:ss") + "},{request:" + info.UrlAccessed + "}";
        }
        public string Serialize<T>(ResponseLogInfo info)
        {
            return info.ReturnCode + "|" + info.ReturnMessage + "|" + info.ResponseTime + "|" + info.BodyContent;
        }
    }

    public interface ILog
    {
        void Save(string logContent);
    }

    public interface ISerializer
    {
        string Serialize<T>(RequestLogInfo info);
        string Serialize<T>(ResponseLogInfo info);
    }

    public class LogMessageHandler : MessageProcessingHandler
    {
        private ILog _log;
        private ISerializer _serializer;

        public LogMessageHandler(ILog log, ISerializer serializer)
        {
            this._log = log;
            this._serializer = serializer;
        }

        /// <summary>
        /// 將 request 相關訊息記錄 log
        /// </summary>
        /// <param name="request">The HTTP request message to process.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>
        /// Returns <see cref="T:System.Net.Http.HttpRequestMessage" />.The HTTP request message that was processed.
        /// </returns>
        private const string _header = "Authorization";
        protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            //由token換成userid

            string userid = "null";
            var token = Enumerable.Empty<string>();
            bool isHeaderExist = request.Headers.TryGetValues(_header, out token);
            if (isHeaderExist)
            {
                if (token.Count() > 0)
                {
                    authFunc auth = new authFunc();
                    userid = auth.validatesToken(token.First());
                }
            }

            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var info = new RequestLogInfo
            {
                HttpMethod = request.Method.Method,
                UrlAccessed = request.RequestUri.AbsoluteUri,
                IpAddress = HttpContext.Current != null ? HttpContext.Current.Request.UserHostAddress : "0.0.0.0",
                RequestTime = DateTime.Now,
                Token = this.GetToken(request),
                Signature = userid,
                Timestamp = this.GetTimestamp(request),
                BodyContent = request.Content == null ? string.Empty : request.Content.ReadAsStringAsync().Result
            };

            var logContent = this._serializer.Serialize<RequestLogInfo>(info);
            this._log.Save(logContent);

            return request;
        }

        /// <summary>
        /// 將 response 相關訊息記錄 log
        /// </summary>
        /// <param name="response">The HTTP response message to process.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>
        /// Returns <see cref="T:System.Net.Http.HttpResponseMessage" />.The HTTP response message that was processed.
        /// </returns>
        protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, System.Threading.CancellationToken cancellationToken)
        {
            if (response == null)
            {
                throw new ArgumentNullException("response");
            }
            var info = new ResponseLogInfo
            {
                StatusCode = ((int)response.StatusCode).ToString(),
                ResponseTime = DateTime.Now,
                ReturnCode = this.GetReturnCode(response),
                ReturnMessage = this.GetReturnMessage(response),
                Signature = this.GetSignature(response),
                BodyContent = response.Content == null ? string.Empty : response.Content.ReadAsStringAsync().Result
            };
            var logContent = this._serializer.Serialize<ResponseLogInfo>(info);
            this._log.Save(logContent);
            return response;
        }

        private string GetReturnCode(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-returnCode").Item2;
        }

        private string GetReturnMessage(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-returnMessage").Item2;
        }

        private string GetSignature(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-signature").Item2;
        }

        private string GetSignature(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-signature").Item2;
        }

        private string GetTimestamp(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-timestamp").Item2;
        }

        private string GetToken(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-token").Item2;
        }
    }

    public class RequestLogInfo
    {
        public string BodyContent { get; set; }
        public string HttpMethod { get; set; }
        public string IpAddress { get; set; }
        public DateTime RequestTime { get; set; }
        public string Signature { get; set; }
        public string Timestamp { get; set; }
        public string Token { get; set; }
        public string UrlAccessed { get; set; }
    }

    public class ResponseLogInfo
    {
        public string BodyContent { get; set; }
        public DateTime ResponseTime { get; set; }
        public string ReturnCode { get; set; }
        public string ReturnMessage { get; set; }
        public string Signature { get; set; }
        public string StatusCode { get; set; }
    }

    internal class HeaderHelper
    {
        internal static Tuple<bool, string> GetHeaderValue(HttpResponseHeaders httpResponseHeaders, string headerName)
        {
            var result = string.Empty;
            var specialHeaders = Enumerable.Empty<string>();
            var isExistHeader = httpResponseHeaders.TryGetValues(headerName, out specialHeaders);

            if (isExistHeader)
            {
                result = specialHeaders.LastOrDefault();
            }

            return Tuple.Create(isExistHeader, result);
        }
        
        internal static Tuple<bool, string> GetHeaderValue(HttpRequestHeaders httpRequestHeaders, string headerName)
        {
            var result = string.Empty;
            var specialHeaders = Enumerable.Empty<string>();
            var isExistHeader = httpRequestHeaders.TryGetValues(headerName, out specialHeaders);

            if (isExistHeader)
            {
                result = specialHeaders.LastOrDefault();
            }

            return Tuple.Create(isExistHeader, result);
        }
    }
}

假設今天是2020-10-01,就會在專案檔資料夾下 ~\OLMapAPI\App_Data\Logs\2020-10-01\OLMapAPI.MessageHandler.actionlog.log 看到以下Log資訊。
https://ithelp.ithome.com.tw/upload/images/20201001/20108631CQd5Q0lYNs.png

3. Message Handler:Request Header 資訊獲取與token驗證

Message Handler,可以讓在不改 Web API 的情況下,透過它來處理一些 WebAPI 共同要處理的項目,服務收到請求後可能會經過數個 Message Handler 逐步處理後再返回訊息至客戶端,見下圖。

https://ithelp.ithome.com.tw/upload/images/20200930/20108631En0AWvdS3k.png

來源:ASP.NET Web API 中的 HTTP 訊息處理常式

新增 MessageHandler/AuthMessageHandler.cs 的頁面,主要就是在執行API之前先執行這支MessageHandler,撈取header裡面的 Authorization 參數值。
(我這邊是設計token要帶在 Authorization 這個參數內,也可自行設計 ex.直接使用Token)

將token分離出來以後,執行 validatesToken() 回傳userid並 SetPrincipal()

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using OLMapAPI.Models;
using OLMapAPI.Infrastructure.auth;

namespace OLMapAPI.MessageHandler
{
    public class AuthMessageHandler : DelegatingHandler
    {
        /// <summary>
        /// Header名稱預設為「APIKey」,改為Authorization
        /// </summary>
        private const string _header = "Authorization";

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            //// 檢查request裡是否有「Authorization」這個Header
            var token = Enumerable.Empty<string>();
            bool isHeaderExist = request.Headers.TryGetValues(_header, out token);
            if (isHeaderExist)
            {
                if (token.Count() > 0)
                {
                    authFunc auth = new authFunc();
                    string userid = auth.validatesToken(token.First());
                    this.SetPrincipal(userid);
                }
            }
            return base.SendAsync(request, cancellationToken);
        }

        /// <summary>
        /// 設定IPrincipal
        /// </summary>
        private void SetPrincipal(string userid)
        {
            //// 設定使用者識別 => 就是使用者名稱啦
            //// GenericIdentity.IsAuthenticated 預設為true
            GenericIdentity identity = new GenericIdentity(userid);

            account acc = new account();
            String[] mMyStringArray = acc.ListUserRoles(userid).ToArray();//{ "admin" };
            //// 將使用者的識別與其所屬群組設定到GenericPrincipal類別上
            GenericPrincipal principal = new GenericPrincipal(identity, mMyStringArray);

            Thread.CurrentPrincipal = principal;

            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = principal;
            }
        }
    }
}

4. 新增filter並註冊

每次撈取API之前,先去檢查是否需要權限授權,與使用者名稱是否有授權,就像濾鏡一樣先篩掉不符合規定的

新增 filter/apiAuthorizationFilter.cs 程式碼檔。

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace OLMapAPI.filter
{
    public class apiAuthorizationFilter : AuthorizationFilterAttribute
    {
        /// <summary>
        /// The authorization service
        /// </summary>
        private IAuthorizationService authorizationService = new CustomAuthorizationService();

        /// <summary>
        /// 在處理序要求授權時呼叫。
        /// </summary>
        /// <param name="actionContext">動作內容,該內容封裝 <see cref="T:System.Web.Http.Filters.AuthorizationFilterAttribute" /> 的使用資訊。</param>
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            bool isAuthorizated = false;

            //// 從Thread取出IPrincipal
            IPrincipal principal = Thread.CurrentPrincipal;
            isAuthorizated = authorizationService.IsAuthorizated(principal, "");

            if (SkipAuthorization(actionContext))
            {
                return;
            }

            if (!isAuthorizated)
            {
                //// CreateResponse是System.Net.Http命名空間的擴充方法
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }

        private static bool SkipAuthorization(HttpActionContext actionContext)
        {
            if (!Enumerable.Any<AllowAnonymousAttribute>((IEnumerable<AllowAnonymousAttribute>)actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>()))
                return Enumerable.Any<AllowAnonymousAttribute>((IEnumerable<AllowAnonymousAttribute>)actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>());
            else
                return true;
        }
    }

    /// <summary>
    /// admin Filter
    /// </summary>
    public class apiAdminFilter : AuthorizationFilterAttribute
    {
        private IAuthorizationService authorizationService = new CustomAuthorizationService();
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            bool isAuthorizated = false;
            IPrincipal principal = Thread.CurrentPrincipal;
            isAuthorizated = authorizationService.IsAuthorizated(principal, "admin");
            if (SkipAuthorization(actionContext)) { return; }
            if (!isAuthorizated) { actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); }
        }

        private static bool SkipAuthorization(HttpActionContext actionContext)
        {
            if (!Enumerable.Any<AllowAnonymousAttribute>((IEnumerable<AllowAnonymousAttribute>)actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>()))
                return Enumerable.Any<AllowAnonymousAttribute>((IEnumerable<AllowAnonymousAttribute>)actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>());
            else
                return true;
        }
    }

    /// <summary>
    /// IAuthorizationService
    /// </summary>
    public interface IAuthorizationService
    {
        /// <summary>
        /// 檢查該使用者的名稱是否有權限
        /// </summary>
        bool IsAuthorizated(IPrincipal principal, string checkrole);
    }

    /// <summary>
    /// CustomAuthorizationService
    /// </summary>
    public class CustomAuthorizationService : IAuthorizationService
    {
        public bool IsAuthorizated(IPrincipal principal, string checkrole)
        {
            string userId = principal.Identity.Name;
            if (userId == "")
            {
                return false;
            }
            else if (userId != "" && checkrole == "")
            {
                return true;
            }
            else
            {
                return principal.IsInRole(checkrole);
            }
        }
    }

}

web.config 中新增 lockYN 參數,用來控制此API是否要開啟驗證機制;這邊當然預設要開啟 "Y"

<!-- 本專案API Filter -->
<add name="lockYN" connectionString="Y" />

WebApiconfig.cs中新增MessageHandlersFilter,並設定 CORS
CORS的部分可以直接鎖Domain,若只有要給自己的網頁使用,可以只鎖定在架設網站的那台AP上。

config.MessageHandlers.Add(new AuthMessageHandler());
config.MessageHandlers.Add(new LogMessageHandler(new actionlog(), new actionlogISerializer()));

/*允許所有的方法跨域*/
config.EnableCors(new EnableCorsAttribute("*", "*", "*"));

//註冊整個專案的Filter
string lockYN = ConfigurationManager.ConnectionStrings["lockYN"].ConnectionString;
if (lockYN == "Y")
{
    config.Filters.Add(new apiAuthorizationFilter());
}

小結

今天的長度好像是20天以來最長的,但主要篇幅都在程式碼上面。
至於這個系列的資安問題,我這邊比較少去驗證、阻擋和著墨,不過最低最低的限度就是不要造成 SQL Injection還有 明碼儲存和驗證 的部分。

今天已經大致完成了90%的驗證機制的建立,token設計的部份明天再行說明。
我想這一系列大家應該已經快看不下去了/images/emoticon/emoticon28.gif


上一篇
Day 20. API登入權限控管機制 #1:ASP.NET Identity
下一篇
Day 22. API登入權限控管機制 #3:Token編碼與加密設計
系列文
WebGIS入門學習 - 以Openlayers實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言