【JavaWeb】(血泪踩雷史...)Token登录前后端交互及跨域问题

本文详细介绍了在JavaWeb中实现基于Token的登录,并解决前后端交互过程中的跨域问题。通过设置响应头、添加自定义头、处理预检请求以及允许跨域携带cookie,成功实现了登录、存储、携带和移除Token。同时,文章提醒读者注意跨域携带cookie时Chrome的限制,以及Token存储在session中的潜在问题。

hello,我是卷卷毛,我又来啦

在这里插入图片描述

咱们书接上回,上一节我们讨论了一种基于token验证方式的登录方案,文章在这里:

上节的内容,简单些说,就是介绍了一种实现用户登录的方式。

服务端在校验用户的账号密码信息无误后,为用户生成一串token作为口令返回给用户。之后,按照约定,用户在访问后端时将token添加在请求头中,发送给后端。后端根据头部中的token信息校验用户的登录状况。

那么这一节,承接上回,我们要解决这一登录方案的前后端交互问题,焦点在于token的获取,存储,传递和移除,别看这个问题简单,实现起来却是困难重重,博主就在这里疯狂蹚雷(爆哭),于是才有了这篇博客,也是帮看到这篇博客的小伙伴避避雷。
在这里插入图片描述

(碎碎念:虽说token登录的方式可以避开跨域请求中Cookie传递的若干问题,但是在实践中却总是无法绕开这个话题,也许跨域问题是前后端分离开发必攻下的一块高地吧)


对于这个问题,有的小伙伴一听:

哈?这么简单,在请求中添加一个请求头不就好了?

在这里插入图片描述
不过看起来确实是很简单,但是别着急嗷,不妨先来回顾一下我们后台的流程:

上一篇文章中,编写的后台有三个接口以及一个拦截器:

  • /user/login用户登录接口,访问的时候需要传递usernamepassword,只认识账号123456789,密码123456。登录成功会返回用户的token

  • /user/logout用户登出接口,访问的时候回检测用户是否处于登录状态,如果用户处于登录状态则将用户登出。

  • /user/loginStatus用户登录状态,访问会返回此时用户的登录状态

  • AuthorizationFilter登录拦截器,会检测请求头Authorization字段中的token以判断用户的登录状态。

复习完后,让我们写个简答的前端,试着使用123456789,123456登录获取token:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>token传递</title>
	</head>
	<body>
		<button id="login">点击登录</button>
		<button id="logout">点击登出</button>
		<button id="login-status">登录状态</button>
	</body>
</html>

在这里插入图片描述
接着给每个按钮编写对应的点击事件,访问三个接口。这三个接口就包含着对token的重要基本操作

  • 获取
  • 存储
  • 携带
  • 移除

用户登录及token存储

在发送token之前我们要先拿到token呐,所以先为登录按钮编写点击事件,像/user/login发送一个登录请求:

$('#login').click(()=>{
	$.ajax({
		url:'http://localhost/user/login',
		data:{
			username:'123456789',
			password:'123456'
		},
		type:'POST',
		success:(res)=>{
			alert(res);
		}
	})
})

点击发送,可以看到后台的提示信息:
在这里插入图片描述
但是前台并没有弹出窗口,感觉这个请求不太对劲,打开控制台一看:
在这里插入图片描述
在这里插入图片描述

第一个跨域问题:访问控制源

原来是遇到了跨域的问题,提示我们需要在response中设置头Control-Allow-Origin。他表示跨域请求允许的访问源。

看到这个问题,不要惊慌,应为

这个问题经常是出现在小伙伴们前后端分离开发的第一只拦路虎。
这个看似前端的问题,其实是后端的,需要在后端给相应头中加上若干跨域相关字段

由于后面我们还要频繁的设置响应头的信息。我们不妨在后端设置一个跨域拦截器CROSFilter,专门负责对跨域请求进行处理:
在这里插入图片描述

@Order(1)//拦截器排序,跨域拦截最先进行拦截
@WebFilter(filterName="CROSFilter",urlPatterns= {"*.action","*"})
public class CROSFilter implements Filter{

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse rep= (HttpServletResponse)response;
		
		System.out.println("跨域拦截器拦截到请求:" + req.getRequestURI());
		
		//设置允许访问域,这里直接取访问域,表示允许来自这个域的请求
        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));
        
        //执行拦截链中的下一环
		chain.doFilter(request, response);
	}
}

(这里在响应中允许了访问控制源为访问源,相当于谁来都允许。有时候也使用*来表示通配,但是与之后会设置的另一些头会产生冲突,所以建议使用当前这个。)

此时再点击按钮进行访问,后端提示信息:
在这里插入图片描述
前端提示:
在这里插入图片描述

这表示我们克服了跨域请求的第一关,成功得到了token。这时可以选择将返回的token储存在本地,可以以键值对的形式存放在sessionStorage中,需要携带token时也可以从中获取,主要使用到的函数用法如下:

sessionStorage.setItem(key,value);//添加一个元素
sessionStorage.getItem(key);//获取一个元素
sessionStorage.removeItem(key);//移出一对映射

