Jsonp实现跨域请求
链接:https://blog.csdn.net/u011707942/article/details/54848325
1、什么是jsonp
说到jsonp这门技术,我们先来了解下javascript中的同源策略。
同源策略
同域名(或ip)、同端口、同协议视为同一个域;一个域内的脚本仅仅具有本域内的权限,可以理解为本域脚本只能读写本域内的资源,而无法访问其它域的资源。这种安全限制称为同源策略。
是不是感觉很懵逼?下面我们用几个问答,来详解下同源策略。
1.1、 怎么辨别同一个域
http://www.baidu.com 与 https://www.baidu.com
协议不同,所以不是同一个域
http://127.0.0.1:8080 与 http://127.0.0.1:8081
端口不同,所以不是同一个域
http://www.tmall.com与http://www.taobao.com
域名不同,所以不是同一个域
http://blog.csdn.net/column.html与http://blog.csdn.net/experts.html
域名(blog.csdn.net)、端口(80)、协议(http)三个都相同,所有是用一个域。
1.3、怎么理解本域脚本只能读取本域内的资源
我们自己的网站可以读写自己域名的Cookie,可不可以读取其他网站域名的Cookie呢?
我们自己的网站域名可以直接向自己发送ajax请求,可不可以直接向其他网站域名发送ajax请求呢?
如果你代码写的比较少,马上试验一下吧!
下面这段代码,在html页面中我们不知道写过多少遍。 ‘//’开头表示自适应其它网站协议 ;‘/’开头表示从自己网站域名的根目录
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.js" type="application/javascript" charset="utf-8"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="/static/js/common/artDialog/dialog-plus-min.js" type="application/javascript" charset="utf-8"></script>
<script src="/static/js/common/base.js" type="application/javascript" charset="utf-8"></script>
<script src="/static/js/common/ajax.js" type="application/javascript" charset="utf-8"></script>
<link href="/static/js/common/artDialog/dialog.css" type="text/css" rel="stylesheet"/>
<link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
看到这个标题和这段代码,你或许会有这样的疑惑;同源策略不是说只能读取本域的资源,这怎么可以读取bootcss域的资源呢!其实你的疑惑是对的,这是因为浏览器给同源策略开了个“后门”,可以通过这三个html标签从其它域读取资源。而我们的jsonp技术正是利用了这个“后门”才给今天的互联网开发带来了福音。
jsonp简单来说就是:javascript动态的在html页面中生成一个‘script’标签,把我们要发送给其他域名的参数用GET方式带在src的url后面,其他域名把处理好的数据用js函数调用的方式包装放回给我们的网站域名
请看网络请求的分类,不是xhr类型而是js类型;返回的数据像个js函数调用,其实就是函数调用。
2、什么是单点登入
一般公司业务做大之后,都会有很多独立域名的业务系统,随着业务系统数量的增多,系统之间的相互协调变的更加多,用户的一个操作往往都会涉及到几个业务系统。例如:我们在淘宝买东西,加入购物车、提交订单、再跳转到支付宝支付,然后在回到淘宝订单系统。我们肯定只希望登入一次,就可以随意的在这些系统之间来回跳转。
单点登入简单说就是:用户访问一组域名网站,只需要登入一次,就可以随意的访问其他成员网站,直到用户点击退出系统或会话超时。
3、技术实现详解
我们从上面的流程图可以看出,不管访问那个域名网站,后台检查到未登入时,浏览器都会重定向到用户中心的登入页http://www.ssouser.com:8080/login.html?redirectUrl=http://www.domain1.com:8081/
,登入成功后,浏览器又会重定向回原地址redirectUrl。
整个sso单点登入的技术核心就是jsonp,搞定了jsonp就想当于完成了技术实现方案的第一步。会话生命周期的同步,目前我想到了三个解决方案。
方案一:
不同的域名公用一个相同的JSESSIONID的Cookie,这样不管访问哪一个域名,后端做好session共享(Redis),会话生命周期会自动“同步”其他域名。想好是美好的,现实是残酷的。我们自己定义的JSESSIONID无法覆盖tomcat生成的。而浏览器中我们定义的JSESSIONID覆盖了tomcat生成的。其结果就很悲剧了,浏览器Cookie当中存在一个服务器不认可的sessionid。
方案二:
在用户登入(密码与ssoToken两种方式)系统后,保存userid和JESSIONID的映射关系,然后在每次接口调用过程中,依次同步其他域名会话生命周期。这种方案带来的开销是非常大,随着系统流量的增大,对session共享存储会带来很大的压力。
方案三:
浏览器做定时的刷新,依次向其他的域名发送jsonp请求,这样就保证了用户会话生命周期的同步。相对前面两个方案来说,技术实现难度最简单,性能损耗是最小的。
3.1、后端技术详解
springmvc 4.x开始支持jsonp操作了,所有后端稍微做点扩展就OK了。
springmvc扩展核心代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonpSign {}
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;
//只要配置好扫描JsonpAdviceHandler这个类就OK了。
@ControllerAdvice(annotations = JsonpSign.class)
public class JsonpAdviceHandler extends AbstractJsonpResponseBodyAdvice {
public JsonpAdviceHandler(){
super("callback","jsonp","jsonpcallback");
}
}
用户中心核心代码
@RestController
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private RedisRepository redisRepository;
// 密码登入接口,返回ssoToken和需要登入的域名,然后js通过jsop在依次去登入其他的网站
@RequestMapping(value = "/user/login", method = {RequestMethod.POST, RequestMethod.GET})
public ResBody login(int cellphone, String passWd){
if(StringUtils.isEmpty(cellphone) || StringUtils.isEmpty(passWd)){
throw new BizException(SysErrorCode.PARAM_ERROR);
}
String host = SessionUtil.getRequest().getServerName();
int port = SessionUtil.getRequest().getLocalPort();
host = port == 80 ? host : host + ":" + port;
String ssoToken = UUIDUtil.getUuid();
// ssoToken 有效期10秒
redisRepository.set("ssotoken:" + ssoToken, cellphone + "", 10, TimeUnit.SECONDS);
SessionUtil.setSessionUserId(cellphone);
LoginRes res = new LoginRes();
res.setSsoToken(ssoToken);
List<String> domains = DomainGroupConfig.getDomainGroup(host);
domains.remove(host);
res.setSsoUrls(domains);
return ResBody.buildSucResBody(res);
}
// 退出,清空当前session的userid
@RequestMapping(value = "/user/logout", method = RequestMethod.POST)
@AuthSign
public ResBody logout(){
redisRepository.set(SessionUtil.getSessionUserId() + "", "");
return ResBody.buildSucResBody();
}
}
业务域名登入核心代码
//@JsonpSign 目前只对class生效,也就是不支持当个方法支持jsonp操作。
@RestController
@JsonpSign
public class SsoController {
private static final Logger log = LoggerFactory.getLogger(SsoController.class);
@Autowired
private RedisRepository redisRepository;
//业务站点通过jsonp技术结合sso-web-user授权的token登入
//对请求做http head refence校验,防止别人窃取token
@RequestMapping(value = "/sso/login", method = RequestMethod.GET)
public ResBody login(String token){
String cellphone = redisRepository.get("ssotoken:" + token);
if(StringUtils.isEmpty(cellphone)){
throw new BizException(SysErrorCode.PARAM_ERROR);
}
SessionUtil.setSessionUserId(Integer.parseInt(cellphone));
return ResBody.buildSucResBody(cellphone);
}
//用来检测业务站点是否登入
@RequestMapping(value = "/sso/isLogin", method = RequestMethod.GET)
@AuthSign
public ResBody isLogin(){
if(SessionUtil.getSessionUserId() == null){
throw new BizException(SysErrorCode.PERMISSION_EXPIRED);
}
return ResBody.buildSucResBody();
}
//退出登入
@RequestMapping(value = "/sso/logout", method = RequestMethod.POST)
public ResBody logout(){
SessionUtil.cleanSessionInfo();
return ResBody.buildSucResBody();
}
}
3.2、前端技术详解
由于jquery直接支持对jsonp的操作,但是为了更好的处理系统通用的业务操作,我还是对jquery的ajax操作进行了封装。
jsonp请求封装
/**
* 发送jsonp(GET)请求,用url带参的方式编码请求数据,返回JSON数据类型
* @param url 请求的url
* @param req 请求的json对象
* @param bizSuccessCallBack 业务成功回调函数
* @param bizFailCallBack 业务失败回调函数
* @param head Http请求头
*/
function getJsonp(url, req, bizSuccessCallBack, bizFailCallBack, head) {
var _settings = {};
if(req != null || req != undefined){
_settings.data = req;
}
if(head != null || head != undefined){
if(head.async != null || head.async != undefined){
_settings.async = head.async;
}
}
_settings.type = "GET";
_settings.contentType = "application/x-www-form-urlencoded; charset=UTF-8";
_settings.cache = false;
_settings.crossDomain = true;
_settings.dataType = "jsonp";
_settings.scriptCharset = "utf-8";
_settings.jsonp = "jsonpcallback";
_settings.success = function (res) {
if(commonCodeFilter(res)){
return;
}
if(res.code == "common-base-1"){
if(bizSuccessCallBack != null || bizSuccessCallBack != undefined){
bizSuccessCallBack(res.data, res.page);
}
else{
minShow(JSON.stringify(res));
}
}
else{
if(bizFailCallBack != null || bizFailCallBack != undefined){
bizFailCallBack(res.code);
}
else{
minShow(JSON.stringify(res));
}
}
}
_settings.error = function (res) {
alert(JSON.stringify(res));
}
$.ajax(url, _settings);
}
postForm请求封装
/**
* 发送post请求,用application/x-www-form-urlencoded编码请求数据,返回JSON数据类型
* @param url 请求的url
* @param req 请求的json对象
* @param bizSuccessCallBack 业务成功回调函数
* @param bizFailCallBack 业务失败回调函数
* @param head Http请求头
*/
function postForm(url, req, bizSuccessCallBack, bizFailCallBack, head) {
var _settings = {};
if(req != null || req != undefined){
_settings.data = req;
}
if(head != null || head != undefined){
if(head.async != null || head.async != undefined){
_settings.async = head.async;
}
}
_settings.async = true;
_settings.type = "POST";
_settings.contentType = "application/x-www-form-urlencoded; charset=UTF-8";
_settings.dataType = "json"
_settings.success = function (res) {
if(commonCodeFilter(res)){
return;
}
if(res.code == "common-base-1"){
if(bizSuccessCallBack != null || bizSuccessCallBack != undefined){
bizSuccessCallBack(res.data, res.page);
}
else{
minShow(JSON.stringify(res));
}
}
else{
if(bizFailCallBack != null || bizFailCallBack != undefined){
bizFailCallBack(res.code);
}
else{
minShow(JSON.stringify(res));
}
}
}
_settings.error = function (res) {
alert(JSON.stringify(res));
}
$.ajax(url, _settings);
}
登入核心代码
ssoCount = 0;
$('#submit').click(login);
function login() {
postForm("/user/login", {
cellphone: $('#userName').val(),
passWd: $('#password').val()
}, ssoLogin, null, {async: false});
}
function ssoLogin(data) {
for(var i = 0; i < data.ssoUrls.length; i++){
getJsonp("http:\/\/" + data.ssoUrls[i] + "/sso/login", {
token: data.ssoToken
}, function (res2) {
ssoCount ++;
console.log(JSON.stringify(res2));
}, null, {async: false});
}
// 这里的ajax同步执行,我感觉有问题。所以这里加了个while循环等待。
while(ssoCount == data.ssoUrls.length){
break;
}
redictBackUrl();
}
本案例的demo已托管到Github上,我会继续完善该Demo,有兴趣的同学可以去下载
4、总结
最初在给系统做sso单点登入方案时,我和大多数想到的都是用cas方案,可能是我个人学习能力不是很强,看了下cas的资料,久久没有搞明白是怎么回事。所以才找到了用jsonp的方式来做这个sso单点登录解决方案。首先我觉得这个方案有以下的有点:
- 业务系统集成简单
- 使用起来非常简单
当能这个解决方案还是有缺点的,在前面我们已经讲到,目前还没有实现正真的会话生命周期同步。但是我个人觉得,有办法总比没办法好,曲线救国也是救国嘛。
最后我想说的是,不要为了学习技术而学习技术,要学会和已有的技术做结合,创造出新的技术。