由前端登陆验证,页面跳转,携带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

相关文章
相关标签/搜索