• Springboot之登录模块探索(含Token,验证码,网络安全等知识)


    简介

    登录模块很简单,前端发送账号密码的表单,后端接收验证后即可~

    淦!可是我想多了,于是有了以下几个问题(里面还包含网络安全问题):

    1.登录时的验证码

    2.自动登录的实现

    3.怎么维护前后端登录状态

    在这和大家分享下我实现此功能的过程,包括一些技术和心得

    1.登录时的验证码

    为什么要验证码,原因很简单,防止脚本无限次重复登录,来暴力破解用户密码或者攻击服务器

    验证码的出现,使得每次登录都有个动态变量需要输入,无法用脚本写死代码

    具体可以参考:滑动验证码的设计和理解

    2.自动登录的实现

    所谓自动登录,指的是当用户登录网站时勾选了自动登录,那么下次再访问网站就不需要输入账号密码直接登录了

    这说明,账号密码信息是必须保存在用户这边的,因此自动登录都是不安全的!(方便的代价呀)

    尽管不安全,但是我们也必须要尽力让它安全一点,有以下常用方法:

    1.账号密码加密保存

    2.降低自动登录后用户的权限(如果用户自动登录想改密码,想给我转钱等操作的话,就必须输入账号密码再登录一次!)

    3.进行ip检测(之前登录的ip小本本记着),如果发现和上次不一致,则不允许自动登录

    数据存储在前端哪里呢

    浏览器有3个经常保存数据的地方

    1.Cookie (我用这个)

    2.LocalStorage

    3.SessionStorage

    各位可以按F12直接观看

    如果你在多个大型网站下都按按F12,会发现SessionStorage基本没数据

    为啥,因为真的不好用,它并不是后台的session那样,生命周期是一个会话,这个SessionStorage存储的数据只限于该标签的页面

    意思是标签1和标签2即使是同个URL的网址,里面的数据都是不互通的(这有个毛用)

    那么LocalStorage存储的数据如何呢,答案是无限期本地存储

    不过后台无法操作这里的数据,只能由js代码操作(至于操作结果,完全看js,后端无法感知,不太可靠),我认为这里不适合保存敏感点的信息,因为前端的功能是展示,状态性的数据应该由后端直接掌控(后端能直接操作Cookie,保证完成任务)

    你看英雄所见略同,CSDN网站的用户密码也是存在Cookie的

     Token就是登录后的令牌(下一点会讲)

    所以用Cookie就对啦,具体实现都很简单,前端多个自动登录的选择,选择后多个参数传给后端,后端根据参数往Cookie里设置加密后的账号密码

    等下次访问时,用拦截器Interceptor进行拦截,检测是否要自动登录即可~

    3.如何维护前后端登录状态

    大家最先想到是用Session来维护,登录后在Session中存放用户信息,不过对分布式很不友好(什么,你说你用不到分布式,我也没用到,可是梦想还是要有的嘛),需要维护个分布式数据库来进行数据同步才行

    于是我用Token实现的,Token就是一串字符串,最适合API鉴权(例如SSO单点登录这种),俗称令牌

    好处就是账号密码用户输入一次就够了,特别是多个系统之间(一张身份的凭证都通用)

    当用户登录后,服务器就会生成一个Token放在Cookie中,之后用户的所有操作都带这个Token访问(将Token放入http头部)

    为什么要将Token放入头部

    1.能抵挡下简单的CSRF攻击

    2.浏览器跨域问题

    什么是CSRF攻击

    举个例子:我登录了A网站,A网站给我返回了一些Cookie信息,然后我再同一浏览器的另外标签访问了B网站,谁知这个B网站返回了一些攻击代码(向A网站发起一些请求,比如转钱给你,这时候由于是访问A网站,会附带A网站的Cookie,让一切都好像是我在访问一样),这个就是CSRF攻击

    但B网站并不知道A网站这么鸡贼,会在头部放了Token,所以这次攻击请求是的头部是没Token的,因此检测后发现非法,所以没得逞

    当然,这并不可靠,哪天B网站知道你头部放了Token,它研究A网站的js代码,清楚逻辑之后也加上,那就防不住了(所以说前端的东西一切都不可靠)

    正确做法应该是后端检测头部的Referer字段,每个网页里发起请求,请求的头部都会带有此字段,如

    这说明这个请求是从 http://localhost:8099/swr 中发出的

    B网站如果返回攻击代码,这里显示的事B网站的网址,判断出不是自家网站发出,就可以禁止访问

    浏览器跨域访问会发生什么

    说到跨域(自家网站去请求别人家的网站),得先了解什么是同源策略:

    同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

    它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

    所谓同源是指:域名、协议、端口相同。

    下表是相对于 http://www.laixiangran.cn/home/index.html 的同源检测结果:

    另外,同源策略又分为以下两种:

    1. DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
    2. XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。(就是ajax)

    咳咳,这里要说下第二种,其实设置一些参数之后,ajax访问时允许跨域请求的,甚至允许跨域时带上自身cookie

    但是,带上自己的Cookie多不安全,明明里面只有1,2个信息要传给对方,现在被人全看见了(不好不好),所以要将Token放入头部

    你说为啥不放到参数里,因为这会跟业务用的参数混淆,造成逻辑混乱(就好像你上学时要扔家里的垃圾,你不会放到书包里吧,都是手里提着的)

    每个请求都放token,所以要封装起来,例如我是将ajax封装起一个新的对象,然后在这个对象使用时添加Token

    当然啦,封装了ajax后还有其他好处(例如统一的成功,失败回调函数,统一的数据解析,统一的等待框等等),有兴趣的同学可以看下

      1 /**
      2  * 访问后台的对象,为ajax封装
      3  * @param url 后台资源路径
      4  * @param param Map参数
      5  * @param contentType 传输类型
      6  * @param success   成功回调函数
      7  * @param error 失败回调函数
      8  * @param requestType 请求类型(get.post,put,delete)
      9  * @constructor
     10  */
     11 var Query = function (url, param, contentType, successFunc, errorFunc, requestType) {
     12     this.url = url;
     13 
     14     //先确认参数存在
     15     if (param) {
     16         //如果是get请求类型,则将参数拼接到url后面
     17         if (requestType == Query.GET_TYPE) {
     18             this.param = this._concatParamToURL(param, url);
     19         } else {
     20             //其他请求类型,要根据不同的传输格式来确定传输的值的类型
     21             if (contentType == Query.NOMAL_TYPE) {
     22                 this.param = JSON.parse(this._convertParamToJson(param));
     23             } else {
     24                 this.param = this._convertParamToJson(param);
     25             }
     26         }
     27     } else {
     28         this.param = null;
     29     }
     30 
     31 
     32     this.contentType = contentType;
     33     this.successFunc = successFunc;
     34     this.errorFunc = errorFunc;
     35     //请求超时,默认10秒
     36     this.timeout = 10000;
     37     //是否异步请求,默认异步
     38     this.async = true;
     39     this.requestType = requestType;
     40 }
     41 
     42 Query.JSON_TYPE = 'application/json';
     43 Query.NOMAL_TYPE = 'application/x-www-form-urlencoded';
     44 
     45 /**
     46  * ajax请求的访问
     47  * 默认是post
     48  * @param url 要访问的地址
     49  * @param paramMap 传给后台的Map参数,key为字符串类型
     50  * @param callback 回调函数
     51  * @param contentType 传输数据的格式  默认传输application/x-www-form-urlencoded格式
     52  */
     53 Query.create = function (url, paramMap, successFunc, errorFunc) {
     54     return new Query(url, paramMap, Query.NOMAL_TYPE, successFunc, errorFunc, Query.GET_TYPE);
     55 }
     56 
     57 //-----------------------以下为RESTFul方法---------------------------
     58 //ajax请求类型
     59 Query.GET_TYPE = "get";
     60 Query.POST_TYPE = "post";
     61 Query.PUT_TYPE = "put";
     62 Query.DELETE_TYPE = "delete";
     63 
     64 //get方法默认是Query.NOMAL_TYPE
     65 Query.createGetType = function (url, paramMap, successFunc, errorFunc) {
     66     return new Query(url, paramMap, Query.NOMAL_TYPE, successFunc, errorFunc, Query.GET_TYPE);
     67 }
     68 Query.createPostType = function (url, paramMap, successFunc, errorFunc) {
     69     return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.POST_TYPE);
     70 }
     71 Query.createPutType = function (url, paramMap, successFunc, errorFunc) {
     72     return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.PUT_TYPE);
     73 }
     74 Query.createDeleteType = function (url, paramMap, successFunc, errorFunc) {
     75     return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.DELETE_TYPE);
     76 }
     77 
     78 /**
     79  * 将paramMap参数转为json格式
     80  * @param paramMap
     81  * @private
     82  */
     83 Query.prototype._convertParamToJson = function (paramMap) {
     84 
     85     return window.tool.strMap2Json(paramMap);
     86 
     87 }
     88 
     89 /**
     90  * 将参数拼接至URL尾部
     91  * @param paramMap
     92  * @param url
     93  * @private
     94  */
     95 Query.prototype._concatParamToURL = function (paramMap, url) {
     96     let size = paramMap.size;
     97 
     98     if (size > 0) {
     99         let count = 0;
    100         url = url + "?";
    101         let urlParam = "";
    102 
    103         for (let [k, v] of paramMap) {
    104             urlParam = urlParam + encodeURIComponent(k) + "=" + encodeURIComponent(v);
    105             if (count < size-1) {
    106                 urlParam = urlParam + " && ";
    107                 count++;
    108             }
    109         }
    110         url = url + urlParam;
    111     }
    112     return url;
    113 }
    114 
    115 //ajax需要跳转的界面
    116 Query.REDIRECT_URL = "REDIRECT_URL";
    117 
    118 /**
    119  * ajax成功返回时调用的方法
    120  * 会根据ajax的ContentType类型,转换Response对象的data给回调的成功函数
    121  * 如application/json格式类型,data会转成json类型传递
    122  * @param queryResult 返回的值,通常为后台的Response对象
    123  * @private
    124  */
    125 Query.prototype._successFunc = function (queryResult) {
    126     var data = this.__afterSuccessComplete(queryResult);
    127     if (this.successFunc) {
    128         this.successFunc(data);
    129     }
    130 
    131     //如果有需要跳转的页面,则自动跳转
    132     if (data && data.REDIRECT_URL != null) {
    133         window.location = data.REDIRECT_URL;
    134     }
    135 }
    136 
    137 /**
    138  * 会根据ajax的ContentType类型,转换Response对象的data给回调的失败函数
    139  * 如application/json格式类型,data会转成json类型传递
    140  * 如果对获得的参数不满意,可以用this.getMsg或this.getJsonMsg来进行获取(this指Query对象)
    141  *
    142  * 这里错误分3种
    143  * 1.是Web容器出错
    144  * 2.是Filter过滤器主动报错(如一些校验失败后主动抛出,会有错误提示)
    145  * 3.是Spring抛出,Spring异常会全局捕捉进行封装
    146  * @param queryResult 返回的值,通常为后台的Response对象
    147  * @private
    148  */
    149 Query.prototype._errorFunc = function (queryResult) {
    150 
    151     //返回的信息
    152     var data = this.__afterErrorComplete(queryResult);
    153     //如果data里面没东西
    154     if (!data) {
    155         data = queryResult.statusText;
    156     }
    157 
    158     //是否调用者自身已解决了错误
    159     var handleError = false;
    160 
    161     //调用回调函数,如果返回结果为true,则不会默认错误处理
    162     if (this.errorFunc instanceof Function) {
    163         handleError = this.errorFunc(data);
    164     }
    165 
    166     //错误编号
    167     var code;
    168     //错误信息
    169     var msg;
    170 
    171     //没有取消对错误的后续处理,那么进行跳转
    172     if (!handleError) {
    173 
    174         //如果data成功转为Json对象
    175         if (data) {
    176             //Filter过滤器主动报错(如一些校验失败后主动抛出,会有错误提示)
    177             if (data.status) {
    178                 code = data.status;
    179             }
    180             if (data.message) {
    181                 msg = data.message;
    182             }
    183         }
    184 
    185         //最终跳转至错误页面
    186         var path = "/system/error";
    187         if (code && msg) {
    188             path = path + "/" + error.code + "/" + error.msg;
    189         }
    190         window.location.href = path;
    191     }
    192 }
    193 
    194 Query.SUCCESS_TYPE = "SUCCESS_TYPE";
    195 Query.ERROR_TYPE = "ERROR_TYPE";
    196 /**
    197  * 当一个请求完成时,无论成功或失败,都要调用此函数做一些处理
    198  * @param queryResult 服务端返回的数据
    199  * @returns {*}
    200  * @private
    201  */
    202 Query.prototype._afterComplete = function (queryResult) {
    203     this._cancleLoadDom();
    204 }
    205 
    206 /**
    207  * 成功的返回处理,会将data部分转为对象
    208  * 默认application/json会进行单引号转双引号
    209  * @param queryResult 服务端返回的数据
    210  * @param queryResult
    211  * @returns {*}
    212  * @private
    213  */
    214 Query.prototype.__afterSuccessComplete = function (queryResult) {
    215     this._afterComplete();
    216     this.response = queryResult;
    217 
    218     var data = queryResult.data;
    219     //data必须要有内容,且不是对象才有转换的意义
    220     if (data && !(data instanceof Object)) {
    221             data = this.getJsonMsg();
    222     }
    223     return data;
    224 }
    225 
    226 /**
    227  * 失败的返回处理
    228  * 最终会根据ajax的contentType来进行data相应类型转换
    229  * 默认application/json会进行单引号转双引号
    230  * @param queryResult 服务端返回的数据
    231  * @private
    232  */
    233 Query.prototype.__afterErrorComplete = function (queryResult) {
    234     this._afterComplete();
    235     this.response = queryResult;
    236     var data = queryResult.responseJSON;
    237     if (!data) {
    238         data = queryResult.responseText;
    239     }
    240 
    241     return data;
    242 }
    243 
    244 /**
    245  * 取消请求时的等待框
    246  * @private
    247  */
    248 Query.prototype._cancleLoadDom = function () {
    249     //取消加载框
    250     if (this.loadDom) {
    251         $(this.loadDom).remove("#loadingDiv");
    252     }
    253 }
    254 
    255 /**
    256  * 正式发送ajax
    257  * @private
    258  */
    259 Query.prototype.sendMessage = function () {
    260     var self = this;
    261     var xhr = $.ajax(
    262         {
    263             url: this.url,
    264             type: this.requestType,
    265             contentType: this.contentType,
    266             data: this.param,
    267             // ajax发送前调用的方法,初始化等待动画
    268             // @param XHR  XMLHttpRequest对象
    269             beforeSend: function (XHR) {
    270                 //试图从Cookie中获得token放入http头部
    271                 var token = window.tool.getCookieMap().get(window.commonStaticValue.TOKEN);
    272                 if(token){
    273                     XHR.setRequestHeader(window.commonStaticValue.TOKEN,token);
    274                 }
    275 
    276                 //绑定本次请求的queryObj
    277                 XHR.queryObj = self;
    278                 if (self.beforeSendFunc instanceof Function) {
    279                     self.beforeSendFunc(XHR);
    280                 }
    281 
    282                 if (self.loadDom instanceof HTMLElement) {
    283                     self.loadDom.innerText = "";
    284                     $(self.loadDom).append("<div id='loadingDiv' class='loading'><img src='/image/loading.gif'/></div>");
    285                 } else if (self.loadDom instanceof jQuery) {
    286                     self.loadDom.empty();
    287                     self.loadDom.append("<div id='loadingDiv' class='loading'><img src='/image/loading.gif'/></div>");
    288                 }
    289             },
    290             //将QueryObj设置为上下文
    291             context: self,
    292             success: this._successFunc,
    293             error: this._errorFunc,
    294             complete:function(){
    295               console.log("ajax完成");
    296             },
    297             timeout: this.timeout,
    298             async: this.async
    299         }
    300     );
    301 }
    302 
    303 //-----------------------------------下面提供了获取后台返回信息方法(帮忙封装了)
    304 /**
    305  * 获取返回信息Response的Meta头
    306  */
    307 Query.prototype.getMeta = function () {
    308     return this.response.meta;
    309 }
    310 
    311 /**
    312  * 获得返回值里的data部分
    313  * @returns {*}
    314  */
    315 Query.prototype.getMsg = function () {
    316     return this.response.data;
    317 }
    318 
    319 /**
    320  * 获得返回值里的data部分,尝试将其转为Json对象
    321  */
    322 Query.prototype.getJsonMsg = function () {
    323     var data = this.response.data;
    324     if (data) {
    325             //先将字符串里的&quot;转为双引号
    326             var data = window.tool.replaceAll(data, "&quot;", """);
    327             try{
    328                 var jsonData = JSON.parse(data);
    329                 return jsonData;
    330             }catch (e) {
    331                 return data;
    332             }
    333     }
    334 }
    335 
    336 //------------------------以下为对Query的参数设置---------------------------
    337 /**
    338  * 在ajax发送前设置参数,可以有加载的动画,并且请求完成后会自动取消
    339  * @param loadDom 需要显示动画的dom节点
    340  * @param beforeSendFunc ajax发送前的自定义函数
    341  */
    342 Query.prototype.setBeforeSend = function (loadDom, beforeSendFunc) {
    343     this.loadDom = loadDom;
    344     this.beforeSendFunc = beforeSendFunc;
    345 }
    346 
    347 /**
    348  * 设置超时时间
    349  * @param timeout
    350  */
    351 Query.prototype.setTimeOut = function (timeout) {
    352     this.timeout = timeout;
    353 }
    354 
    355 Query.prototype.setAsync = function (async) {
    356     this.async = async;
    357 }
    View Code

    预防XSS攻击,Filter知识讲解

    网上有些文章说,后端设置HttpOnly,让Cookie无法让js读写,可以防止XSS攻击。

    (⊙o⊙)…简直就是乱写,首先要了解下什么是XSS攻击

    Xss攻击是什么

    举个简单的例子,假设你前端有个地方可以输入,然后保存的数据库的地方

    用户A输入了以下东西

    <script>alert(123)</script>

    然后这东西就到了后台,当作一串字符串保存了起来

    刚好你网站的html代码里,有个地方是显示用户输入过的东西的(例如评论区),然后上面的东西就被加载到html里面,如

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title></title>
            <p><script>alert(123)</script></p>
        </head>
        <body>
        </body>
    </html>

    接下来每个人打开你的网站,都会弹出123的对话框,这就是XSS攻击

    怎么预防呢,在后端设置过滤器,对输入进行过滤,先上代码

     1 /**
     2  * @auther: NiceBin
     3  * @description: 系统的拦截器,注册在FilterConfig类中进行
     4  *               不能使用@WebFilter,因为Filter要排序
     5  *               1.对ServletRequest进行封装
     6  *               2.防止CSRF,检查http头的Referer字段
     7  * @date: 2020/12/15 15:32
     8  */
     9 @Component
    10 public class SystemFilter implements Filter {
    11     private final Logger logger = LoggerFactory.getLogger(SystemFilter.class);
    12     @Autowired
    13     private Environment environment;
    14 
    15     @Override
    16     public void init(FilterConfig filterConfig) throws ServletException {
    17         logger.info("系统拦截器SystemFilter开始加载");
    18     }
    19 
    20     @Override
    21     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    22         SystemHttpServletRequestWrapper requestWrapper = new SystemHttpServletRequestWrapper((HttpServletRequest) request);
    23 
    24         //检测http的Referer字段,不允许跨域访问
    25         String hostPath = environment.getProperty("server.host-path");
    26         String referer = requestWrapper.getHeader("Referer");
    27         if(!Tool.isNull(referer)){
    28             if(referer.lastIndexOf(hostPath)!=0){
    29                 ((HttpServletResponse)response).setStatus(HttpStatus.FORBIDDEN.value()); //设置错误状态码
    30                 return;
    31             }
    32         }
    33         chain.doFilter(requestWrapper,response);
    34     }
    35 
    36     @Override
    37     public void destroy() {
    38 
    39     }
    40 }

    乍一看,是不是没发现哪里预防了XSS,其实正在的关键点在22行和33行代码,里面的SystemHttpServletRequestWrapper类才是关键,这个类是包装类,是替换参数里的ServletRequest类的,为的就是重写里面的方法,来达到预防XSS的目的,因为Spring也是根据ServletRequest类来进行前端参数读取的,所以它就是后端获得数据的源头

     1 /**
     2  * @auther: NiceBin
     3  * @description: 包装的httpServlet,进行以下增强
     4  *               1.将流数据取出保存,方便多次读出
     5  *               2.防止XSS攻击,修改读取数据的方法,过滤敏感字符
     6  * @date: 2020/4/23 19:50
     7  */
     8 public class SystemHttpServletRequestWrapper extends HttpServletRequestWrapper {
     9     private final byte[] body;
    10     private HttpServletRequest request;
    11 
    12     public SystemHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    13         super(request);
    14         //打印属性
    15         //printRequestAll(request);
    16         body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));  //HttpHelper是我自己写的工具类
    17         this.request = request;
    18     }
    19 
    20     @Override
    21     public BufferedReader getReader() throws IOException {
    22         return new BufferedReader(new InputStreamReader(getInputStream()));
    23     }
    24 
    25     @Override
    26     public ServletInputStream getInputStream() throws IOException {
    27         final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    28         return new ServletInputStream() {
    29             @Override
    30             public boolean isFinished() {
    31                 return false;
    32             }
    33 
    34             @Override
    35             public boolean isReady() {
    36                 return false;
    37             }
    38 
    39             @Override
    40             public void setReadListener(ReadListener readListener) {
    41 
    42             }
    43 
    44             @Override
    45             public int read() throws IOException {
    46                 return bais.read();
    47             }
    48         };
    49     }
    50 
    51     /**
    52      * 可以打印出HttpServletRequest里属性的值
    53      * @param request
    54      */
    55     public void printRequestAll(HttpServletRequest request){
    56         Enumeration e = request.getHeaderNames();
    57         while (e.hasMoreElements()) {
    58             String name = (String) e.nextElement();
    59             String value = request.getHeader(name);
    60             System.out.println(name + " = " + value);
    61         }
    62     }
    63 
    64     //以下为XSS预防
    65     @Override
    66     public String getParameter(String name) {
    67         String value = request.getParameter(name);
    68         if (!StringUtils.isEmpty(value)) {
    69             value = StringEscapeUtils.escapeHtml4(value);
    70         }
    71         return value;
    72     }
    73 
    74     @Override
    75     public String[] getParameterValues(String name) {
    76         String[] parameterValues = super.getParameterValues(name);
    77         if (parameterValues == null) {
    78             return null;
    79         }
    80         for (int i = 0; i < parameterValues.length; i++) {
    81             String value = parameterValues[i];
    82             parameterValues[i] = StringEscapeUtils.escapeHtml4(value);
    83         }
    84         return parameterValues;
    85     }
    86 }

    HttpHelper工具类:

     1 public class HttpHelper {
     2     /**
     3      * 获取请求中的Body内容
     4      * @param request
     5      * @return
     6      */
     7     public static String getBodyString(ServletRequest request) {
     8         StringBuilder sb = new StringBuilder();
     9         InputStream inputStream = null;
    10         BufferedReader reader = null;
    11         try {
    12             inputStream = request.getInputStream();
    13             reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
    14             String line = "";
    15             while ((line = reader.readLine()) != null) {
    16                 sb.append(line);
    17             }
    18         } catch (IOException e) {
    19             e.printStackTrace();
    20         } finally {
    21             if (inputStream != null) {
    22                 try {
    23                     inputStream.close();
    24                 } catch (IOException e) {
    25                     e.printStackTrace();
    26                 }
    27             }
    28             if (reader != null) {
    29                 try {
    30                     reader.close();
    31                 } catch (IOException e) {
    32                     e.printStackTrace();
    33                 }
    34             }
    35         }
    36         return sb.toString();
    37     }
    38 }
    View Code

    可以看到SystemHttpServletRequestWrapper的64行开始,重写了两个获取参数的方法,在获取参数的时候进行过滤即可~

    那64行往上是干啥的咧,这个是将ServletRequest里的数据读出来保存一份,因为ServletRequest里的数据流只能读取一次,很不方便

    啥意思呢,就是你在这个Filter里

    inputStream = request.getInputStream();
    reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
    String line = "";
    while ((line = reader.readLine()) != null) {
    sb.append(line);
    }

    把数据读完,下个Filter再执行这些代码,就没数据了(从而导致Spring也接收不到数据)

    所以要保存起来,让后面的过滤器Filter和拦截器Interceptor快乐的读数据,没有后顾之忧(例如上面提到的验证码设计,如果你想用拦截器拦截,然后进行验证,则势必会读数据),既然封装ServletRequest这么重要,那必须得保证这个Filter第一个加载啊

    在Springboot中,Filter的排序用@Order是没用的,必须要用FilterRegistrationBean进行注册才能排序,如:

     1 /**
     2  * @auther: NiceBin
     3  * @description: 为了排序Filter,如果Filter有顺序要求
     4  *               那么需要在此注册,设置order(值越低优先级越高)
     5  *               其他没顺序需要的,可以@WebFilter注册
     6  *               如@WebFilter(filterName = "SecurityFilter", urlPatterns = "/*", asyncSupported = true)
     7  * @date: 2020/12/15 15:48
     8  */
     9 @Configuration
    10 public class FilterConfig {
    11 
    12     @Autowired
    13     SystemFilter systemFilter;
    14     /**
    15      * 注册SystemFilter,顺序为1,任何其他filter不能比他优先
    16      * @return
    17      */
    18     @Bean
    19     public FilterRegistrationBean filterRegist(){
    20         FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    21         filterRegistrationBean.setFilter(systemFilter);
    22         filterRegistrationBean.setName("SystemFilter");
    23         filterRegistrationBean.addUrlPatterns("/*");
    24         filterRegistrationBean.setAsyncSupported(true);
    25         filterRegistrationBean.setOrder(1);
    26         return filterRegistrationBean;
    27     }
    28 }

    当然了,如果你没用Springboot,那web.xml中定义的顺序就是Filter加载的顺序

    知识点提问:在我们之后的Filter或者Interceptor中,需要

    1 SystemHttpServletRequestWrapper requestWrapper = (SystemHttpServletRequestWrapper) request

    这样强制转换才能用吗?

    答案是不用的,你可以想想Spring也用了这个东西的,它怎么知道你定义的类叫什么名字,怎么强制转换,那么这设计到Java什么知识呢

    没错,就是Java的多态性,我们看以下代码

    public class Father {
        public void sayName(){
            System.out.println("我是爸爸");
        }
    }
    
    public class Son extends Father{
        public void sayName(){
            System.out.println("我是儿子");
        }
    }
    
    public class Test {
    
        @org.junit.Test
        public void test() throws Exception {
            Father father = new Son();
            otherMethod(father);
        }
    
        public void otherMethod(Father father){
            father.sayName();
        }
    }

    输出:我是儿子

    答错了的留言,看看有多少小伙子~~ 接下来言归正传

    选择JWT生成Token

    JWT全称JSON Web Tokens 是一种规范化的 token(别人想的挺多挺全面的了,比你自己想的token要好一点)

    一个 JWT token 是一个字符串,它由三部分组成,头部、载荷与签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

    头部(header)

    头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA.)。

    例如:

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    然后用 Base64Url 编码得到头部,即 xxxxx。Base64Url编码后,才能在URL中正常传输(因为有人会把Token放在URL里.....)

    载荷(Payload)

    载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换,如:

    {
      "sub": "1",
    
      "iss": "http://localhost:8000/auth/login",
    
      "iat": 1451888119,
    
      "exp": 1454516119,
    
      "nbf": 1451888119,
    
      "jti": "37c107e4609ddbcc9c096ea5ee76c667",
    
      "aud": "dev"
    
    }

    可以将载荷用别的方式加密一遍,这样别人得到了token也看不懂

    签名(Signature)

    签名时需要用到前面编码过的两个字符串,如果以 HMACSHA256 加密,就如下:

    HMACSHA256(
    
        base64UrlEncode(header) + "." +
    
        base64UrlEncode(payload),
    
        secret
    
    )

    加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz。

    组合便可以得到 token:xxxxx.yyyyy.zzzzz。

    签名的作用:保证 JWT 没有被篡改过,原理如下:

    HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

    具体Java使用:

    <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.10.2</version>
            </dependency>
            <!--jwt一些工具类-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
    </dependency>
      1 **
      2  * @auther: NiceBin
      3  * @description: Jwt构造器,创建Token来进行身份记录
      4  * jwt由3个部分构成:jwt头,有效载荷(主体,payLoad),签名
      5  * @date: 2020/5/7 22:40
      6  */
      7 public class JwtTool {
      8 
      9     //以下为JwtTool生成时的主题
     10     //登录是否还有效
     11     public static final String SUBJECT_ONLINE_STATE = "online_state";
     12 
     13     //以下为载荷固定的Key值
     14     //主题
     15     public static final String SUBJECT = "subject";
     16     //发布时间
     17     public static final String TIME_ISSUED = "timeIssued";
     18     //过期时间
     19     public static final String EXPIRATION = "expiration";
     20 
     21     /**
     22      * 生成token,参数都是载荷(自定义内容)
     23      * 其中Map里为非必要数据,而其他参数为必要参数
     24      *
     25      * @param subject  主题,token生成干啥用的,用上面的常量作为参数
     26      * @param liveTime 存活时间(秒单位),建议使用TimeUnit方便转换
     27      *                 如TimeUnit.HOURS.toSeconds(1);将1小时转为秒 = 3600
     28      * @param claimMap 自定义荷载,可以为空
     29      * @return
     30      */
     31     public static String createToken(String subject, long liveTime, HashMap<String, String> claimMap) throws Exception {
     32 
     33         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
     34 
     35         //毫秒要转为秒
     36         long now = System.currentTimeMillis() / 1000;
     37 
     38 //        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(EncrypRSA.keyString);
     39 //
     40 //        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
     41 
     42         JwtBuilder jwtBuilder = Jwts.builder()
     43                 //加密算法
     44                 .setHeaderParam("alg", "HS256")
     45                 //jwt签名
     46                 .signWith(signatureAlgorithm, EncrypRSA.convertSecretKey);  //这个Key是我自个的密码,你们自己设个字符串也成,这个得保密
     47 
     48         HashMap<String,String> payLoadMap = new HashMap<>();
     49         payLoadMap.put(SUBJECT,subject);
     50         payLoadMap.put(TIME_ISSUED,String.valueOf(now));
     51         //设置Token的过期时间
     52         if (liveTime >= 0) {
     53             long expiration = now + liveTime;
     54             payLoadMap.put(EXPIRATION,String.valueOf(expiration));
     55         } else {
     56             throw new SystemException(SystemStaticValue.TOOL_PARAMETER_EXCEPTION_CODE, "liveTime参数异常");
     57         }
     58 
     59         StringBuilder payLoad = new StringBuilder();
     60 
     61 
     62 
     63         if (!Collections.isEmpty(claimMap)) {
     64             payLoadMap.putAll(claimMap);
     65         }
     66 
     67         //拼接主题payLoad,采用 key1,value1,key2,value2的格式
     68         for (Map.Entry<String, String> entry : payLoadMap.entrySet()) {
     69             payLoad.append(entry.getKey()).append(',').append(entry.getValue()).append(',');
     70         }
     71 
     72         //对payLoad进行加密,这样别人Base64URL解密后也不是明文
     73         String encrypPayLoad = EncrypRSA.encrypt(payLoad.toString());
     74 
     75         jwtBuilder.setPayload(encrypPayLoad);
     76 
     77         //会自己生成签名,组装
     78         return jwtBuilder.compact();
     79     }
     80 
     81     /**
     82      * 私钥解密token信息
     83      *
     84      * @param token
     85      * @return 存有之前定义的Key, value的Map,解析失败则返回null
     86      */
     87     public static HashMap getMap(String token) {
     88         if (!Tool.isNull(token)) {
     89             try {
     90                 String encrypPayLoad = Jwts.parser()
     91                         .setSigningKey(EncrypRSA.convertSecretKey)
     92                         .parsePlaintextJws(token).getBody();
     93 
     94                 String payLoad = EncrypRSA.decrypt(encrypPayLoad);
     95 
     96                 String[] payLoads = payLoad.split(",");
     97                 HashMap<String, String> map = new HashMap<>();
     98                 for (int i = 0; i < payLoads.length - 1; i=i+2) {
     99                     map.put(payLoads[i], payLoads[i + 1]);
    100                 }
    101                 return map;
    102             } catch (Exception e) {
    103                 System.out.println("Token解析失败");
    104                 return null;
    105             }
    106         } else {
    107             return null;
    108         }
    109     }
    110 
    111     /**
    112      * 判断token是否有效
    113      *
    114      * @param map 已经解析过token的map
    115      * @return true 为有效
    116      */
    117     public static boolean isAlive(HashMap<String, String> map) {
    118 
    119         if (!Collections.isEmpty(map)) {
    120             String tokenString = map.get(EXPIRATION);
    121 
    122             if (!Tool.isNull(tokenString)) {
    123                 long expiration = Long.valueOf(tokenString) / 1000;
    124                 long now = System.currentTimeMillis();
    125                 if (expiration > now) {
    126                     return true;
    127                 } else {
    128                     return false;
    129                 }
    130             }
    131         }
    132         return false;
    133     }
    134 
    135     /**
    136      * 判断token是否有效
    137      * @param token 还未被解析的token
    138      * @return
    139      */
    140     public static boolean isAlive(String token) {
    141         return JwtTool.isAlive(JwtTool.getMap(token));
    142     }
    143 }

    至此,Token的生成和使用就介绍完了,大家有没兴趣了解下重放攻击(淦,我也是在某个博文看到的,又得花时间研究)

    Https防止半路被截和重放攻击

    前面提到了Token就是身份令牌,可以相当于已登录一样进入系统,那么半路被人截了那就不好了

    所以要用Https协议,具体怎么设置大家自行百度吧(直接在tomcat操作的,不需要更改代码,证书也有免费的~)

    这里说下Https建立连接的过程,来看看为什么就不会被人截获了

    1.服务器先向CA(证书颁布机构)申请一个证书(证书里有自己的ip等等消息),然后在自己服务器设置好

    2.浏览器向服务器发送HTTPS请求,服务器将自己的证书发给浏览器

    3.浏览器拿到证书后,查看证书是否过期啊,ip是不是跟服务器的一样啊,跟检查身份证跟你长得像不像一样,检查没问题后,跟自己系统里的CA列表比对,看看是谁发的(找不到就报错,说证书不可信),比对成功后从列表里拿出对应的CA公钥解密证书(具体方法跟JWT的很像,浏览器用相同的算法和公钥对证书部分进行加密,看得到的值和证书的签名是否一致),得到服务器的公钥

    4.然后生成一个传输私钥,用服务器的公钥加密,发给服务器

    5.服务器用服务器的私钥解密,得到了传输秘钥,然后用传输秘钥进行加密要传送的信息发给浏览器

    6.浏览器用秘钥解密,然后用传输秘钥进行加密要传送的信息发给服务器(对称加密)

    7.重复5,6步骤直到结束

    以上哪个步骤黑客得到数据都看不懂

    至于为什么能防重放攻击,是因为Https通信自带序列号,如果黑客截取了浏览器的请求,重复发送一遍,那么序列号会一样,会被直接丢弃

    至此分享完啦,喜欢的小伙伴给个赞呀~~

    参考:https://learnku.com/articles/17883

         https://www.cnblogs.com/laixiangran/p/9064769.html

    学习和点赞一样,可不能下次一定啊! 感谢你的三连~
  • 相关阅读:
    CentOS同步时间
    使用dnsmasq来提升CentOS上网速度
    bash的变量设置
    CentOS找回root密码
    知识学习网站
    webservice接口测试,使用SoapUI工具进行接口测试
    js中字符串转换为数字
    css颜色大全
    Table分页显示调整
    iframe中,页面转换后回到页面的顶部
  • 原文地址:https://www.cnblogs.com/top-housekeeper/p/14141871.html
Copyright © 2020-2023  润新知