然后我们用sessionStorage改造一下登录方法,将获取到的token存放其中:

$('#login').click(()=>{
	$.ajax({
		url:'http://localhost/user/login',
		data:{
			username:'123456789',
			password:'123456'
		},
		type:'POST',
		success:(res)=>{
			// alert(res);
			sessionStorage.setItem('token',res);
		}
	})
})

(这里是默认成功的,直接将token存储了,实际使用的时候记得检测一下返回的状态呀,登录成功了再存储token)

之后,再次发送请求,在开发者工具中查看获取的token:
在这里插入图片描述
这样就完成了token的获取和存储了
在这里插入图片描述

用户登录信息及token携带

用户登录成功后,前端获取了登录token并将其存储起来,那么接下来的访问就要在请求头中携带上这个token。

当然,用户登录其实也是可以携带token的,这可以帮助后台辨别用户是否存在重复登录的问题。不过咱们为了演示方便,就先考虑这种情况了。

jquery的ajax请求中,添加请求头的方式为:

$.ajax({
	...
	headers:{
		...
	},
	...
})

按照这个格式,在发送任何形式的请求之前,我们需要将sessionStorage中的token取出来,放到请求头Authorization字段中,那么这段代码应该是:

$.ajax({
	...
	headers:{
		Athorization:sessionStorage.getItem('token')
	},
	...
})

现在我们将它添加到获取登录信息的请求说明中:

$('#login-status').click(()=>{
	$.ajax({
		url:'http://localhost/user/loginStatus',
		type:'GET',
		headers:{
			Authorization:sessionStorage.getItem('token')
		},
		success:(res)=>{
			alert(res);
		}
	})
})

好的,当我们满怀欣喜点击登录状态按钮,期待看到已登录的提示时。。

不出所料,意外又发生了:

后端提示,没有拦截到我们传递的token:
在这里插入图片描述
前端提示,诶嘛一大堆看起来就头疼的提示:
在这里插入图片描述

奇怪的现象又出现了!!

跨域请求添加自定义头

当我们打算查看刚才发出的请求是否存在问题时
在这里插入图片描述
在这里插入图片描述
纳尼?竟然发送了两个请求?

那么那个多发出去的preflight类型的请求是啥呢?

其实他是跨域请求的一种预检机制,即在发送真正请求的时候先发送一个预检请求探查一下服务端的行为。

触发这个机制的事件是当请求不是简单请求的时候。

非简单请求的判别如下:

  • 请求类型不是POST,GET,HEAD其中的一种
  • 请求头包含了Accept,Accept-Language,Content-Language,Last-Event-ID之外的字段
  • Content-Type头取了application/x-www-form-urlencoded,multipart/form-data,text/plain之外的值

所以,刚才出现预检请求是因为包含了自定义字段。

那么,返回的错误信息代表了什么意思呢?
在这里插入图片描述

它的意思是我们自定义的请求头Authorization不被允许。解决这个问题,我们需要在响应头中添加Access-Control-Allow-Headers并在其中指定允许使用额外的头。这个工作还是需要后端来实现,我们在刚才的跨域拦截其中加上一条rep.setHeader("Access-Control-Allow-Headers","Authorization");

@Order(1)
@WebFilter(filterName="CROSFilter",urlPatterns= {"*.action","*"})
public class CROSFilter implements Filter{

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse rep= (HttpServletResponse)response;
		
		System.out.println("跨域拦截器拦截到请求:" + req.getRequestURI());

        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));
        //访问控制允许头:Authorization
        rep.setHeader("Access-Control-Allow-Headers","Authorization");
        
		chain.doFilter(request, response);
	}
}

如果想加入更多的头部,可以使用逗号分隔开。这样就可以携带自定义头啦~
在这里插入图片描述

信心满满,添加完成后,重启后端,再用前端进行访问,后端提示:
在这里插入图片描述
前端提示:
在这里插入图片描述
这。。。
在这里插入图片描述
查看访问请求,发现其中并没有携带记录着sessionid的Cookie:
在这里插入图片描述
这个问题形成的原因就是前端在进行跨域访问后端时没有携带cookie,也就没有携带首次访问获取的sessionid,使得服务端误以为前端是首次访问,为其创建了新的session。

也就说,token早就丢失了!!

跨域请求携带cookie信息

为了让我们的请求能够携带cookie信息,我们必须在前后端同时允许跨域请求对cookie携带的允许。

在前端,需要加上:xhrFields:{withCredentials:true}即:

$('#login').click(()=>{
	$.ajax({
		url:'http://localhost/user/login',
		data:{
			username:'123456789',
			password:'123456'
		},
		xhrFields:{
			withCredentials:true
		},
		type:'POST',
		success:(res)=>{
			// alert(res);
			sessionStorage.setItem('token',res);
		}
	})
})

