由前端登陸驗證,頁面跳轉,攜帶headers token引起的思考和嘗試

目錄html

1 前言

在作工程實踐項目的管理員模塊時,我想實現下面的效果:前端

  • 1)在未登陸狀態下經過url訪問 /pages/admin/** 下的靜態頁面,除了 login.html,其餘都會被攔截,而後跳轉到 login.html 頁面;
  • 2)在 login.html 頁面登陸後,會自動跳轉到 /pages/admin/index.html 頁面;

先給個效果圖,對應的是:解決方案 3.3 放棄後端對/pages/admin/** 下靜態頁面的攔截,在前端作登陸檢測和跳轉vue

對錄屏軟件感興趣的請戳:Apowersoft 免費在線錄屏java

 

回到目錄jquery

2 個人實現方式與存在的問題

1)後端定義 JWTAdminInterceptor.java 來驗證登陸狀態,若是未登陸則重定向到 /pages/admin/login.html 頁面。代碼以下:ios

 1 /**
 2  * JWT驗證攔截器(管理員),對於須要身份認證的請求,必須先通過該攔截器處理
 3  * @author southday
 4  * @date 2019/2/26
 5  */
 6 public class JWTAdminInterceptor extends HandlerInterceptorAdapter {
 7     private static final Logger logger = LogManager.getLogger(JWTAdminInterceptor.class);
 8 
 9     @Autowired
10     private AdminService adminService;
11 
12     @Override
13     public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
14         String jws = JWTer.getToken();
15         JWTer jwter = new JWTer(jws);
16         boolean flag = false;
17         if (!jwter.isUsable()) {
18             logger.info("權限驗證失敗,異常:" + jwter.getException().getMessage() + " | [token = " + jws + "]");
19         } else if (!CommonConst.USER_TYPE_ADMIN.equals(jwter.getUserType())) {
20             logger.info("權限驗證失敗,用戶類型不匹配,[token = " + jws + "]");
21         } else {
22             flag = adminService.isAdminExists(jwter.getUserName());
23         }
24         if (!flag) {
25             resp.setStatus(401);
26             resp.sendRedirect("/idevtools/pages/admin/login.html");
27         }
28         return flag;
29     }
30 }
View Code
2)spring-mvc.xml 攔截器配置以下:攔截器中配置了對 /pages/admin/** 下的全部靜態頁面進行攔截,除了 /pages/admin/login.html 頁面;
 1 <!-- 攔截器配置 southday 2019.02.26 -->
 2 <mvc:interceptors>
 3     <!-- JWT 身份驗證攔截器,針對管理員須要先進行登錄後才能操做的請求進行攔截 -->
 4     <mvc:interceptor>
 5         <mvc:mapping path="/a/**"/>
 6         <!-- 配置管理員模塊靜態頁面的攔截 southday 2019.05.17 -->
 7         <mvc:mapping path="/pages/admin/**"/>
 8         <mvc:exclude-mapping path="/a/login"/>
 9         <mvc:exclude-mapping path="/a/adminInfo"/>
10         <mvc:exclude-mapping path="/pages/admin/login.html"/>
11         <bean class="cn.idevtools.interceptor.JWTAdminInterceptor"/>
12     </mvc:interceptor>
13 </mvc:interceptors>
View Code

3)前端在未登陸的狀況下訪問:http://localhost:8080/idevtools/pages/admin/index.html,就會被攔截,而後重定向到管理員登陸頁面;web

4)管理員 login.html 中加載了 admin.js 來實現登陸,登陸後要跳轉到 /pages/admin/index.html 頁面;以下:spring

 1 /**
 2  * 管理員登錄模態框 /pages/admin/login.html
 3  * southday 2019.05.17
 4  */
 5 let vmAdminLogin = new Vue({
 6     el: "#admin-login",
 7     data: {
 8         adminName: '',
 9         password: '',
10         jcaptcha: '',
11         jcaptchaURL: cookurl('/idevtools/jcaptcha.jpg')
12     },
13     methods: {
14         login: function() {
15             axios({
16                 method: 'post',
17                 url: cookurl('/idevtools/a/login'),
18                 params: {
19                     adminName: vmAdminLogin.adminName,
20                     password: vmAdminLogin.password,
21                     jcaptcha: vmAdminLogin.jcaptcha
22                 }
23             }).then(function(resp) {
24                 let ret = resp.data
25                 if (ret.code == 'VALID_ERROR') {
26                     showValidMsgs(ret.data)
27                 } else if (ret.code == 'FAILURE') {
28                     toastr.error(ret.msg)
29                 } else {
30                     saveAdmin(ret.data)
31                     saveAdminToken(resp.headers.token)
32                     window.location.href = "/idevtools/pages/admin/index.html"
33                 }
34                 vmAdminLogin.changeJCaptcha()
35             }).catch(function(error) {
36                 console.log(error)
37                 vmAdminLogin.changeJCaptcha()
38             })
39         },
40         changeJCaptcha: function() {
41             vmAdminLogin.jcaptchaURL = changeVerifyCode()
42         },
43         logout: function() {
44             axios({
45                 method: 'post',
46                 url: cookurl('/idevtools/a/logout'),
47                 headers: {'token': getAdminToken()}
48             }).then(function(resp) {
49                 let ret = resp.data
50                 if (ret.code == "SUCCESS") {
51                     saveAdmin(null)
52                     saveAdminToken(null)
53                 } else {
54                     toastr.error(ret.msg)
55                 }
56             }).catch(function(error) {
57                 console.log(error)
58             })
59         }
60     }
61 })
View Code

