简介

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

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

1.登录时的验证码

2.自动登录的实现

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

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

1.登录时的验证码

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

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

具体可以参考:滑动验证码的设计和理解:
www.cnblogs.com/top-houseke…

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 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

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

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

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

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

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

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

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

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

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

 /*** 访问后台的对象,为ajax封装* @param url 后台资源路径* @param param Map参数* @param contentType 传输类型* @param success   成功回调函数* @param error 失败回调函数* @param requestType 请求类型(get.post,put,delete)* @constructor*/var Query = function (url, param, contentType, successFunc, errorFunc, requestType) {this.url = url;//先确认参数存在if (param) {//如果是get请求类型,则将参数拼接到url后面if (requestType == Query.GET_TYPE) {this.param = this._concatParamToURL(param, url);} else {//其他请求类型,要根据不同的传输格式来确定传输的值的类型if (contentType == Query.NOMAL_TYPE) {this.param = JSON.parse(this._convertParamToJson(param));} else {this.param = this._convertParamToJson(param);}}} else {this.param = null;}this.contentType = contentType;this.successFunc = successFunc;this.errorFunc = errorFunc;//请求超时,默认10秒this.timeout = 10000;//是否异步请求,默认异步this.async = true;this.requestType = requestType;}Query.JSON_TYPE = 'application/json';Query.NOMAL_TYPE = 'application/x-www-form-urlencoded';/*** ajax请求的访问* 默认是post* @param url 要访问的地址* @param paramMap 传给后台的Map参数,key为字符串类型* @param callback 回调函数* @param contentType 传输数据的格式  默认传输application/x-www-form-urlencoded格式*/Query.create = function (url, paramMap, successFunc, errorFunc) {return new Query(url, paramMap, Query.NOMAL_TYPE, successFunc, errorFunc, Query.GET_TYPE);
}//-----------------------以下为RESTFul方法---------------------------//ajax请求类型Query.GET_TYPE = "get";Query.POST_TYPE = "post";Query.PUT_TYPE = "put";Query.DELETE_TYPE = "delete";//get方法默认是Query.NOMAL_TYPEQuery.createGetType = function (url, paramMap, successFunc, errorFunc) {return new Query(url, paramMap, Query.NOMAL_TYPE, successFunc, errorFunc, Query.GET_TYPE);}Query.createPostType = function (url, paramMap, successFunc, errorFunc) {return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.POST_TYPE);}Query.createPutType = function (url, paramMap, successFunc, errorFunc) {return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.PUT_TYPE);}Query.createDeleteType = function (url, paramMap, successFunc, errorFunc) {return new Query(url, paramMap, Query.JSON_TYPE, successFunc, errorFunc, Query.DELETE_TYPE);}/*** 将paramMap参数转为json格式* @param paramMap* @private*/Query.prototype._convertParamToJson = function (paramMap) {return window.tool.strMap2Json(paramMap);}/*** 将参数拼接至URL尾部* @param paramMap* @param url* @private*/Query.prototype._concatParamToURL = function (paramMap, url) {let size = paramMap.size;if (size > 0) {let count = 0;url = url + "?";let urlParam = "";for (let [k, v] of paramMap) {urlParam = urlParam + encodeURIComponent(k) + "=" + encodeURIComponent(v);if (count < size-1) {urlParam = urlParam + " && ";count++;}}url = url + urlParam;}return url;}//ajax需要跳转的界面Query.REDIRECT_URL = "REDIRECT_URL";/*** ajax成功返回时调用的方法* 会根据ajax的ContentType类型,转换Response对象的data给回调的成功函数* 如application/json格式类型,data会转成json类型传递* @param queryResult 返回的值,通常为后台的Response对象* @private*/Query.prototype._successFunc = function (queryResult) {var data = this.__afterSuccessComplete(queryResult);if (this.successFunc) {this.successFunc(data);}//如果有需要跳转的页面,则自动跳转if (data && data.REDIRECT_URL != null) {window.location = data.REDIRECT_URL;}}/*** 会根据ajax的ContentType类型,转换Response对象的data给回调的失败函数* 如application/json格式类型,data会转成json类型传递* 如果对获得的参数不满意,可以用this.getMsg或this.getJsonMsg来进行获取(this指Query对象)** 这里错误分3种* 1.是Web容器出错* 2.是Filter过滤器主动报错(如一些校验失败后主动抛出,会有错误提示)* 3.是Spring抛出,Spring异常会全局捕捉进行封装* @param queryResult 返回的值,通常为后台的Response对象* @private*/Query.prototype._errorFunc = function (queryResult) {//返回的信息var data = this.__afterErrorComplete(queryResult);//如果data里面没东西if (!data) {data = queryResult.statusText;}//是否调用者自身已解决了错误var handleError = false;//调用回调函数,如果返回结果为true,则不会默认错误处理if (this.errorFunc instanceof Function) {handleError = this.errorFunc(data);}//错误编号var code;//错误信息var msg;//没有取消对错误的后续处理,那么进行跳转if (!handleError) {//如果data成功转为Json对象if (data) {//Filter过滤器主动报错(如一些校验失败后主动抛出,会有错误提示)if (data.status) {code = data.status;}if (data.message) {msg = data.message;}}//最终跳转至错误页面var path = "/system/error";if (code && msg) {path = path + "/" + error.code + "/" + error.msg;}window.location.href = path;}}Query.SUCCESS_TYPE = "SUCCESS_TYPE";
Query.ERROR_TYPE = "ERROR_TYPE";/*** 当一个请求完成时,无论成功或失败,都要调用此函数做一些处理* @param queryResult 服务端返回的数据* @returns {*}* @private*/Query.prototype._afterComplete = function (queryResult) {this._cancleLoadDom();}/*** 成功的返回处理,会将data部分转为对象* 默认application/json会进行单引号转双引号* @param queryResult 服务端返回的数据* @param queryResult* @returns {*}* @private*/Query.prototype.__afterSuccessComplete = function (queryResult) {this._afterComplete();this.response = queryResult;var data = queryResult.data;//data必须要有内容,且不是对象才有转换的意义if (data && !(data instanceof Object)) {data = this.getJsonMsg();}return data;}/*** 失败的返回处理* 最终会根据ajax的contentType来进行data相应类型转换* 默认application/json会进行单引号转双引号* @param queryResult 服务端返回的数据* @private*/Query.prototype.__afterErrorComplete = function (queryResult) {this._afterComplete();this.response = queryResult;var data = queryResult.responseJSON;if (!data) {data = queryResult.responseText;}return data;}/*** 取消请求时的等待框* @private*/Query.prototype._cancleLoadDom = function () {//取消加载框if (this.loadDom) {$(this.loadDom).remove("#loadingDiv");}}/*** 正式发送ajax* @private*/Query.prototype.sendMessage = function () {var self = this;var xhr = $.ajax({url: this.url,type: this.requestType,contentType: this.contentType,data: this.param,// ajax发送前调用的方法,初始化等待动画// @param XHR  XMLHttpRequest对象beforeSend: function (XHR) {//试图从Cookie中获得token放入http头部var token = window.tool.getCookieMap().get(window.commonStaticValue.TOKEN);if(token){XHR.setRequestHeader(window.commonStaticValue.TOKEN,token);}//绑定本次请求的queryObjXHR.queryObj = self;if (self.beforeSendFunc instanceof Function) {self.beforeSendFunc(XHR);}if (self.loadDom instanceof HTMLElement) {self.loadDom.innerText = "";$(self.loadDom).append("<div id='loadingDiv' class='loading'><img src='/image/loading.gif'/></div>");} else if (self.loadDom instanceof jQuery) {self.loadDom.empty();self.loadDom.append("<div id='loadingDiv' class='loading'><img src='/image/loading.gif'/></div>");}},//将QueryObj设置为上下文context: self,success: this._successFunc,error: this._errorFunc,complete:function(){console.log("ajax完成");},timeout: this.timeout,async: this.async});}//-----------------------------------下面提供了获取后台返回信息方法(帮忙封装了)
/*** 获取返回信息Response的Meta头*/Query.prototype.getMeta = function () {return this.response.meta;}/*** 获得返回值里的data部分* @returns {*}*/Query.prototype.getMsg = function () {return this.response.data;}/*** 获得返回值里的data部分,尝试将其转为Json对象*/Query.prototype.getJsonMsg = function () {var data = this.response.data;if (data) {//先将字符串里的"转为双引号var data = window.tool.replaceAll(data, """, "\"");try{var jsonData = JSON.parse(data);return jsonData;}catch (e) {return data;}}
}//------------------------以下为对Query的参数设置---------------------------
/*** 在ajax发送前设置参数,可以有加载的动画,并且请求完成后会自动取消* @param loadDom 需要显示动画的dom节点* @param beforeSendFunc ajax发送前的自定义函数*/Query.prototype.setBeforeSend = function (loadDom, beforeSendFunc) {this.loadDom = loadDom;this.beforeSendFunc = beforeSendFunc;}/*** 设置超时时间* @param timeout*/Query.prototype.setTimeOut = function (timeout) {this.timeout = timeout;
}Query.prototype.setAsync = function (async) {this.async = async;}

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: NiceBin3  * @description: 系统的拦截器,注册在FilterConfig类中进行4  *               不能使用@WebFilter,因为Filter要排序5  *               1.对ServletRequest进行封装6  *               2.防止CSRF,检查http头的Referer字段7  * @date: 2020/12/15 15:328  */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: NiceBin3  * @description: 包装的httpServlet,进行以下增强4  *               1.将流数据取出保存,方便多次读出5  *               2.防止XSS攻击,修改读取数据的方法,过滤敏感字符6  * @date: 2020/4/23 19:507  */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 request5      * @return6      */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: NiceBin3  * @description: 为了排序Filter,如果Filter有顺序要求4  *               那么需要在此注册,设置order(值越低优先级越高)5  *               其他没顺序需要的,可以@WebFilter注册6  *               如@WebFilter(filterName = "SecurityFilter", urlPatterns = "/*", asyncSupported = true)7  * @date: 2020/12/15 15:488  */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.Testpublic 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: NiceBin3  * @description: Jwt构造器,创建Token来进行身份记录4  * jwt由3个部分构成:jwt头,有效载荷(主体,payLoad),签名5  * @date: 2020/5/7 22:406  */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小时转为秒 = 360028      * @param claimMap 自定义荷载,可以为空29      * @return30      */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 token85      * @return 存有之前定义的Key, value的Map,解析失败则返回null86      */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通信自带序列号,如果黑客截取了浏览器的请求,重复发送一遍,那么序列号会一样,会被直接丢弃

作者:MarkerHub
链接:
https://juejin.cn/post/7054839835424456712

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

  1. [SpringBoot]使用token 短信验证码 Redis的功能实现基本的登陆注册操作(含Redis token 验证码如何配置)

    浅谈对于登录注册业务的思路以及部分代码的实现 近期开始了与移动端项目的开发,由于我个人是后台方面的所以肯定避免不了要实现每个app都应该具有的功能:登录与注册,我一开始写也是感觉非常的没有头绪,所以这 ...

  2. 【Java闭关修炼】SpringBoot项目-贪吃蛇对战小游戏-配置Mysql与注册登录模块2

    [Java闭关修炼]SpringBoot项目-贪吃蛇对战小游戏-配置Mysql与注册登录模块2 传统的登录验证模式 JWT登录验证方式 下载安装依赖 创建JWTUTIL JwtAuthenticati ...

  3. SpringBoot 实现登录验证码(附集成SpringSecurity)

    SpringBoot 实现登录验证码 1. 生成验证码的工具类 2. 验证码测试接口 3. 验证码过滤器 4. Spring Security配置类引入验证码过滤器 5. 效果图 1. 生成验证码的工 ...

  4. 登录模块与token的使用和创建

    登录模块的使用与创建 一.如何实现登录注册 1.在Vue里创建文件Login.vue和user.vue 2.在router里配置路由 {path: '/user',name: 'User',compo ...

  5. 2.vue3医疗在线问诊项目 - _登录模块 ==> 代码片段、css变量主题定制、cp-nav-bar组件封装、svg打包精灵图插件、cp-icon组件封装、表单校验、密码登录、短信验证码登录及两者

    2.医疗在线问诊项目 - _登录模块 ==> 代码片段.css变量主题定制.cp-nav-bar组件封装.svg打包精灵图插件.cp-icon组件封装.表单校验.密码登录.短信验证码登录及两者的 ...

  6. 002云E办项目之登录模块

    如果你没有看第一篇文章,建议你去看一下 文章链接 好好看每一步 你可以知道什么是逆向工程 你可以在这里好好学习一下登录模块 由于本的sql语句放在001云E办项目创建 一. 逆向工程(创建AutoGe ...

  7. 仿牛客网项目第二章:开发社区登录模块(详细步骤和思路)

    目录 1. 发送邮件 1.0 三步走 1.1 邮箱设置 1.2 Spring Email 1.3 模板引擎 1.4 发送邮件的过程 1.5 检验发送邮件的过程 2. 开发注册功能 2.0 注册功能的步 ...

  8. 【笑小枫的SpringBoot系列】【九】SpringBoot用户登录功能实现

    关于本文 其实用户登录拦截的这块不想这么早写,加个登录后面好多东西就要考虑登录状态了,我其实想把这个系列写成非必要关系,解耦性比较强的系列.但是,写完redis,总是感觉登录是对它最简单的实践,那就加 ...

  9. vue 登录模块滑动验证

    vue 登录模块滑动验证 借助阿里云云盾开发 首先登陆账号开通验证码服务 https://yundun.console.aliyun.com/?spm=5176.2020520162.categori ...

最新文章

  1. python中文读音ndarray-numpy中的ndarray方法和属性
  2. 李彦宏候选院士招致大批网民反对
  3. outlook邮箱邮件大小限制_配置邮箱的邮件大小限制: Exchange 2013 帮助 | Microsoft Docs...
  4. maven插件之build-helper-maven-plugin
  5. 利用最新Apache解析漏洞(CVE-2017-15715)绕过上传黑名单
  6. 人生是自己的选择,双11技术大队长的育女心经
  7. oracle dba (适用,OracleDBA笔试题
  8. 被孟加拉题吊打的ACM考试
  9. Vue开源项目汇总(史上最全)
  10. python代码扫描工具_Python脚本实现Web漏洞扫描工具
  11. 怎么批量给文件夹名称加上数字序号前缀?怎么对文件夹名称进行编号排序?
  12. PG概述及OSD对PG状态的影响
  13. URL Decode - URL解码函数
  14. TiDB 在马上消费金融核心账务系统归档及跑批业务下的实践
  15. Alphago进化史 漫画告诉你Zero为什么这么牛
  16. 天河二号上运行ZHT(a zero-hop distributed table)
  17. 星际战魂java_星际战甲 专精聚魂选择推荐
  18. NVDIA Jetson TX2软件介绍
  19. Java容器【集合】笔记
  20. CQ40_519TX_XP_SP3系统声卡无声、显卡驱动安装方法

热门文章

  1. Des加密原理与简单实现
  2. python画指数函数图像_python中指数函数的回归线拟合
  3. C# 最小二乘法拟合曲线成直线
  4. 【论文笔记】投影仪-相机系统标定方法
  5. DLP投影机投影技术之成像原理与工作流程
  6. 生鲜蔬菜生产称重管理系统
  7. 百度搜索引擎优化指南 for baidu SEO
  8. 地铁逃生的服务器正在维护吗,LifeKeeper为北京地铁保护维护系统
  9. windows bat批处理解压文件
  10. Delphi 多线程传递参数的问题