1.背景介绍
团队开发的项目,前端基于Bootstrap+AngularJS,后端Spring MVC以RESTful接口给前端调用。开发和部署都是前后端分离。项目简单部署图如下,因为后台同时采用微服务的方式,所以后台不止3个,画图示意。终极方案是采用Docker,在前端和后台调用中间添加一层:API Gateway。
因为考虑到和其他系统集成的可能性,所以在登录这一块使用了Token来做登录,认证服务器负责生成Token和验证Token。因为客户需要提高系统的安全性,需要在登录页添加一个验证码。但是因为项目是基于前后端分离的,所以添加验证码的功能还是有一些不一样。
2.Session解决方案
有经验的开发者第一反应就是之前验证码怎么添加的,现在在这里也是同样的道理,为什么不一样呢?因为前后端分离,系统登录使用的是Token,后台不再设置Session了。后台必须保证当前用户输入的验证码是用户开始请求页面时候的验证码,必须保证验证码的唯一性。举个例子:
A用户看到的验证码是:ABC;B用户看到的验证码是:DEF。后台存储了ABC和DEF这2个验证码,如果不限定A用户输入的验证码是ABC,那么当A用户碰巧输入DEF,然后用户名和密码也是正确的话,A用户也是可以登录系统的。
在早期可以使用Session系统中,后台返回验证码信息同时写入一个session,有一个SessionID的字段和当前这个验证码对应。所以当用户输入用户名、密码和验证码的时候,浏览器自动把存有session信息的cookie发送到服务器,服务器基于Session可以判断当前这个验证码确实是A用户应该要输入的。
缺点:为了考虑到后续和其他系统集成,同时后台部署是多台服务器,采用的API网关。已经使用了Token,如果为了验证码这个功能,引入Session,有点得不偿失。
3.无Session解决方案
不能使用Session,那只能考虑无Session的方案。要同时获取验证码和验证码对应的一个id值。作为前端的我,第一反应是通过AngularJS中的$http
请求去获取。但是后台验证码是直接读取图片返回二进制流格式给到前端,所以不能额外返回一个ID字段。后端开发同事就说,那在Response Header里面返回一个id的字段,和验证码的值相关联起来。到现在听起来一切都很顺利。前端代码如下:
//控制器层代码
$scope.getCaptcha = function () {
loginService.getCaptcha().success(function (response, status, headers) {
$scope.id = headers().id;
if (response.size > 0) {
$scope.verificationImage = window.URL.createObjectURL(response);
} else {
toastr.error("获取验证码失败:" + response.message);
}
}).error(function (response) {
console.log(response);
});
}
//服务层代码
getCaptcha: function () {
return $http({
method: "GET",
responseType: "blob",
url: AppConfig.userServerUrl + "/user/Captcha/request"
});
},
前端AngularJS代码无法获取header头部额外字段,能获取的字段如下:
在*上搜索一番,解决办法是后台需要设置允许前端浏览器能获取header头部里面的字段。后台同事修改之后,response header里面信息如下图所示:
同域和跨越解决办法:How to read response headers in angularjs?
4.IE9下的bug
以为大功告成,然后在IE9浏览器上测试一下,发现无法加载到验证码,而且控制台报错误。折腾半天,发现IE9不支持window.URL.createObjectURL();
,而且AngularJS发送请求加载二进制流文件就报错。
为了支持IE9,目前解决方法是让后台不返回二进制流文件,而是返回base64编码的字符串,这样IE9也是支持的。
5.可选一种方式
和之前同事交流一番,同事提出了一个可选的方案。因为我们在请求验证码的时候有2个内容,一个是验证码id,一个验证码图片。其实验证码id可以在前端使用随机数生成一个,然后前端把这个id传入后台,后台根据这个id,然后加一些特殊字符,拼接之后一个唯一字符,同时生成一个图片,这个唯一字符和这个验证码图片关联起来,然后将图片返回base64编码内容返回到前端。这种可以不需要前端发送Ajax请求,直接在图片上使用ng-src
。
<img ng-src="http://www.example.com?uid=1001cmss" >