前言
最近在写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>
然后前端随便写了下,差不多长这样
根据接口回调,他会访问后端设置的回调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×tamp=".$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有了新的理解......明天上班再思考了。
后记
以上是我开发的思路和历程。通过这两个,让自己的个人博客也有那种扫码登录的功能。还不错吧。