小北

这是很长很美的一生.

钉钉OAUTH开发笔记

体验

前言

    最近在写SeveTools,详情看置顶。考虑到框架的特殊性,以及对整体代码安全的考量,所以采用第三方登录,将风险规避到第三方网站,并且方便用户登录无需增加额外的记忆去存储本站的账号密码。

    所以未来OAUTH认证是大势所趋,的确非常方便。一个是通过第三方账号密码登录,然后回调code判断登录。还有一种是扫码登录,更加安全方便。第一种已经写有gitee,github,微博,qq,代码存储在github。

    代码地址:https://github.com/h4ckdepy/depyseve_oauth/

    第二种想写微信扫码登录,但是官方的SDK非常晦涩难懂。而且需要企业认证,公众号扫码登录也尝试过,看文章无数还是做不出来。说到底还是SDK少了,官方不够负责。这次写的是钉钉的扫码登录,记录一下开发过程。

过程

    根据钉钉的文章,能够大致写出前端代码。

<html>
<head>
    <meta content="webkit" name="renderer" />
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>SeveTools</title>
    <link href="/login/common.css" rel="stylesheet"/>
    <link href="/login/css.css" rel="stylesheet" />
    <script src="/login/common.js"></script>
    <script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
        <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/h4ckdepy/depytheme@latest/depy/font-awesome.min.css">
</head>

<body>
    <div class="logon">
        <div class="logon-logo"><a href="javascript:;" target="_blank" title="depy"></a></div>
        <div class="logon-frame logon-nopadding">
            <div class="logon-sort">
                <a href="javascript:;" class="active">钉钉扫码登录</a>
                <a href="javascript:;">第三方账号登录</a>
            </div>

            <div class="qr-code">
				<div id="login_container"></div>
                <p><span class="qr-ico"></span>本站采用钉钉认证登录</p><p>若您未绑定账号,将返回OpenID</p>
            </div>


            <div class="form-group logon-box-inner" style="display:none;">
                <form class="logon-form">
                	<a onclick="window.location.href='/index/giteelogin'" style="font-size:30px;margin-left:20px">
    					  <i class="fa fa-git-square"></i>
					 </a>	 
                    <div class="free-registration"><span>还没有账号?<a href="/" id="ReturnRegist">立即申请</a></span></div>
                </form>
            </div>
        </div>
    </div>

    <script>

        $('[data-checkbox]').iCheck({
            checkboxClass: 'icheckbox_square-green',
            radioClass: 'iradio_square-green',
            increaseArea: '20%'
        });

        $(".logon-sort a").click(function () {
            $(this).parent("div").find("a").removeClass("active");
            $(this).addClass("active");
            if($(this).index()==0)
            {
                $(".qr-code").show();
                $(".logon-box-inner").hide();
            }
            else if ($(this).index() == 1)
            {
                $(".qr-code").hide();
                $(".logon-box-inner").show();
            }
        });
        $("#ReturnRegist").click(function () {
            $(".logon-sort a:eq(0)").click();
        });

    </script>
   
   <script>
var obj = DDLogin({
     id:"login_container",//这里需要你在自己的页面定义一个HTML标签并设置id,例如<div id="login_container"></div>或<span id="login_container"></span>
     goto: encodeURIComponent('https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoajgkufuf5wmu2fb07&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=https://www.106107.xyz/index/dinglogin'), //请参考注释里的方式
     style: "border:none;background-color:#FFFFFF;",
     width : "365",
     height: "365"
 });
 
 
 var handleMessage = function (event) {
  var origin = event.origin;
  console.log("origin", event.origin);
  if( origin == "https://login.dingtalk.com" ) { //判断是否来自ddLogin扫码事件。
    var loginTmpCode = event.data; 
    window.location.href="https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=dingoajgkufuf5wmu2fb07&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=https://www.106107.xyz/index/dinglogin&loginTmpCode="+loginTmpCode;
    console.log("loginTmpCode", loginTmpCode);
  }
};

if (typeof window.addEventListener != 'undefined') {
    window.addEventListener('message', handleMessage, false);
} else if (typeof window.attachEvent != 'undefined') {
    window.attachEvent('onmessage', handleMessage);
}
 </script>

</body>
</html>

    然后前端随便写了下,差不多长这样

5fe083ff38c4b.png

    根据接口回调,他会访问后端设置的回调uri加上两个参数,code和state。state根据我的理解,是自己设置的校验,避免重放攻击。但是我没有用到。code是临时代码,需要接收到在后端,对接口发送post请求。

大致代码如下:

public function index()    
{    
	$code=$_GET['code'];    
	$state=$_GET['state'];    
    	if($code!=null && $state!=null){    
    	    date_default_timezone_set('Asia/Shanghai');    
    	    $timestamp = $this->getMillisecond();    
    	    $appId="Your APPID";    
       	    $appSecret="YOUR APPSECRET";    
              $s = hash_hmac('sha256',$timestamp, $appSecret, true);    
              $signature = base64_encode($s);    
              $api="https://oapi.dingtalk.com/sns/getuserinfo_bycode?accessKey=$appId&timestamp=".$timestamp."&signature=$signature";    
              $post_data=json_encode(array("tmp_auth_code"=>$code));    
              $json=$this->http_post_json($api, $post_data);    
              $res=json_decode($json,true);    
    	    if($res['user_info']['openid']!=null){    
    		//赋值    
    		try{    
    	   	//登录逻辑    
    		}else{    
            header("Refresh:0");    
    		}    
    	}else{    
    	    exit('Error!');    
    	}    
	}    
	public function http_post_json($url, $jsonStr){    
            $ch = curl_init();    
            curl_setopt($ch, CURLOPT_POST, 1);    
            curl_setopt($ch, CURLOPT_URL, $url);    
            curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonStr);    
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);    
            curl_setopt($ch, CURLOPT_HTTPHEADER, array(    
            'Content-Type: application/json; charset=utf-8',    
            'Content-Length: ' . strlen($jsonStr)    
            )    
            );    
            $response = curl_exec($ch);    
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);    
            curl_close($ch);    
            return $response;    
    }    
	public function getMillisecond(){    
      list($s1, $s2) = explode(' ', microtime());    
      return (float)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000);    
    }

    上面有两个需要注意的点。一个是时间戳时区的问题,即使我设置了上海时区,也会和钉钉服务器相差一分钟导致80032报错。一直无法得到解决,time()函数的时间戳一直无法到签名校验。后来下了一份钉钉的SDK来看,发现他用的是getmillisecond函数,获取毫秒级时间戳。他妈的,官方也不写清楚,害的一个多小时都在搞服务器时间,还以为是php的问题。

    第二也是时间戳的问题。获取tmpcode之后对接口POST,第一次POST过去显示签名校验失败,刷新一次网页才会反馈用户信息(毕竟要获得唯一的鉴权才可以设置后台联动登录),所以这里想了个办法。如果第一次获取的openid为空将会刷新网页(表示没获取到openid),否则就去尝试登录(获取到了)。

但是这里也有个问题,就是一直刷新的问题,因为tmpcode只有一次有效期。但是想了想,扫码出来肯定是有openid的,搞出来就去尝试登录了,除非我直接打开这个接口设置了一个无效的code,导致无法获取openid会一直刷新。会让我的网站对dingding重复大量的POST操作。然后我现在写这篇文章的时候,突然对那个state有了新的理解......明天上班再思考了。

后记

    以上是我开发的思路和历程。通过这两个,让自己的个人博客也有那种扫码登录的功能。还不错吧。

//