存在的問題:bootstrap

問題就在於(4)中管理員登陸後的跳轉語句: window.location.href = "/idevtools/pages/admin/index.html", 其沒有攜帶headers: {'token': getAdminToken()},因此在跳轉時會被後端攔截,而後又重定向到 login.html,就這樣一直循環;

3 我想到的解決方案

3.1 前端跳轉時攜帶headers{'token': token} 不就好了(經驗證不可行)axios

很遺憾,我找了好多資料,目前發現並不能實現這樣的效果;通常用js作前端跳轉,代碼爲: window.location.href = "/idevtools/pages/admin/index.html", 查閱資料後得知 window.location 中並不支持 headers 的設置,以下:

(圖源:Location 對象:https://www.runoob.com/jsref/obj-location.html

3.2 前端跳轉封裝請求,攜帶headers{'token': token},後端請求轉發 (經驗證不可行)

1)在前端封裝一個方法用來提交請求,參數爲要跳轉的目標url,以下:

 1 function redirect(url) {
 2     axios({
 3         method: 'get',
 4         url: cookurl(url),
 5         headers: {'token': getAdminToken()}
 6     }).then(function(resp) {
 7         consolog.log('跳轉到' + url)
 8     }).catch(function(error) {
 9         console.log(error)
10     })
11 }
View Code

須要注意的是:這裏的url不是直接的靜態頁面形式,好比你要訪問 /pages/admin/index.html ,這裏的url就可寫爲:/idevtools/pages/admin/index;

2)後端設置相應的 AdminHtmlController.java 來處理這個請求;
  • 由於前端請求時攜帶了headers {'token': token},然後端在作請求轉發時會共用前一次請求的request和response;
  • 因此在攔截器中能夠獲取到 token,進而正確跳轉;(通過驗證:請求確實轉發了,但前端頁面沒跳轉,在我看來,要在後端作靜態頁面的跳轉,仍是須要重定向;固然若是你用的是jsp,確實能夠用請求轉發來作頁面跳轉,由於jsp最終會編譯成Servlet)
1 @Controller
2 @RequestMapping("/pages/admin")
3 public class AdminHtmlController {
4     @GetMapping("/index")
5     public String adminIndex() throws Exception {
6         System.out.println("請求收到");
7         return "forward:/pages/admin/index.html";
8     }
9 }
View Code
關於SpringMVC視圖解析器的請求轉發和重定向,能夠參考: SpringMVC系列(九)自定義視圖、重定向、轉發

