
理解OAuth2.0 授权模式以及PHP实现
在该链接中”OAuth2.0授权协议详细版“,阮一峰老师的文章中已经写得比较详细了,
OAuth授权协议主要为了限制其他网站对自己资源服务器的调用,做到可控性,特定网站只开放特定的权限,所以以前是C<->S,现在加入了一层Auth认证层,C<->Auth<->S。
以下是文章的授权模式的简要流程,放上一张另外一个博客比较经典的豆瓣调用QQ授权服务器的通信时序图。
(A)用户豆瓣,点击qq登陆,豆瓣让用户浏览器携带豆瓣的回调地址去访问qq的授权页面。
(B)用户成功登陆后,认证页面询问授予哪些权限给豆瓣,勾选好后,点击登陆,qq认证服务器根据豆瓣的回调地址,携带code参数进行跳转。
(C)用户浏览器跳转到回调页面后,豆瓣系统再携带code对qq认证服务器发起获取token请求。
(D)qq认证服务器确定code有效,然后生成token值[或者放在B步骤的时候生成],返回给豆瓣系统。
(E)之后豆瓣携带token去qq资源服务器获得用户信息。
关于授权模式文字就讲得差不多了,接下来我们就开始一步一步来实现这个认证吧。
1. 新开一个项目文件夹OAuth2,里面放入两个站点文件夹site_1与site_2
文档结构:
–OAuth2
—-site_1
—-site_2
模拟的就是两个不同的站点。
2. site_1中新建一个登陆页site_1/index.html
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>豆瓣</title> </head> <body> <div style="padding: 5px; border:2px solid #000; width:230px;"> <h2>豆瓣登陆</h2> <label>用户名<input type="text"/></label> <label>密码<input type="text"/></label> <br> <a href="#" onclick="login()">QQ登陆</a> <script> // site_2的登录页 var url="http://localhost/sso/OAuth2/site_2/auth.php?callback="; var login=function () { // 回调url地址 var site_1_callback='http://localhost/sso/OAuth2/site_1/callback.php'; // 因为携带的参数是url,将敏感字符 / 编码一下 var decode=encodeURIComponent(site_1_callback); // 重定向到site_2的地址 window.location.href=url+decode; } </script> </div> </body> </html>
预览:
3. site_2中新建一个登录页site_2/auth.php
<?php // 第三方登录页 // 获取回调地址,加入到下面的callback隐藏表单字段中去 $callback=urldecode($_GET['callback']); ?> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>QQ</title> </head> <body> <form action="login.php" method="post" style="padding: 5px; border:2px solid #000; width:230px;"> <h2>QQ登陆</h2> <label>用户名<input name="account" type="text" value="123"/></label> <label>密码<input name="password" type="text" value="123"/></label> <label><input hidden name="callback" value="<?php echo $callback;?>" ></label> <button type="submit" >登陆</button> <br> </form> </body> </html>
预览:
4. site_2中新建一个认证中心site_2/login.php
<?php // redis键名 const REDIS_TABLE='auth:test'; // 获取login.php传来的字段 $acc= $_POST['account']; $password= $_POST['password']; $callback= $_POST['callback']; $errcode=1; //一次性凭证,换取token $code=''; //账号与密码是否正确 if($acc==='123'&&$password==='123'){ $redis=new Redis(); $redis->connect('localhost',6379); //生成code。生产环境中,应对code与token进行对称加密,这里就省略了。code的格式是[4位随机+当前时间戳+4位随机] $code=strGenerator(4).time().strGenerator(4); //我这里就先生成token了,对应上面B步骤,token的格式是[4位随机+当前时间戳+';'+账号+';'+4位随机] $token=strGenerator(4).time().';'.$acc.';'.strGenerator(4); //将code:token的键值对形式放入redis中 $redis->hSet(REDIS_TABLE,$code,$token); $errcode=0; } //拼凑url $callback.='?errcode='.$errcode.'&code='.$code; //携带code请求回调地址,这里为了方便直接用get了 header('HTTP/1.1 302 Found'); header('Location: '.$callback); function strGenerator($len = 0){ $tmp=''; $strlist='zxcvbnmasdfghjklqwertyuiopZXCVBNMASDFGHJKLQWERTYUIOP1234567890'; for ($i=0;$i<$len;$i++) $tmp.=$strlist[mt_rand(0,61)]; return $tmp; }
这里认证完后用户浏览器转到回调地址
5. site_1中新建一个回调地址site_1/callback.php
<?php // 若返回的errcode为0,则说明用户账号与密码匹配成功 if($_GET['errcode']=='0'){ //拿code获取第三方凭证token $authResult=json_decode( file_get_contents('http://localhost/sso/OAuth2/site_2/getToken.php?code='.$_GET['code'])); //因为定义返回的是json,json_deocde后就变成了对象,获取对象中的errocode,判断是否获取token成功 if($authResult->errcode==0){ $result='认证成功'; //获取成功后,我们就能往site_2里面拿我们可以拿的数据了。 $userInfo=json_decode( file_get_contents("http://localhost/sso/OAuth2/site_2/userInfo.php?token={$authResult->token}")); if(isset($userInfo)){ $status=$userInfo->errcode; $userInfo=$userInfo->data; } }else $result='认证失败'; }else{ $result='登陆失败'; } ?> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>豆瓣</title> </head> <body> <div style="padding: 5px; border:2px solid #000; width:230px;"> <?php echo $result;?> <br> <h2>用户信息</h2> <?php if(isset($status)){?> 用户名:<?php echo $userInfo->name;?> 地址:<?php echo $userInfo->addr;?> <?php }?> </div> </body> </html>
预览放在最后,看完下面的获取token与获取资源代码
6. site_2中新建一个获取token地址site_2/getToken.php
<?php const REDIS_TABLE='auth:test'; const REDIS_SET_TABLE='auth:set'; $redis=new Redis(); $redis->connect('localhost',6379); //获取第5中的code $code=$_GET['code']; $data=array( 'errcode'=>1 ); if($token=$redis->hGet(REDIS_TABLE,$code)){ $data['errcode']=0; //若hash中有该键名,返回token键值 $data['token']=$token; //清除该键值对 $redis->hDel(REDIS_TABLE,$code); //将token加入认证通过集合中去 $redis->sAdd(REDIS_SET_TABLE,$token); } echo json_encode($data);
7. site_2中新建一个获取用户信息地址site_2/userInfo.php
<?php //可换成其他方式 const REDIS_SET_TABLE='auth:set'; $redis=new Redis(); $redis->connect('localhost',6379); // 模拟数据库 $mysql=[ '123'=>array( 'name'=>'幺九', 'addr'=>'www.bayaojiu.com' ) ]; $token=$_GET['token']; $data=array('errcode'=>1); if(!empty($token)&&$redis->sIsMember(REDIS_SET_TABLE,$token)){ //分割token串 $arr=explode(';',$token); //查询模拟数据库,返回数据 $data['data']=$mysql[$arr[1]]; } echo json_encode($data);
最后的结果就是这样的:
此时的文档结构是:
生产环境中,往往会加多许多流程,比如加解密,seesion、redis与mysql搭配、审查网关、权限变动之类的。
对称与非对称加密这里有介绍哦,
👉对称加密
参考文档
https://www.cnblogs.com/flashsun/p/7424071.html