Springboot之登入模組探索(含Token,驗證碼,網路安全等知識)

半天想不出暱稱的斌發表於2020-12-16

簡介

登入模組很簡單,前端傳送賬號密碼的表單,後端接收驗證後即可~

淦!可是我想多了,於是有了以下幾個問題(裡面還包含網路安全問題):

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後還有其他好處(例如統一的成功,失敗回撥函式,統一的資料解析,統一的等待框等等),有興趣的同學可以看下

Springboot之登入模組探索(含Token,驗證碼,網路安全等知識)
  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工具類:

Springboot之登入模組探索(含Token,驗證碼,網路安全等知識)
 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

ServletRequest

相關文章