小谈JWT安全策略(一)
作者:互联网
春节假期总是那么短暂,回到岗位继续撸码。JWT对于大多数开发者来说都不会陌生,多用于分布式架构方面的认证,由于其不可篡改特性(非对称加密手段),使其无法被伪造;
下面描述一下JWT产生及应用的简单场景:有下面几个中心服务器:授权中心、WebAPI1、WebAPI2、WebApi3等、PC客户端、移动客户端、其他客户端,这些客户端想要访问WebApi就需要授权认证才可以访问,如当移动了客户端首次访问webApi1时由于没有登录,跳转到授权中心登录认证,输入用户名、密码后返回token给客户端,下次客户端带着这个token便可以随意穿梭于各个API了,大致就是这意思,但是里面有很多安全问题值得思考;
问题一:提交用户名、密码到授权中心时如何保证密码不被抓取泄漏,以及CSRF攻击呢?
客户端到服务器(授权中心)间会经过很多个网络节点,期间抓包一定会抓到提交的用户名、密码,所以密码必须加密,用户名无所谓加密否,很多大厂如百度、新浪都没加密用户名,那么如何加密呢?JS前端+非对称加密(RSA),JS用公钥加密密码后通过ajax提交服务器,服务器再用私钥解密密码进行验证,RSA有两种格式PEM、XML,C#里面支持XML,JSEncrypt支持PEM,所以提前准备好的公钥、私钥需要转换对应的格式,比如
XML公钥:
"pubKey": "<RSAKeyValue><Modulus>+X4odHfmozvdfqRhkX9/w11IoUZ0fPwvIHT9H6YvTk0Jbi7pRzqZ2ZUGfHtm0aeC4L+tEAON86zkNL6W84KJ0fLSv1wEzguRVTc3omM7cT2vdMcVW0nkO91XEuxYn4tJJZ8sSZfPnyozemCqdiRc+TMY4iUd+8KfnSSnnbygOiU=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"
XML的私钥:
"pfxKey": "<RSAKeyValue><Modulus>+X4odHfmozvdfqRhkX9/w11IoUZ0fPwvIHT9H6YvTk0Jbi7pRzqZ2ZUGfHtm0aeC4L+tEAON86zkNL6W84KJ0fLSv1wEzguRVTc3omM7cT2vdMcVW0nkO91XEuxYn4tJJZ8sSZfPnyozemCqdiRc+TMY4iUd+8KfnSSnnbygOiU=</Modulus><Exponent>AQAB</Exponent><P>+d9iMb7UUj4hgE8VRfdyysrudBYlS8324dXatKvqT3lMQYnCffVAuO6fnh64avowb35yhQ7bL0zRbqcIUTUYkw==</P><Q>/5xj6Ubpw14xHMgeYaN3AOSeUYS7GnxBAS0EYH//VmtMHBS/I7/ydCitY4avMDiigsHGMdo+p3hUjLTHh7itZw==</Q><DP>MyuQ41kOCA2bNLh9RcfH5UmrjXBfkOeL8HGoCSnPoRjZCe7ZOow4BecwHuRxpc64joyZjTAeqNAVb5CbPm4eKw==</DP><DQ>WAdZ+TqswtWrgEgC/ok3MJ2LUHOvpB1qnacNPNaX7wEgwwL6GKvAKfTgUchOiQLYJMPrJ7wLm807AQI5PSqCvQ==</DQ><InverseQ>swsRS+qbC8M32mC0P/sV2n1aFLvuYiO4wxT2JDGOTEUTysMNTz1okpllaKBMwDHnSOtbPaQu4wh6BP/5QgIXNw==</InverseQ><D>ACrhhBKAiSOFJoydtCdNwqfRYKpXca4Jqg/qC5dk+Lx8gSJu7alQaSD8fCDpFML0B4qZZyVZdwxqqs5l84vm48jbEhfFFBGqNHI+C1T86ppc/Z7ioip88/u7Cr3ePdEFnEKrAbNcmS32UuF5c2UMLjJk8tgsGOq7ntDcSCsa95E=</D></RSAKeyValue>"
JS需要使用公钥对应密码进行加密,所以需要把公钥转换成PEM格式,提供个转换在线地址:https://the-x.cn/certificate/XmlToPem.aspx
PEM公钥
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD5fih0d+ajO91+pGGRf3/DXUih RnR8/C8gdP0fpi9OTQluLulHOpnZlQZ8e2bRp4Lgv60QA43zrOQ0vpbzgonR8tK/ XATOC5FVNzeiYztxPa90xxVbSeQ73VcS7Fifi0klnyxJl8+fKjN6YKp2JFz5Mxji JR37wp+dJKedvKA6JQIDAQAB -----END PUBLIC KEY-----
大致代码如下:
前端login页面
@model MicroserviceArchitecture.WebApplication.Models.LoginViewModel @{ ViewBag.Title = "Log in"; Layout = null; } <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/js/JSEncrypt.js"></script> <script type="text/javascript"> var login = ( function ($) { $(document).ready( function () { $('#LoginButton').click(function () { var publicKey = $('#PublicKey').data("val"); var plainpassword = $('#passwordTextBox').val(); var AccountLoginURL = $('#AccountLoginURL').data("url"); var encryptedPassword; //alert(encryptedPassword); var formSelector = "#loginForm"; //var form = $(formSelector); //form.validate(); //var isFormValid = form.valid(); //encrypt password if (plainpassword !== null && plainpassword !== "") { var crypt = new JSEncrypt(); crypt.setPublicKey(publicKey); encryptedPassword = crypt.encrypt(plainpassword); console.log(encryptedPassword); $('#passwordTextBox').val(encryptedPassword); } alert(encryptedPassword); if (true) { //blockUI //showSpinner(); var formData = $("#loginForm").serializeArray(); $.ajax({ type: "POST", url: AccountLoginURL, data: formData, success: function (data, textStatus, jqXHR) { if (data.RedirectUrl !== null) { window.location.href = data.RedirectUrl; } else { $('#errorMsg').text(data.ErrorMessage); } }, error: function (jqXhr, textStatus, errorThrown) { console.log('error: ' + jqXhr.responseText); }, complete: function (jqXHR, textStatus) { //hideSpinner(); } }); } }); }); }(jQuery)); </script> <div class="wrapper--login"> <div class="wrapper--login__body"> <h1>Login</h1> @using (@Html.BeginForm("Login", "Login", FormMethod.Post, new { id = "loginForm" })) { <div id="AccountLoginURL" class="hidden" data-url="@Url.Action("Login", "Login")"></div> @Html.AntiForgeryToken() <div class="val"> @Html.LabelFor(m => m.UserName) <div class="val__field"> @Html.TextBoxFor(model => model.UserName, new { @class = "form-control", id = "userNameTextBox" }) @Html.ValidationMessageFor(model => model.UserName) </div> </div> <div class="val"> @Html.LabelFor(m => Model.EncryptedPassword) <div class="val__field"> @Html.PasswordFor(model => model.EncryptedPassword, new { @class = "form-control", id = "passwordTextBox" }) @Html.ValidationMessageFor(model => model.EncryptedPassword) </div> </div> @Html.HiddenFor(model => model.ReturnUrl) @Html.HiddenFor(model => model.RedirectDomain) <div class="val__message" id="errorMsg"></div> <div class="row"> <input type="button" id="LoginButton" value="Login" class="button--primary"> </div> } <div id="PublicKey" class="hidden" data-val="@Model.PublicKey"></div> <div class="row line"> <span class="line__1"></span>or<span class="line__2"></span> </div> <div class="row"> <a href="@Url.Action("forgotpassword", "Account")" class="button--secondary">Forgot Password ?</a> </div> </div> </div>
<script src="~/js/JSEncrypt.js"></script>自己到网上找,成熟的JS库,好用稳定;
MicroserviceArchitecture.WebApplication.Models.LoginViewModel这个vm主要就是把公钥从后台带过来,一些配置的东西
@Html.AntiForgeryToken()防止CSRF攻击产生的token后端会验证这个加密的token
public class LoginViewModel { public string UserName { get; set; } public string EncryptedPassword { get; set; } public string ReturnUrl { get; set; } public string RedirectDomain { get; set; } public string PublicKey = @"-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD5fih0d+ajO91+pGGRf3/DXUih RnR8/C8gdP0fpi9OTQluLulHOpnZlQZ8e2bRp4Lgv60QA43zrOQ0vpbzgonR8tK/ XATOC5FVNzeiYztxPa90xxVbSeQ73VcS7Fifi0klnyxJl8+fKjN6YKp2JFz5Mxji JR37wp+dJKedvKA6JQIDAQAB -----END PUBLIC KEY----- "; }
下面后端处理:
RSAProcessor帮助类
namespace MicroserviceArchitecture.Common.Security { public class RSAProcessor { public static void CreateRSAKey(ref string publicKey, ref string pfxKey) { RSACryptoServiceProvider provider = new RSACryptoServiceProvider(); pfxKey = provider.ToXmlString(true); publicKey = provider.ToXmlString(false); } public static string RSAEncrypt(string xmlPublicKey, string m_strEncryptString) { byte[] btEncryptedSecret = Encoding.UTF8.GetBytes(m_strEncryptString); btEncryptedSecret = CRSAWrap.EncryptBuffer(xmlPublicKey, btEncryptedSecret); return Convert.ToBase64String(btEncryptedSecret); } public static string RSADecrypt(string xmlPrivateKey, string m_strDecryptString) { byte[] btDecryptedSecred = Convert.FromBase64String(m_strDecryptString); btDecryptedSecred = CRSAWrap.DecryptBuffer(xmlPrivateKey, btDecryptedSecred); return Encoding.UTF8.GetString(btDecryptedSecred); } internal class CRSAWrap { internal static byte[] EncryptBuffer(string rsaKeyString, byte[] btSecret) { int keySize = 0; int blockSize = 0; int lastblockSize = 0; int counter = 0; int iterations = 0; int index = 0; byte[] btPlaintextToken; byte[] btEncryptedToken; byte[] btEncryptedSecret; RSACryptoServiceProvider rsaSender = new RSACryptoServiceProvider(); rsaSender.FromXmlString(rsaKeyString); keySize = rsaSender.KeySize / 8; blockSize = keySize - 11; if ((btSecret.Length % blockSize) != 0) { iterations = btSecret.Length / blockSize + 1; } else { iterations = btSecret.Length / blockSize; } btPlaintextToken = new byte[blockSize]; btEncryptedSecret = new byte[iterations * keySize]; for (index = 0, counter = 0; counter < iterations; counter++, index += blockSize) { if (counter == (iterations - 1)) { lastblockSize = btSecret.Length % blockSize; btPlaintextToken = new byte[lastblockSize]; Array.Copy(btSecret, index, btPlaintextToken, 0, lastblockSize); } else { Array.Copy(btSecret, index, btPlaintextToken, 0, blockSize); } btEncryptedToken = rsaSender.Encrypt(btPlaintextToken, false); Array.Copy(btEncryptedToken, 0, btEncryptedSecret, counter * keySize, keySize); } return btEncryptedSecret; } internal static byte[] DecryptBuffer(string rsaKeyString, byte[] btEncryptedSecret) { int keySize = 0; int blockSize = 0; int counter = 0; int iterations = 0; int index = 0; int byteCount = 0; byte[] btPlaintextToken; byte[] btEncryptedToken; byte[] btDecryptedSecret; RSACryptoServiceProvider rsaReceiver = new RSACryptoServiceProvider(); rsaReceiver.FromXmlString(rsaKeyString); keySize = rsaReceiver.KeySize / 8; blockSize = keySize - 11; if ((btEncryptedSecret.Length % keySize) != 0) { return null; } iterations = btEncryptedSecret.Length / keySize; btEncryptedToken = new byte[keySize]; Queue<byte[]> tokenQueue = new Queue<byte[]>(); for (index = 0, counter = 0; counter < iterations; index += blockSize, counter++) { Array.Copy(btEncryptedSecret, counter * keySize, btEncryptedToken, 0, keySize); btPlaintextToken = rsaReceiver.Decrypt(btEncryptedToken, false); tokenQueue.Enqueue(btPlaintextToken); } byteCount = 0; foreach (var PlaintextToken in tokenQueue) { byteCount += PlaintextToken.Length; } counter = 0; btDecryptedSecret = new byte[byteCount]; foreach (var PlaintextToken in tokenQueue) { if (counter == (iterations - 1)) { Array.Copy(PlaintextToken, 0, btDecryptedSecret, btDecryptedSecret.Length - PlaintextToken.Length, PlaintextToken.Length); } else { Array.Copy(PlaintextToken, 0, btDecryptedSecret, counter * blockSize, blockSize); } counter++; } return btDecryptedSecret; } } } }
LoginController.cs
public ActionResult Login() { return View(new LoginViewModel()); } [AllowAnonymous] [HttpPost] [ValidateAntiForgeryToken] public ActionResult Login(string username, string EncryptedPassword) { string PfxKey = "<RSAKeyValue><Modulus>+X4odHfmozvdfqRhkX9/w11IoUZ0fPwvIHT9H6YvTk0Jbi7pRzqZ2ZUGfHtm0aeC4L+tEAON86zkNL6W84KJ0fLSv1wEzguRVTc3omM7cT2vdMcVW0nkO91XEuxYn4tJJZ8sSZfPnyozemCqdiRc+TMY4iUd+8KfnSSnnbygOiU=</Modulus><Exponent>AQAB</Exponent><P>+d9iMb7UUj4hgE8VRfdyysrudBYlS8324dXatKvqT3lMQYnCffVAuO6fnh64avowb35yhQ7bL0zRbqcIUTUYkw==</P><Q>/5xj6Ubpw14xHMgeYaN3AOSeUYS7GnxBAS0EYH//VmtMHBS/I7/ydCitY4avMDiigsHGMdo+p3hUjLTHh7itZw==</Q><DP>MyuQ41kOCA2bNLh9RcfH5UmrjXBfkOeL8HGoCSnPoRjZCe7ZOow4BecwHuRxpc64joyZjTAeqNAVb5CbPm4eKw==</DP><DQ>WAdZ+TqswtWrgEgC/ok3MJ2LUHOvpB1qnacNPNaX7wEgwwL6GKvAKfTgUchOiQLYJMPrJ7wLm807AQI5PSqCvQ==</DQ><InverseQ>swsRS+qbC8M32mC0P/sV2n1aFLvuYiO4wxT2JDGOTEUTysMNTz1okpllaKBMwDHnSOtbPaQu4wh6BP/5QgIXNw==</InverseQ><D>ACrhhBKAiSOFJoydtCdNwqfRYKpXca4Jqg/qC5dk+Lx8gSJu7alQaSD8fCDpFML0B4qZZyVZdwxqqs5l84vm48jbEhfFFBGqNHI+C1T86ppc/Z7ioip88/u7Cr3ePdEFnEKrAbNcmS32UuF5c2UMLjJk8tgsGOq7ntDcSCsa95E=</D></RSAKeyValue>"; string decryPassword= Common.Security.RSAProcessor.RSADecrypt(PfxKey, EncryptedPassword); if(decryPassword=="123456") { ///验证通过 } else { ///失败 } return View(); }
问题二 如何防止黑客拿到授权后的token重放攻击呢?
一般措施都是token内加入时间戳,从请求发起到服务器处理不会超过60秒,验证时间戳在60秒以内为正常请求,超过60秒为重放攻击拒绝请求,黑客从抓包到请求攻击时间多数超过60秒,但有漏洞,如果60秒内重放攻击无法辨别,所以需要加入once字段判断,也就是每次60秒内只能请求一次,下次的必为攻击,客户端构造token时加入60秒内有效的时间戳和随机字符串once进行非对称加密发送给服务器,服务器收到后先解密,验证时间戳,以及到服务器redis列表中查询once,如果redis中没有查询到对应once数据,且时间戳在60内,请求有效,此时记录once随机字符串到redis中且设置过期时间为60秒;
其实上面场景都是有持有私钥能力的客户端,也就是客户端有自己生成token能力,那么对应于没有持有私钥能力的客户端如何处理token的重放攻击呢?如客户端都从授权中心获取oken,再带着token访问API服务器,其实这就需要微服务架构的使用场景了,把所有的webApi服务都配置在内网中,用Ocelot搭建网关暴露在外网,用Consual服务注册发现给网关(也就是网关能通过配置找到对应的wepAPI地址,并且支持负载均衡策略),此时暴露在外网的只有网关,所有都客户端请求都发给网关,由网关来转发请求返回信息,同时网关配合+polly可完成“限流、熔断、雪崩”等策略控制访问入口,如下面这个配置就是限制一秒内可请求的次数
{ "ReRoutes": [ { "DownstreamPathTemplate": "/api/{url}", //服务地址--url变量 "DownstreamScheme": "http", "UpstreamPathTemplate": "/consul/{url}", //网关地址--url变量 "UpstreamHttpMethod": [ "Get", "Post" ], "ServiceName": "ZhaoxiUserService", //consul服务名称 "LoadBalancerOptions": { "Type": "RoundRobin" //轮询 LeastConnection-最少连接数的服务器 NoLoadBalance不负载均衡 }, "UseServiceDiscovery": true, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, //允许多少个异常请求 "DurationOfBreak": 10000, // 熔断的时间,单位为ms "TimeoutValue": 10000 //如果下游请求的处理时间超过多少则自如将请求设置为超时 默认90秒 } //"RateLimitOptions": { // "ClientWhitelist": [], //白名单 // "EnableRateLimiting": true, // "Period": "5m", //1s, 5m, 1h, 1d jeffzhang // "PeriodTimespan": 5, //多少秒之后客户端可以重试 // "Limit": 5 //统计时间段内允许的最大请求数量 //}, //"FileCacheOptions": { // "TtlSeconds": 10 //} //"缓存" } ], "GlobalConfiguration": { "BaseUrl": "http://127.0.0.1:6299", //网关对外地址 "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "Consul" //由Consul提供服务发现 }, //"RateLimitOptions": { // "QuotaExceededMessage": "Too many requests, maybe later? 11", // 当请求过载被截断时返回的消息 // "HttpStatusCode": 666 // 当请求过载被截断时返回的http status //} } }
"PeriodTimespan": 5, //多少秒之后客户端可以重试
"Period": "5m" 一秒内最多5次请求,这样即使黑客拿到你的token无法篡改,重放攻击也索然无味了,能影响多少服务器性能呢?
终极技能:配合HTTPS 证书双向认证就更万无一失了,服务器、客户端会双向认证证书,经过3次握手,确认无误后采用随机密码对称加密通信数据,建立私密通信通道,大大增加了黑客攻击的难度。
下一篇来说下HTTPS为什么是终极技能呢?原理是什么?厉害在哪里?
标签:string,小谈,安全策略,counter,JWT,keySize,new,byte,客户端 来源: https://www.cnblogs.com/MethodInvoke/p/14419722.html