1.介紹
1.1功能需求及介紹
由於具體情況需要,WebAPI不托管在IIS上,而是由Winform托管,且客戶端訪問服務端需要進行身份驗證,身份驗證的信息進行加密,另外需要允許由HTML頁面發出的跨域請求。
1.2內容分布說明
C# JS DES加密
首先研究數據加密算法,DES算法是最常用的對稱加密算法之一,雙方通過相同的秘鑰加解密。
C#提供System.Security.Cryptography
進行加密,而JS有知名的crypto-js
加密庫,提供有DES加密相關。
重點在於要讓兩種編程語言的加解密結果一致。
自托管WebAPI
在exe程序中運行WebAPI
Basic身份驗證
HTTP的基礎身份驗證方式
HTTPMessageHandler
防止API被惡意訪問,對無法驗證身份的訪問進行過濾
Web端跨域配置
訪問方式及配置
2.C# JS DES加密
2.1C# DES加密
首先添加相關引用using System.Security.Cryptography;
然后設置秘鑰private const string pKey = "12345678";
,注意秘鑰需要為8位,不可少,若多了需要看算法是否有截斷處理
2.1.1加密
/// <summary>
/// 加密
/// </summary>
/// <param name="StrOrign">待加密字符串,字符、數字、中文等</param>
/// <returns>返回base64字符串</returns>
public static string Encrypt(string StrOrign)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] inputByteArray;
inputByteArray = System.Text.Encoding.UTF8.GetBytes(StrOrign);
// @#建立加密對象的密鑰和偏移量
// @#原文使用ASCIIEncoding.ASCII方法的GetBytes方法
// @#使得輸入密碼必須輸入英文文本
des.Key = System.Text.Encoding.UTF8.GetBytes(pKey);
des.IV = System.Text.Encoding.UTF8.GetBytes(pKey);
des.Mode = CipherMode.ECB;
des.Padding = PaddingMode.PKCS7;
// @#寫二進制數組到加密流
// @#(把內存流中的內容全部寫入)
System.IO.MemoryStream ms = new System.IO.MemoryStream();
CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(des.Key, des.IV), CryptoStreamMode.Write);
// @#寫二進制數組到加密流
// @#(把內存流中的內容全部寫入)
cs.Write(inputByteArray, 0, inputByteArray.Length);
cs.FlushFinalBlock();
// '將8位無符號整數數組的值轉換成用Base64數字編碼的等效字符串表現形式,字符包含:A-Z a-z 0-9 + / 無值字符"="用於尾部空白
byte[] bytes = ms.ToArray();
string ret = Convert.ToBase64String(bytes);
des.Dispose();
cs.Dispose();
ms.Dispose();
return ret;
}
2.1.2解密
/// <summary>
/// 解密。注意判斷傳入參數的有效性,非base64字符串會報錯,建議try-catch捕捉異常
/// </summary>
/// <param name="StrOrigin">待解密字符串,base64字符串</param>
/// <returns>返回原始字符串</returns>
public static string Decrypt(string StrOrigin)
{
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
byte[] inputByteArray = Convert.FromBase64String(StrOrigin); // 將base64字符串轉換為16進制字節數組
// @#建立加密對象的密鑰和偏移量,此值重要,不能修改
des.Key = System.Text.Encoding.UTF8.GetBytes(pKey);
des.IV = System.Text.Encoding.UTF8.GetBytes(pKey);
des.Mode = CipherMode.ECB;
des.Padding = PaddingMode.PKCS7;
System.IO.MemoryStream ms = new System.IO.MemoryStream();
CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(des.Key,des.IV), CryptoStreamMode.Write);
cs.Write(inputByteArray, 0, inputByteArray.Length);
cs.FlushFinalBlock();
des.Dispose();
cs.Dispose();
ms.Dispose();
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
}
2.1.3重要參數
算法中有幾個重要參數如des.Key
、des.IV
、des.Mode
、des.Padding
需要將其顯式設置,否則會使用默認值而不一定與Js參數配置相同。
2.1.4格式編碼統一
需要注意的是,在加解密時,需要將原始字符串、秘鑰、偏移向量等參數都以相同方式進行編碼與解碼,另外加密完成后將密文轉換為base64類型。
解密時,由於接受base64類型密文,在處理前需要特別驗證是否為base64類型,否則算法崩潰。
加解密完成后需要將流關閉。
2.2JS DES加密
2.2.1crypto-js
加密庫
DES加密需要添加這個庫里的幾個文件:tripledes.js
、mode-ecb.js
,另外為了方便操作,我使用了jQuery。
以下為相關代碼,完整源代碼在文末下載。
2.2.2加密
function encryptByDES(message, key) {
var keyHex = CryptoJS.enc.Utf8.parse(key);
var encrypted = CryptoJS.DES.encrypt(message, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
// return base64toHEX(encrypted.toString());//base64轉16進制字符串
return encrypted.toString();
}
2.2.3解密
function decryptByDES(ciphertext, key) {
// ciphertext=HexToBase64(ciphertext);//16進制轉base64
var keyHex = CryptoJS.enc.Utf8.parse(key);
// direct decrypt ciphertext
var decrypted = CryptoJS.DES.decrypt({
ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
}, keyHex, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
2.2.4 base64轉16進制
此代碼為網上資料
function base64toHEX(base64) {
var raw = atob(base64);
var HEX = '';
for (i = 0; i < raw.length; i++) {
var _hex = raw.charCodeAt(i).toString(16)
HEX += (_hex.length == 2 ? _hex : '0' + _hex);
}
return HEX.toUpperCase();
}
2.2.5 16進制轉base64
此代碼為網上資料
function HexToBase64(sha1) {
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var base64_rep = "";
var cnt = 0;
var bit_arr = 0;
var bit_num = 0;
for (var n = 0; n < sha1.length; ++n) {
if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
ascv = sha1.charCodeAt(n) - 55;
}
else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
ascv = sha1.charCodeAt(n) - 87;
}
else {
ascv = sha1.charCodeAt(n) - 48;
}
bit_arr = (bit_arr << 4) | ascv;
bit_num += 4;
if (bit_num >= 6) {
bit_num -= 6;
base64_rep += digits[bit_arr >>> bit_num];
bit_arr &= ~(-1 << bit_num);
}
}
if (bit_num > 0) {
bit_arr <<= 6 - bit_num;
base64_rep += digits[bit_arr];
}
var padding = base64_rep.length % 4;
if (padding > 0) {
for (var n = 0; n < 4 - padding; ++n) {
base64_rep += "=";
}
}
return base64_rep;
3.自托管WebAPI及Basic驗證、HTTP Message Handler
WebAPI托管運行在winform程序中,HTTP請求中需要附帶身份驗證header,通過消息處理程序提前批量過濾未驗證請求,而無需在每個API接口處驗證。
3.1自托管WebAPI
3.1.1引用
//引用自托管包
//Install-Package Microsoft.AspNet.WebApi.SelfHost -Version 5.2.7
3.3.2建立服務
private HttpSelfHostServer server;//服務器對象
try
{
HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(sUrl);
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
lblState.Text = "web服務已經啟動並運行中...";
btnStop.Enabled = true;
btnStart.Enabled = false;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
3.3.3API控制器
建立一個AddController.cs
的文件,並繼承ApiController。注意文件名一定要以Controller為后半部分,否則無法識別,不會生效。
一個簡單的接口,發送get請求,帶數字參數,服務端會將其乘10后返回。
public class AddController : ApiController
{
[HttpGet]
public string Add(int id)
{
return (id * 10).ToString();
}
[HttpGet]
public string Add()
{
return "hello";
}
}
3.3.4注意
webapi服務端需要監聽端口,直接運行程序會報錯,需要將vs先以管理員權限運行再打開項目,如果只要運行程序,也可以管理員身份打開exe程序。
3.2Basic驗證
3.2.1Basic驗證方式
HTTP請求中有身份驗證頭Authorization
,格式為:Authorization:
basic驗證只是提供一種身份驗證規范,具體的驗證方式需要自己構造。
3.2.2客戶端構造
客戶端構造basic驗證只需要為Authorization標頭設置scheme和parameter兩個參數,scheme是類型,parameter是身份信息。
reqMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, parameter);
客戶端發送GET請求完整代碼為:
public static string HttpWithAuthorize(string url, string scheme,string parameter)
{
HttpClient client = new HttpClient();
HttpRequestMessage reqMessage = new HttpRequestMessage();
Uri thisUri = new Uri(url);
reqMessage.Method = HttpMethod.Get;
reqMessage.RequestUri = thisUri;
reqMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(scheme, parameter);
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
HttpResponseMessage resp = client.SendAsync(reqMessage).Result;
StringBuilder sb = new StringBuilder();
sb.AppendLine("響應碼:" + resp.StatusCode);
sb.AppendLine("返回值:" + resp.Content.ReadAsStringAsync().Result);
return sb.ToString();
}
3.2.3服務端解析
檢查HTTP請求的header,讀取數據並解密,與服務端保存的身份信息對比,判斷是否通過。
private bool ValidateRequest(HttpRequestMessage message)//驗證信息解密並對比
{
var authorization = message.Headers.Authorization;
//如果此header為空或不是basic方式則返回未授權
if (authorization != null && authorization.Scheme == "Basic" && authorization.Parameter != null)
{
string Parameter = DES1.Decrypt(authorization.Parameter);
System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authorization.Parameter));//將base64轉為字符串形式
return (Parameter == "lbh:123456");
}
else
{
return false;
}
}
3.3HTTP Message Handler
3.3.1建立驗證文件
新建BasicAuthorizationHandler.cs
文件,繼承DelegatingHandler
public class BasicAuthorizationHandler:DelegatingHandler
對傳入服務端的請求進行處理:
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//包含自定義header字段的跨域請求,瀏覽器會先向服務器發送OPTIONS請求,探測該服務器是否允許自定義的跨域字段。如果允許,則繼續實際的POST/GET正常請求,否則,返回錯誤。https://blog.csdn.net/xuedapeng/article/details/79076704
//必須理解在正常請求前會收到option請求,如果不讓其通過message handler,則既不能通過身份驗證(沒有authorization頭),也不能通過跨域訪問控制(在本類中即被返回response,根本到達不了路由配置處),因此在前端頁面會出現兩種錯誤讓人無法理解
if (request.Method == HttpMethod.Options)
{
//如果是前端用於驗證跨域允許的option請求,則將其轉到路由配置處處理,路由跨域配置處理后前端會發送真正的請求,由下面的身份驗證代碼處理
var optRes = await base.SendAsync(request, cancellationToken);
return optRes;
}
if (!ValidateRequest(request))
{
//注意:和上面同樣道理,假如身份驗證失敗,該請求沒有進入路由跨域配置(真正的請求而非option),直接返回后前端會出現跨域錯誤。不過問題不大,此時是有403和cors兩種錯誤,前端能據此定位錯誤。
//沒有通過則創建response,code為401
var response = new HttpResponseMessage(HttpStatusCode.Forbidden);//用403拒絕訪問。如果用401未授權且有頁面的話會被重定向到登錄頁
var content=new Result{
success=false,
errs=new []{"服務端拒絕訪問:你沒有權限"}
};
//添加響應頭返回給跨域請求
//response.Headers.Add("Access-Control-Allow-Origin", "*");
//response.Headers.Add("Cache-Control", "no-cache");
//response.Headers.Add("Access-Control-Allow-Headers", "Content-Type,Authorization,token");//允許自定義頭
//response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");//content添加錯誤信息
return response;//返回響應
}
var res = await base.SendAsync(request, cancellationToken);
return res;
//以下為官方文檔用法,這種方法會使被拒絕方沒有任何提示信息
//var tsc = new TaskCompletionSource<HttpResponseMessage>();
//tsc.SetResult(response); // Also sets the task state to "RanToCompletion"
//return tsc.Task;
}
public class Result//構建用於返回錯誤信息的對象
{
public bool success { get; set; }
public string[] errs { get; set; }
}
3.3.2服務器配置
可以為指定路由配置,也可以全局配置
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
//constraints:null,
//handler:new BasicAuthorizationHandler());//可以為指定路由添加message handler。注意重載參數順序
config.MessageHandlers.Add(new BasicAuthorizationHandler());//全局HTTP Message Handler
4.Web端跨域訪問配置
HTML頁面訪問webapi會受到跨域訪問策略的阻止,因此需要進行配置
4.1服務端配置
4.1.1引用
//引入跨域包
//Install-Package Microsoft.AspNet.WebApi.Cors -Version 5.2.7
4.1.2config配置
//配置跨域訪問。一定要添加這個,才能使用EnableCorsAttribute,否則,在Contoler或者Action上面添加這個特性無效
config.EnableCors(new System.Web.Http.Cors.EnableCorsAttribute("*", "*", "*"));//配置全局跨域
//config.EnableCors();//為特定控制器或方法添加跨域特性
或者在控制器處添加:
//[EnableCors(origins: "*", headers: "*", methods: "*")]
public class AddController : ApiController
{
[HttpGet]
public string Add(int id)
{
return (id * 10).ToString();
}
}
4.2客戶端訪問
function WebApi() {
var ServerUrl = $.trim($('#ServerBase').val() + $('#ServerRelative').val());//webapi接口地址
var strKey = $.trim($('#key').val());//key
var strMsg = $.trim($('#Parameter').val());//需要加密的身份信息
var ciphertext = encryptByDES(strMsg, strKey);//加密后結果
console.log('Basic ' + ciphertext);
$.ajax({
url: ServerUrl,
//crossDomain:true,
type: "GET",
headers: {
'Accept': "text/html, application/xhtml+xml, */*",
'Content-Type': "application/json",
'Authorization': 'Basic ' + ciphertext,
'token':"hello"
},
success: function (data,textStatus) {
console.log(textStatus + data);
document.getElementById('txtInfo').innerHTML += data + '<br/>';
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log(textStatus + errorThrown);
var errdata = textStatus + errorThrown
document.getElementById('txtInfo').innerHTML += errdata + '<br/>';
}
})
}
5.完整代碼
https://files.cnblogs.com/files/ygxddxc/WebApi身份驗證.zip
為了減小體積,文件里的自托管、跨域相關庫已經刪除,可以自行通過nuget下載。