C# winform自托管WebApi及身份信息加密、Basic验证、Http Message Handler、跨域配置
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下载。