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


目錄

1 前言

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

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

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

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

 

回到目錄

2 我的實現方式與存在的問題

1)后端定義 JWTAdminInterceptor.java 來驗證登錄狀態,如果未登錄則重定向到 /pages/admin/login.html 頁面。代碼如下:

 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,就會被攔截,然后重定向到管理員登錄頁面;

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

 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

存在的問題:

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

3 我想到的解決方案

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

很遺憾,我找了好多資料,目前發現並不能實現這樣的效果;一般用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM