欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Jsonp实现跨域请求

程序员文章站 2022-07-10 18:06:16
...

链接: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.comhttp://www.taobao.com 
域名不同,所以不是同一个域 
http://blog.csdn.net/column.htmlhttp://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函数调用的方式包装放回给我们的网站域名

Jsonp实现跨域请求

请看网络请求的分类,不是xhr类型而是js类型;返回的数据像个js函数调用,其实就是函数调用。

2、什么是单点登入

一般公司业务做大之后,都会有很多独立域名的业务系统,随着业务系统数量的增多,系统之间的相互协调变的更加多,用户的一个操作往往都会涉及到几个业务系统。例如:我们在淘宝买东西,加入购物车、提交订单、再跳转到支付宝支付,然后在回到淘宝订单系统。我们肯定只希望登入一次,就可以随意的在这些系统之间来回跳转。

单点登入简单说就是:用户访问一组域名网站,只需要登入一次,就可以随意的访问其他成员网站,直到用户点击退出系统或会话超时。

3、技术实现详解

Jsonp实现跨域请求

我们从上面的流程图可以看出,不管访问那个域名网站,后台检查到未登入时,浏览器都会重定向到用户中心的登入页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单点登录解决方案。首先我觉得这个方案有以下的有点:

  • 业务系统集成简单
  • 使用起来非常简单

当能这个解决方案还是有缺点的,在前面我们已经讲到,目前还没有实现正真的会话生命周期同步。但是我个人觉得,有办法总比没办法好,曲线救国也是救国嘛。

最后我想说的是,不要为了学习技术而学习技术,要学会和已有的技术做结合,创造出新的技术。

相关标签: 跨域