簡介
登入模組很簡單,前端傳送賬號密碼的表單,後端接收驗證後即可~
淦!可是我想多了,於是有了以下幾個問題(裡面還包含網路安全問題):
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