其他分享
首页 > 其他分享> > 小谈JWT安全策略(一)

小谈JWT安全策略(一)

作者:互联网

春节假期总是那么短暂,回到岗位继续撸码。JWT对于大多数开发者来说都不会陌生,多用于分布式架构方面的认证,由于其不可篡改特性(非对称加密手段),使其无法被伪造;

下面描述一下JWT产生及应用的简单场景:有下面几个中心服务器:授权中心、WebAPI1、WebAPI2、WebApi3等、PC客户端、移动客户端、其他客户端,这些客户端想要访问WebApi就需要授权认证才可以访问,如当移动了客户端首次访问webApi1时由于没有登录,跳转到授权中心登录认证,输入用户名、密码后返回token给客户端,下次客户端带着这个token便可以随意穿梭于各个API了,大致就是这意思,但是里面有很多安全问题值得思考;

问题一:提交用户名、密码到授权中心时如何保证密码不被抓取泄漏,以及CSRF攻击呢? 

客户端到服务器(授权中心)间会经过很多个网络节点,期间抓包一定会抓到提交的用户名、密码,所以密码必须加密,用户名无所谓加密否,很多大厂如百度、新浪都没加密用户名,那么如何加密呢?JS前端+非对称加密(RSA),JS用公钥加密密码后通过ajax提交服务器,服务器再用私钥解密密码进行验证,RSA有两种格式PEMXML,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