基于jsonp技术的单点登入案例分享二

1、什么是jsonp

说到jsonp这门技术,我们先来了解下javascript中的同源策略。

同源策略
同域名(或ip)、同端口、同协议视为同一个域;一个域内的脚本仅仅具有本域内的权限,可以理解为本域脚本只能读写本域内的资源,而无法访问其它域的资源。这种安全限制称为同源策略。

是不是感觉很懵逼?下面我们用几个问答,来详解下同源策略。

1.1、 怎么辨别同一个域

http://www.baidu.comhttps://www.baidu.com
协议不同,所以不是同一个域
http://127.0.0.1:8080http://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函数调用的方式包装放回给我们的网站域名

这里写图片描述

请看网络请求的分类,不是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单点登录解决方案。首先我觉得这个方案有以下的有点:

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

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值