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 的同源检测结果:
另外,同源策略又分为以下两种:
- DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
- 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 //先将字符串里的"转为双引号
326 var data = window.tool.replaceAll(data, """, "\"");
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 }
预防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 }
可以看到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
ServletRequest
Springboot之登录模块探索(含Token,验证码,网络安全等知识)的更多相关文章
- 做一个开源的小程序登录模块组件(token)
先了解下SSO 对于单点登陆浅显一点的说就是两种,一种web端的基于Cookie.另一种是跨端的基于Token,一般想要做的都优先做Token吧,个人建议,因为后期扩展也方便哦. 小程序也是呢,做成t ...
- Admin.Admin/Login --- 后台项目中的管理员及登录模块
管理员模块: using System; using System.Collections.Generic; using System.Linq; using System.Web; using Sy ...
- Springboot +redis+⾕歌开源Kaptcha实现图片验证码功能
Springboot +redis+⾕歌开源Kaptcha实现图片验证码功能 背景 注册-登录-修改密码⼀般需要发送验证码,但是容易被 攻击恶意调⽤ 什么是短信-邮箱轰炸机 手机短信轰炸机是批.循环给 ...
- Web应用程序系统的多用户权限控制设计及实现-登录模块【4】
通过前三个模块的介绍,把web权限系统开发所需要的基本类,Css文件,EasyUI框架等准备好后,就可以着手开始系统的编码了. 登陆模块是权限处理系统的关键,根据输入用户的的信息,可自动从数据库中加载 ...
- 如何设计App登录模块?
1.熟悉目前常见的手机APP登陆方式 ① 账号登陆(手机.邮箱) ② 第三方登陆(微信,QQ,微博) ③ 一键快捷登录(工具类,如不记单词) ④ 游客登陆(bbs) ⑤ demo测试登陆(如友盟等) ...
- python爬虫scrapy框架——人工识别登录知乎倒立文字验证码和数字英文验证码(2)
操作环境:python3 在上一文中python爬虫scrapy框架--人工识别知乎登录知乎倒立文字验证码和数字英文验证码(1)我们已经介绍了用Requests库来登录知乎,本文如果看不懂可以先看之前 ...
- SpringBoot注册登录(三):注册--验证账号密码是否符合格式及后台完成注册功能
SpringBoot注册登录(一):User表的设计点击打开链接SpringBoot注册登录(二):注册---验证码kaptcha的实现点击打开链接 SpringBoot注册登录(三):注册 ...
- 手把手教你做JavaWeb项目:登录模块
现如今,无论是客户端还是移动端,无论是游戏登陆还是社交平台登陆,无处不在的“登陆”.那么你知道怎么制作吗?今天就为你娓娓道来: 用户登录 在各大信息管理系统中,登录功能是必不可少的,他的作用就是验证用 ...
- 单点登录详解(token简述)(七)
前言 为什么整理单点登录? 主要的原因还是自己以前学习的时候曾经用过,但是时间太久,忘记了里面用到了哪些技术.及如何实现的,每次想到单点登录总是感觉即会又不会,这次整理session时,又涉及到了单点 ...
随机推荐
- unctf2020 部分简单题题解
unctf2020 水一波简单题..菜狗前来报道..大佬轻喷..如果有时间做题就好了呜呜呜 misc 1.baba_is_you 题目告诉我们,了解png文件格式. 下载得到一张png格式的图片. 用 ...
- FL Studio杂项设置页讲解(上)
今天我们来看一下FL Studio通道设置窗口中的杂项设置页面.该页面存在于FL Studio绝大多数的通道中,我们可以通过它来设置一些发生器或者第三方插件的参数,接下来就让我们一起来学习下这些参数的 ...
- 左右声道音频怎么制作,用Vegas就对啦
一款优秀的视频剪辑软件,不仅有高水平的视频制作功能,它的音频编辑功能也是必不可少的.Vegas就是这么一款软件,同时具备视频制作特效制作的同时,还能帮助制作轨道音频效果. 下面,就让小编带大家去学习, ...
- web自动化测试,弹出窗的操作
弹出窗有两种: 1.alert弹窗 2.页面弹出窗 什么是alert弹窗呢,点击某一个事件后,会弹出一个弹窗,如下图所示,相信大家在测试中有遇到过,怎么操作它呢 1.1弹窗出现后,使用switch_t ...
- Java之 函数(五)
第一部分 : IDEA开发工具 1.数组 1.1 数组介绍 数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致. 1.2 数组的定义格式 1.2.1 第一种格式 数据类型[] 数组名 ...
- python办公入门4:xlrd操作excel行
操作excel行 1 #通过索引获取操作行 2 sheet=data.sheet_by_index(0) 3 #获取当前sheet下的有效行数 4 print(sheet.nrows) 5 #获取某一 ...
- 牛客练习赛71 数学考试 题解(dp)
题目链接 题目大意 要你求出有多少个长度为n的排列满足m个限制条件 第i个限制条件 p[i]表示前 p[i]个数不能是1-p[i]的排列 题目思路 这个感觉是dp但是不知道怎么dp 首先就是要明白如果 ...
- 蓝桥杯——递增三元组(2018JavaB-第6题,11分)
递增三元组(18JB-6-11') 给定三个整数数组 A = [A1, A2, ... AN], B = [B1, B2, ... BN], C = [C1, C2, ... CN], 请你统计有多少 ...
- Ajax Status(状态码) & readyState()
Ajax Status & readyState readyState(状态值) 是指运行AJAX所经历过的几种状态,论访问是否成功都将响应的步骤,可以理解成为AJAX运行步骤,使用" ...
- python办公入门7:xlwt
xlwt写入excel步骤 创建工作簿 创建工作表 填充工作表内容 保存文件 1 import xlwt 2 3 #创建工作簿 4 wb=xlwt.Workbook() 5 #创建工作表 6 ws=w ...