$('#login-status').click(()=>{
	$.ajax({
		url:'http://localhost/user/loginStatus',
		type:'GET',
		headers:{
			Authorization:sessionStorage.getItem('token')
		},
		xhrFields:{
			withCredentials:true
		},
		success:(res)=>{
			alert(res);
		}
	})
})

在后端,需要加上:rep.setHeader("Access-Control-Allow-Credentials", "true");,同时记得处理一下预检请求OPTIONS,即:

@Order(1)
@WebFilter(filterName="CROSFilter",urlPatterns= {"*.action","*"})
public class CROSFilter implements Filter{

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse rep= (HttpServletResponse)response;
		
		
		
		System.out.println("跨域拦截器拦截到请求:" + req.getRequestURI());
		
        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));//允许访问源
        rep.setHeader("Access-Control-Allow-Credentials", "true");//允许携带cookie
        rep.setHeader("Access-Control-Allow-Headers","Authorization");//允许头
        
        if("OPTIONS".equals(req.getMethod())) {//处理预检请求	
        	System.out.println("preflight 请求");
        	return;
        }
        
        System.out.println("session id : " + req.getSession().getId());//打印sessionid
        
		chain.doFilter(request, response);
	}
}

完事后,我们再点击按钮进行访问:
在这里插入图片描述
在这里插入图片描述
另外,如果前端加入了xhrCredentials,后端也都配置好了,但仍然不管用。

首先检查一下各处的拼写,尤其是前端的。

然后如果使用的是Chrome浏览器的话,一定要修改一下配置,它会默认禁止携带cookie!!!!!

  • google打开访问 chrome://flags/#same-site-by-default-cookies 设置disabled,然后重启
    在这里插入图片描述

不过还是有必要提及的是,token存放在session中并不是一个很好的选择。相比来说,我们更倾向于将其存放在数据库或者中间件像Redis中。

但是本文为了方便演示,只好将其存放在session中,但是很显然,这带来了非常多的问题。

所以,真的,博主蹚的雷小伙伴们一定不要在去猜一遍呀。
在这里插入图片描述

用户登出及token清除

最后,我们排除了万难,已经可以完整的传递token了。

临终一脚就是完成用户的登出,相信有了之前的铺垫,用户登出功能将会变得比较容易实现。

直接编写前端代码:

$('#logout').click(()=>{
	$.ajax({
		url:'http://localhost/user/logout',
		xhrFields:{
			withCredentials:true
		},
		type:'GET',
		headers:{
			Authorization:sessionStorage.getItem('token')
		},
		success:(res)=>{
			alert(res);
		}
	})
})

点击执行登出:
在这里插入图片描述


最终代码

前端:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>token传递</title>
	</head>
	<body>
		<button id="login">点击登录</button>
		<button id="logout">点击登出</button>
		<button id="login-status">登录状态</button>
	</body>
</html>
<script src="jquery-3.5.1.js"></script>
$('#login').click(()=>{
	$.ajax({
		url:'http://localhost/user/login',
		data:{
			username:'123456789',
			password:'123456'
		},
		xhrFields:{
			withCredentials:true
		},
		type:'POST',
		success:(res)=>{
			// alert(res);
			sessionStorage.setItem('token',res);
		}
	})
})

$('#logout').click(()=>{
	$.ajax({
		url:'http://localhost/user/logout',
		xhrFields:{
			withCredentials:true
		},
		type:'GET',
		headers:{
			Authorization:sessionStorage.getItem('token')
		},
		success:(res)=>{
			alert(res);
		}
	})
})

$('#login-status').click(()=>{
	$.ajax({
		url:'http://localhost/user/loginStatus',
		type:'GET',
		headers:{
			Authorization:sessionStorage.getItem('token')
		},
		xhrFields:{
			withCredentials:true
		},
		success:(res)=>{
			alert(res);
		}
	})
})

后端跨域请求拦截:

@Order(1)
@WebFilter(filterName="CROSFilter",urlPatterns= {"*.action","*"})
public class CROSFilter implements Filter{

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse rep= (HttpServletResponse)response;
		
		
		
		System.out.println("跨域拦截器拦截到请求:" + req.getRequestURI());
		
        rep.setHeader("Access-Control-Allow-Origin", req.getHeader("origin"));
        rep.setHeader("Access-Control-Allow-Credentials", "true");
        rep.setHeader("Access-Control-Allow-Headers","Authorization");
        
        if("OPTIONS".equals(req.getMethod())) {
        	System.out.println("preflight 请求");
        	
        	return;
        }
        
        System.out.println("session id : " + req.getSession().getId());
        
		chain.doFilter(request, response);
	}
}

后言

这篇文章知识量不少,主要是博主最近若干天的踩雷史,用以记录,希望能够帮助到更多的小伙伴。

限于篇幅,很多问题在文中并没有特别展开,因此后续还会继续整理和跨域相关的知识点。

下面的文章给博主帮了不少忙,也推荐给你们:


系列文章

这个系列是博主有关Javaweb的实践记录,会分析记录一些博主在实践Javaweb过程中遇到的成果、问题、困难、解决方案等。

欢迎关注博主,一起学习交流~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值