3)前端 admin.js 中替換頁面跳轉的方法,將:window.location.href = "/idevtools/pages/admin/index.html" 改成 redirect('/idevtools/pages/admin/index')

4)在測試時出現了:StackOverflowError,緣由我以前的博客已經分析過了: SSM 返回靜態頁面HTML Controller 被遞歸調用引發的StackOverflowError
 
5)修改代碼,將請求地址改成: redirect('/idevtools/pages/admin/aindex'), AdminHtmlController.java 中的 adminIndex的@GetMapping改成 @GetMapping("/aindex"), 繼續測試,發現沒有遞歸調用,可是前端也沒有跳轉到 index.html 頁面;

因此,得出結論:要在後端作靜態頁面的跳轉,仍是須要重定向;固然若是你用的是jsp,確實能夠用請求轉發來作頁面跳轉,由於jsp最終會編譯成Servlet;

此外,即便上述的操做均可行,方案3.2 也不是一個好的設計。由於:
  • 1)攔截 /pages/admin/** 下的全部靜態頁面,意味着管理員模塊的前端在進行頁面跳轉時都須要寫專門的請求,而不能直接寫靜態頁面的跳轉;
  • 2)進而在 AdminHtmlController.java 中須要對專門的請求進行響應,這樣,前端 /pages/admin/** 下有多少個頁面須要跳轉,後端 AdminHtmlController 中就須要寫多少個 @GetMapping("/xxx") 來響應;這是一種很糟糕的設計;

若是某件事的解決方案很複雜,那就該反思是否是哪裏出問題了,畢竟「簡潔是智慧的靈魂,冗長是膚淺的藻飾

3.3 放棄後端對/pages/admin/** 下靜態頁面的攔截,在前端作登陸檢測和跳轉

注:後端 JWTAdminInterceptor 依舊會對管理員的操做進行攔截,若是管理員未登陸,則跳轉到登陸頁面;差異是不對 /pages/admin/** 下的靜態頁面進行攔截,普通用戶有可能在不登陸的狀況下訪問到管理員模塊的相關頁面;

1)取消 spring-mvc.xml 中關於 /pages/admin/** 的攔截;

 1 <!-- 攔截器配置 southday 2019.02.26 -->
 2 <mvc:interceptors>
 3     <!-- JWT 身份驗證攔截器,針對管理員須要先進行登錄後才能操做的請求進行攔截 -->
 4     <mvc:interceptor>
 5         <mvc:mapping path="/a/**"/>
 6         <mvc:exclude-mapping path="/a/login"/>
 7         <mvc:exclude-mapping path="/a/adminInfo"/>
 8         <bean class="cn.idevtools.interceptor.JWTAdminInterceptor"/>
 9     </mvc:interceptor>
10 </mvc:interceptors>
View Code

2)admin.js 中依舊用 window.location.href = "/idevtools/pages/admin/index.html" 來作頁面跳轉;

3)另外建立 admin-index.js,在 index.html 中引用該js文件,實現每次加載首頁時都會向後端請求管理員信息,若是未獲取到,說明管理員未登陸,則跳轉到登陸頁面;

admin-index.js 以下:

 1 $(function() {
 2     axios({
 3         method: 'get',
 4         url: cookurl('/idevtools/a/adminInfo'),
 5         headers: {'token': getAdminToken()}
 6     }).then(function(resp) {
 7         let ret = resp.data
 8         if (ret.code == 'SUCCESS') {
 9             saveAdmin(ret.data)
10         } else {
11             console.log(ret)
12             window.location.href = "/idevtools/pages/admin/login.html"
13         }
14     }).catch(function(error) {
15         console.log(error)
16     })
17 })
View Code

index.html 以下:

 1 <!DOCTYPE html>
 2 <html lang="zh-CN">
 3 <head>
 4     <meta charset="utf-8">
 5     <title>IDevTools</title>
 6 </head>
 7 <body>
 8 welcome to admin index
 9 
10 <script src="../../js/jquery-3.3.1.min.js"></script>
11 <script src="../../js/bootstrap.min.js"></script>
12 <script src="../../js/axios.min.js"></script>
13 <script src="../../js/vue.min.js"></script>
14 <script src="../../js/toastr.min.js"></script>
15 <!-- my js -->
16 <script src="../../js/my/common.js"></script>
17 <script src="../../js/my/admin/admin-index.js"></script>
18 </body>
19 </html>
View Code

4)通過測試,能夠實現我想要的效果:

如今主要來看看:普通用戶在什麼狀況下能夠訪問到管理員模塊的相關頁面

從方案3.3的實現中能夠發現,進行登陸檢測和頁面跳轉控制是在前端 admin-index.js 中實現的,那麼咱們只要設置瀏覽器禁用js,就能夠不執行這段js代碼,直接訪問到 index.html 頁面;

Firefox 中禁用 js 設置以下:

一塊兒來看看是否是如咱們預測的同樣:

通過驗證,發現普通用戶確實在某些狀況下能夠直接訪問到管理員頁面。上面只演示了瀏覽器禁用js的方式來繞過檢測,固然可能還存在其餘方式;

其實我以爲這種狀況是不該該出現的,因此我纔會想攔截 /pages/admin/** 下全部靜態頁面的訪問,但我一時間沒找到好的解決方法,就只想到了方案3.3這種不太完美的方法。不過好在:
  • 1)普通用戶不會平白無故去禁用js,因此大部分的擔憂其實沒必要要的;
  • 2)即便攻擊者繞過了前端的檢測進入了管理員的頁面,後端 JWTAdminInterceptor 依舊會對管理員的相關操做進行攔截,若是發現沒有登陸,一樣會跳轉到登陸頁面;
  • 3)差異無非是攻擊者能夠獲取到管理員模塊的靜態頁面代碼;

因此在進行Web開發時,重要的操做在後端都要作驗證,不能期望前端來作驗證,前端的校驗只是爲了方便大多數用戶。不能圖一時方便,讓攻擊者有可乘之機。

回到目錄

4 其餘相關代碼

common.js 部分代碼以下:

 1 /** 通用 js
 2  * @author southday
 3  * @date 2019.02.27
 4  * @version v0.1
 5  */
 6 
 7 /** url更改器 southday 2019.03.01
 8  * 1) 前端單獨開發,測試時,url前面須要加http://localhost:8080
 9  * 2) 集成到java web項目中時,url前面不用加http://localhost:8080
10  * 該方法是爲了方便以上兩種狀況的相互轉換,真正部署時,要取消該方法的調用
11  */
12 function cookurl(url) {
13     // return url;
14     return 'http://localhost:8080' + url;
15 }
16 
17 /** 更換驗證碼 */
18 function changeVerifyCode() {
19     return cookurl('/idevtools/jcaptcha.jpg?r=' + (Math.random()))
20 }
21 
22 /** code = VALID_ERROR,表單驗證失敗,提示消息
23  * southday 2019.03.01
24  */
25 function showValidMsgs(validMsgs) {
26     for (i = 0, len = validMsgs.length; i < len; i++)
27         toastr.warning(validMsgs[i].errorMsg)
28 }
29 
30 /**
31  * 從localStorage中獲取adminToken
32  * southday 2019.05.17
33  * @returns {string}
34  */
35 function getAdminToken() {
36     return localStorage.getItem("adminToken")
37 }
38 
39 /**
40  * 將adminToken保存到localStorage中
41  * @param token
42  */
43 function saveAdminToken(token) {
44     localStorage.setItem("adminToken", token)
45 }
46 
47 /**
48  * 將admin保存到localStorage
49  * southday 2019.05.17
50  * @param admin
51  */
52 function saveAdmin(admin) {
53     localStorage.setItem("admin", ($.isEmptyObject(admin) ? null : JSON.stringify(admin)))
54 }
55 
56 /**
57  * 從localStorage中取user
58  * southday 2019.05.17
59  * @returns {admin}
60  */
61 function getAdmin() {
62     let a = localStorage.getItem("admin")
63     return $.isEmptyObject(a) ? null : JSON.parse(a)
64 }
View Code

轉載請說明出處,have a good time! :D

相關文章
相關標籤/搜索