Nginx集群之基於Redis的WebApi身份驗證
目錄
1 大概思路... 1
2 Nginx集群之基於Redis的WebApi身份驗證... 1
3 Redis數據庫... 2
4 Visualbox虛擬機ubuntu下的redis部署... 3
5 編寫.NET WebApi的OnAuthorization身份驗證... 6
6 編寫.NET WebApi的ActionFilterAttribute令牌驗證... 8
7 編寫.NET WebApi的服務端... 10
8 編寫.NET WebApi的客戶端... 11
9 部署WebApi到本機... 17
10 Nginx集群配置搭建... 18
11 運行結果... 19
12 總結... 20
1 大概思路
l Nginx集群之基於Redis的WebApi身份驗證
l Redis數據庫
l Visualbox虛擬機ubuntu下的redis部署
l 編寫.NET WebApi的OnAuthorization身份驗證
l 編寫.NET WebApi的ActionFilterAttribute令牌驗證
l 編寫.NET WebApi的服務端
l 編寫.NET WebApi的客戶端
l 部署WebApi到本機
l Nginx集群配置搭建
l 運行結果
l 總結
2 Nginx集群之基於Redis的WebApi身份驗證
Nginx在集群上使用Redis數據庫進行身份驗證,達到了支持集群、分布式。在此基礎上能夠實現單點登錄、時效性的訪問,結合WebApi最大限度地發揮了後臺身份驗證的管理。
基於Redis的原因有幾個:第一點是Redis是基於內存的數據庫訪問起來比較快,能夠設置數據庫存儲的時效性;第二點,Redis數據庫的語法比較簡單、輕巧,在Linux、Windows服務器均可以一鍵安裝完成;第三點,Redis支持數據的備份,即master-slave模式的數據備份。
以下是本文講述的主要結構圖:
客戶端輸入用戶名密碼服務器,通過了用戶名密碼驗證,其中一臺WebApi服務器生成一個Token並返回https的響應。客戶端收到Token保存在本地,帶上token發出ajax請求、WebRequest請求,經過Action過濾器的檢驗,訪問Action並返回數據。
Token令牌身份驗證機制:
Nginx集群之基於Redis的WebApi身份驗證,如下圖所示:
3 Redis數據庫
Redis是一個開源的使用ANSI C語言編寫、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API。它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。
具體可以在以下網址進行學習:
http://www.runoob.com/redis/redis-tutorial.html
4 Visualbox虛擬機ubuntu下的redis部署
(已安裝Oracle VM VirtualBox、ubuntu-17.10-desktop-amd64.iso)
Redis安裝可參考:http://www.runoob.com/redis/redis-install.html
- 虛擬機
因網絡的限制,本文采取的是“網絡地址轉換(NAT)”方式,進行redis數據庫連接訪問。有條件的可以采用“橋接網上”的方式,便可以使多臺PC機在同一redis數據庫進行訪問,達到集群的效果。
Linux的ubuntu打開6379端口:
sudo iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
具體如下所示:
登錄Redis服務器,設置密碼:
redis-cli config set requirepass 123456
- 主機
Windows安裝Telnet客戶端,進行測試連接成功與否
運行CMD,輸入命令行如下:
Microsoft Windows [版本 6.1.7601] 版權所有 (c) 2009 Microsoft Corporation。保留所有權利。 C:\Users\zhyongfeng>telnet 10.93.85.66 6379
測試成功
5 編寫.NET WebApi的OnAuthorization身份驗證
CustomAuthorizeAttribute.cs
using System.Web.Http; using System.Web.Http.Controllers; namespace SSLWebApi.Controllers { public class CustomAuthorizeAttribute : AuthorizeAttribute { public override void OnAuthorization(HttpActionContext actionContext) { //判斷用戶是否登錄 if (actionContext.Request.Headers.Authorization != null) { string userInfo = System.Text.Encoding.Default.GetString(System.Convert.FromBase64String(actionContext.Request.Headers.Authorization.Parameter)); //用戶驗證邏輯 if (string.Equals(userInfo, string.Format("{0}:{1}", "zhyongfeng", "123456"))) { IsAuthorized(actionContext); } else { HandleUnauthorizedRequest(actionContext); } } else { HandleUnauthorizedRequest(actionContext); } } protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext) { var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); challengeMessage.Headers.Add("WWW-Authenticate", "Basic"); throw new System.Web.Http.HttpResponseException(challengeMessage); } } }
生成Token
BaseController.cs
using ServiceStack.Redis; using SSLWebApi.Models; using System; using System.Web; using System.Web.Http; namespace SSLWebApi.Controllers { /// <summary> /// BaseController繼承BaseController則需要身份驗證 /// </summary> [CustomAuthorize] [RoutePrefix("api/Base")] public class BaseController : ApiController { [HttpGet] [Route("Login")] public string Login(string userId) { if (HttpRuntime.Cache.Get(userId) == null) { return CreateToken(userId); } else { HttpRuntime.Cache.Remove(userId); return CreateToken(userId); } } /// <summary> /// 生成token /// </summary> /// <param name="userId"></param> /// <returns></returns> private string CreateToken(string userId) { Token token = new Token(); token.UserId = userId; token.SignToken = Guid.NewGuid(); token.Seconds = 12; token.ExpireTime = DateTime.Now.AddSeconds(token.Seconds); //redis使用sudo iptables -A INPUT -p tcp --dport 6379 -j ACCEPT打開6379的端口 //visualbox虛擬機使用“設置->網絡->高級->端口轉發”創建商品規則 //連接遠程visualbox虛擬機ubtuntu的redis,形成一個分布式的登錄驗證 string remoteIp = "10.93.85.66"; int remotePort = 6379; string remoteDbPassword = "123456"; //RedisClient(地址,端口,密碼,0) using (var client = new RedisClient(remoteIp, remotePort, remoteDbPassword, 0)) { string strToken = token.SignToken.ToString(); if (client.Exists(userId) > 0) client.Del(userId); if (client.Add(userId, strToken)) { //設置key的過期時間為12s client.Expire(userId, 12); return token.SignToken.ToString(); } else return string.Format("遠程虛擬機{0}的redis創建token失敗", remoteIp); } } } }
6 編寫.NET WebApi的ActionFilterAttribute令牌驗證
WebApiSecurityFilter.cs
using ServiceStack.Redis; using SSLWebApi.Models; using System; using System.Linq; using System.Net.Http; using System.Web; using System.Web.Http.Controllers; using System.Web.Http.Filters; namespace SSLWebApi.Filter { public class WebApiSecurityFilter : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (actionContext.ActionDescriptor.ActionName == "Login") { //登錄成功則生成token base.OnActionExecuting(actionContext); return; } else { //判斷token令牌 HttpRequestMessage request = actionContext.Request; string staffid = request.Headers.Contains("userid") ? HttpUtility.UrlDecode(request.Headers.GetValues("userid").FirstOrDefault()) : string.Empty; string timestamp = request.Headers.Contains("timestamp") ? HttpUtility.UrlDecode(request.Headers.GetValues("timestamp").FirstOrDefault()) : string.Empty; string nonce = request.Headers.Contains("nonce") ? HttpUtility.UrlDecode(request.Headers.GetValues("nonce").FirstOrDefault()) : string.Empty; string signature = request.Headers.Contains("signature") ? HttpUtility.UrlDecode(request.Headers.GetValues("signature").FirstOrDefault()) : string.Empty; if (String.IsNullOrEmpty(staffid) || String.IsNullOrEmpty(timestamp) || String.IsNullOrEmpty(nonce) || String.IsNullOrEmpty(signature)) { //令牌檢驗不通過 actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); return; } else { string remoteIp = "10.93.85.66"; int remotePort = 6379; string remoteDbPassword = "123456"; using (var client = new RedisClient(remoteIp, remotePort, remoteDbPassword, 0)) { //令牌檢驗是否存在這個用戶 if (client.Exists(staffid) > 0) { string tempStr = System.Text.Encoding.UTF8.GetString(client.Get(staffid)); if (tempStr.Trim(‘"‘).Equals(signature)) { //時間轉換成功、時間有效、token值相等 //令牌通過 base.OnActionExecuting(actionContext); return; } else { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); return; } } else { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden); return; } } } } } } }
7 編寫.NET WebApi的服務端
UserController.cs
using System; using System.Net; using System.Web.Http; namespace SSLWebApi.Controllers { [RoutePrefix("api/User")] public class UserController : ApiController { /// <summary> /// 獲取當前用戶信息 /// </summary> /// <param name="msg"></param> /// <returns></returns> [HttpPost] [Route("PostMessage")] public string PostMessage(dynamic obj) { return string.Format("當前輸入的消息是:{0}", Convert.ToString(obj.msg)); } [Route("GetMachine")] public string GetMachine() { string AddressIP = string.Empty; foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList) { if (_IPAddress.AddressFamily.ToString() == "InterNetwork") { AddressIP = _IPAddress.ToString(); } } return string.Format("當前WebApi部署的IP是:{0}", AddressIP); } } }
8 編寫.NET WebApi的客戶端
WebApiHelper.cs
using Newtonsoft.Json; using System; using System.IO; using System.Net; using System.Text; namespace SSLWebApiClient.Common { public class WebApiHelper { /// <summary> /// Post請求 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">url</param> /// <param name="data">數據</param> /// <param name="userid">帳戶</param> /// <param name="signature">數字簽名</param> /// <returns></returns> public static string Post(string url, string data, string userid, string signature) { return PostData(url, data, userid, signature); } /// <summary> /// Post請求 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="url">url</param> /// <param name="data">數據</param> /// <param name="userid">帳戶</param> /// <param name="signature">數字簽名</param> /// <returns></returns> public static T Post<T>(string url, string data, string userid, string signature) { return JsonConvert.DeserializeObject<T>(Post(url, data, userid, signature)); } /// <summary> /// /// </summary> /// <param name="webApi"></param> /// <param name="queryStr"></param> /// <param name="userid"></param> /// <param name="signature"></param> /// <returns></returns> public static string Get(string webApi, string queryStr, string userid, string signature) { return GetData(webApi, queryStr, userid, signature); } /// <summary> /// Get請求 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="webApi"></param> /// <param name="query"></param> /// <param name="queryStr"></param> /// <param name="userid"></param> /// <param name="signature"></param> /// <returns></returns> public static T Get<T>(string webApi, string queryStr, string userid, string signature) { return JsonConvert.DeserializeObject<T>(GetData(webApi, queryStr, userid, signature)); } /// <summary> /// 獲取時間戳 /// </summary> /// <returns></returns> private static string GetTimeStamp() { TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0); return ts.TotalSeconds.ToString(); } /// <summary> /// 獲取隨機數 /// </summary> /// <returns></returns> private static string GetRandom() { Random rd = new Random(DateTime.Now.Millisecond); int i = rd.Next(0, int.MaxValue); return i.ToString(); } /// <summary> /// Post請求 /// </summary> /// <param name="url"></param> /// <param name="data"></param> /// <param name="userid">用戶名稱</param> /// <param name="signature">數字簽名</param> /// <returns></returns> private static string PostData(string url, string data, string userid, string signature) { try { byte[] bytes = Encoding.UTF8.GetBytes(data); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); string timeStamp = GetTimeStamp(); string nonce = GetRandom(); //加入頭信息 //當前請求用戶 request.Headers.Add("userid", userid); //發起請求時的時間戳(單位:秒) request.Headers.Add("timestamp", timeStamp); //發起請求時的時間戳(單位:秒) request.Headers.Add("nonce", nonce); //當前請求內容的數字簽名 request.Headers.Add("signature", signature); //寫數據 request.Method = "POST"; request.ContentLength = bytes.Length; request.ContentType = "application/json"; request.GetRequestStream().Write(bytes, 0, bytes.Length); //讀數據 request.Timeout = 300000; request.Headers.Set("Pragma", "no-cache"); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream streamReceive = response.GetResponseStream(); StreamReader streamReader = new StreamReader(streamReceive, Encoding.UTF8); string strResult = streamReader.ReadToEnd(); //關閉流 //reqstream.Close(); streamReader.Close(); streamReceive.Close(); request.Abort(); response.Close(); return strResult; } catch (Exception ex) { return ex.Message; } } /// <summary> /// Get請求 /// </summary> /// <param name="webApi"></param> /// <param name="queryStr"></param> /// <param name="userid"></param> /// <param name="signature"></param> /// <returns></returns> private static string GetData(string webApi, string queryStr, string userid, string signature) { try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(webApi + "?" + queryStr); string timeStamp = GetTimeStamp(); string nonce = GetRandom(); //加入頭信息 //當前請求用戶 request.Headers.Add("userid", userid); //發起請求時的時間戳(單位:秒) request.Headers.Add("timestamp", timeStamp); //發起請求時的時間戳(單位:秒) request.Headers.Add("nonce", nonce); //當前請求內容的數字簽名 request.Headers.Add("signature", signature); request.Method = "GET"; request.ContentType = "application/json"; request.Timeout = 90000; request.Headers.Set("Pragma", "no-cache"); HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream streamReceive = response.GetResponseStream(); StreamReader streamReader = new StreamReader(streamReceive, Encoding.UTF8); string strResult = streamReader.ReadToEnd(); streamReader.Close(); streamReceive.Close(); request.Abort(); response.Close(); return strResult; } catch (Exception ex) { return ex.Message; } } } }
Program.cs
using System; using System.IO; using System.Net; using System.Text; using Newtonsoft.Json; namespace SSLWebApiClient { class Program { static void Main(string[] args) { string basicUrl = "http://zhyongfeng.com"; string html = string.Empty; for (int i = 0; i < 1; i++) { //https協議基本認證 Authorization string url = basicUrl + "/api/base/Login?userId=zhyongfeng"; ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); NetworkCredential credential = new NetworkCredential("zhyongfeng", "123456"); req.Credentials = credential; HttpWebResponse response = (HttpWebResponse)req.GetResponse(); Stream responseStream = response.GetResponseStream(); StreamReader streamReader = new StreamReader(responseStream, Encoding.UTF8); html = streamReader.ReadToEnd().Replace("\"", ""); Console.WriteLine("Redis Token服務器保存時間為12s"); Console.WriteLine(String.Format("Redis服務器返回的Token值為:{0}", html)); } //token設置了12s有效期 for (int j = 0; j < 5; j++) { System.Threading.Thread.Sleep(1000); string url = basicUrl + "/api/user/PostMessage"; Console.WriteLine(Common.WebApiHelper.Post(url, JsonConvert.SerializeObject(new { msg = "hello" }), "zhyongfeng", html)); } for (int j = 0; j < 10; j++) { System.Threading.Thread.Sleep(1000); string url = basicUrl + "/api/user/GetMachine"; Console.WriteLine(Common.WebApiHelper.Get(url, null, "zhyongfeng", html)); } Console.Read(); } } }
9 部署WebApi到本機
將WebApi部署到以下10.93.85.66(因網絡限制,所以這裏只做單個集群,虛擬機ubuntu中的數據redis主要是NAT網絡連接方式,使用端口轉發進行訪問)
10 Nginx集群配置搭建
通過自主義域名zhyongfeng.com:80端口進行負載均衡集群訪問,則訪問C:\Windows\System32\drivers\etc\hosts,添加下列“本機IP 自定義的域名”:
10.93.85.66 zhyongfeng.com
Nginx的集群配置:
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65;
upstream zhyongfeng.com { server 10.93.85.66:20107; } server { listen 80; server_name localhost; location / { proxy_pass http://zhyongfeng.com; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
運行CMD:
D:\DTLDownLoads\nginx-1.10.2>start nginx D:\DTLDownLoads\nginx-1.10.2>nginx -s reload
11 運行結果
訪問集群:http://zhyongfeng.com
WebApi經過Action過濾器,生成了Token值,並存儲到Redis,運行結果如下:
12 總結
Nginx集群使用Redis數據庫,客戶端利用 http basic身份驗證,訪問WebApi獲得Token並將Token存儲到Redis內在數據庫,通過Token值獲取相應的權限數據,這樣子可以做到單點登錄,集群分布式的身份驗證效果。既方便了用戶在整個業務領域的系統操作,同時可以為整個公司、集團等各個區域的系統進行統一有效的身份驗證管理。
Nginx集群之基於Redis的WebApi身份驗證