前言:
现在人的账号越来越多,大家对新注册账号越来越反感。这时候,当一个公司有多个产品时,打通各个应用壁垒,使用一个统一的登录中心。实现一号全网通,或是只需登录一次的单点登录。都是非常有必要的。今天介绍的就是如何打造一款可以无限接入的登录中心。在这个登录中心中,我选择使用了JWT来做加密。来更大扩展开发边界。让多种语言也可以轻松完成接入
登录时序图

这是一个用户没有登录的情况的图
数据表交代
-
app表
app表
app表主要用来存储业务方应用的回调地址,appkey,appid这个三个重要的参数,appid就是主键id
-
user表
user表
用户表就比较随意了。大家可以根据自己的需要来设计
登录中心部分
首先介绍下登录中心这个产品:
Q:登录中心是做什么的?
A:登录中心是用来做登录的,每个应用都到这里来登录,获取授权。登录成功后会回到访问的应用中
Q:登录中心是如何实现无线扩展且支持多语言的?
A:依靠之前展示过的app表,任何的接入方都需要先注册这个app表。多语言支持是因为使用了JWT来做加解密。这个技术几乎支持市面上所有的语言
好了 Talk is cheap.Show me your code!
- 当用户去浏览第三方的应用的时候,第三方会去检查他的登录情况,如果没有登录,就会重定向到登录中心。并且带上自己的appid和当前用户所访问的页面url
参数名:appid,callback_url
这两个参数实质上是用于校验请求是否合法
有了appid 我们就可以去app表里获得当初注册的回调地址 。我们将注册时回调地址和传递过来的回调地址拿来对比看域名是否一致。
如果appid不存在或回调地址不一致,则视为非法
- 如果校验通过,则需要判断登录状态的密文是否在cookie中存在。
假如一个用户在浏览应用A时做过登录。在登录中心会保存一份他登录的密文。当用户浏览应用B时,他也会被重定向到登录中心,这时,发现他已经登录,则会直接带上密文重定向到应用B。无需再登录,也就是单点登录
- 如果在不存在cookie。则渲染登录页面。引导登录
登录后验证账号密码。账号密码出错自然就结束了。验证通过后,会制作一条包含登录信息的密文,这里使用JWT的依赖包。随后写入登录中心的cookie。在重定向到应用方的注册的回调地址,并带上两个参数
1.code //这个就是我们生成的密文
2.redirect //这个就是用户最初访问的那个地址
/**
* @throws \think\exception\DbException
*/
public function sso()
{
/**
* 1.检查参数是否存在(appid,callback_url)
* 2.根据appid查询app表,callback_url 是否正确
* 3.判断是否有cookie
* 4.如果有cookie
* 5.重定向到callback_url?code=cookie里的密文
* 6.如果没有cookie,渲染登录页
*/
$requestObj = request();
$appId = $requestObj->get('appid');
$callbackUrl = $requestObj->get('callback_url');
if (empty($appId) || empty($callbackUrl)) {
$this->error('参数不能为空');
}
//校验参数格式
$urlArr = parse_url($callbackUrl);
if ($urlArr == false || !is_numeric($appId) || !array_key_exists('host', $urlArr)) {
$this->error('参数错误');
}
//查找应用
$data = App::get($appId);
if (empty($data)) {
$this->error('应用不存在');
}
//取出域名
$Uhost = $urlArr['host'];
$Dhost = parse_url($data['callback_url'])['host'];
//对比域名
if ($Uhost != $Dhost) {
$this->error('回调地址不正确');
}
$authCode = cookie('authCode');
if (empty($authCode)) {
//将回调地址赋值给模板
$this->assign('callback_url',$callbackUrl);
//app表中的回调地址
$this->assign('redirect',$data['callback_url']);
//引导登录,渲染登录页
return $this->fetch();
} else {
//拼接回调地址
$url=BaseService::assembleUrl($data['callback_url'],$callbackUrl,'code',$authCode);
$this->redirect($url);
}
}
在登录中心下有cookie的情况下。会调用个自定义的方法assembleUrl()
这个使用来拼接url的
/**
* 制作用于重定向 业务方的地址
* @param $url
* @param $callBackurl
* @param $key
* @param $value
* @return string
*/
public static function assembleUrl($url,$callBackurl,$key,$value)
{
//有登录记录
if (strpos($url, "?")) {
//包含?
$url = $url . "&$key=" . $value;
} else {
$url = $url . "?$key=" . $value;
}
//最终拼接上用户访问的url
$url.="&redirect={$callBackurl}";
return $url;
}
那么没有cookie的情况下就要引导登录对不对。上面我也提到了,渲染登录页面。这里前端代码我就不贴了。
大概长这个样子

这里有两个小细节,就是我在渲染模板之前,我给模板赋值。将app表里存的回调地址和用户最初访问的url地址都赋值给了模板。目的是为了提交表单的时候把地址一并带过来。方便我们做重定向。
那么用户输入账号密码之后,就会提交到登录方法中。登录方法的思路其实就比较简单了
- 验证账号
- 制作authCode
- 制作重定向地址
- 重定向
/**
/**
* @param $username
* 账号
* @param $password
* 密码
* @param $callback_url
* 用户访问的url
* @param $redirect
* 业务方注册的回调地址
*/
public function login($username,$password,$redirect,$callback_url)
{
if (empty($username)||empty($password)) {
$this->error('账号或密码不能为空');
}
try{
$data['username']=$username;
$data['password']=$password;
$authService=new Auth();
$token= $authService->makeAuthCode($data,$redirect);
//将token写入cookie
cookie('authCode',$token,3600);
//拼装
$url=BaseService::assembleUrl($redirect,$callback_url,'code',$token);
$this->redirect($url);
}catch (Exception $e){
$this->error($e->getMessage());
}
}
下面我们重点来关注下authservice中的makeAuthCode方法
/**
* 利用jwt制作密文
* @param $data
* @param $redirect
* @return \Lcobucci\JWT\Token
* @throws Exception
* @throws \think\exception\DbException
*/
public function makeAuthCode($data,$redirect)
{
$user = Base::checkUser($data);
if (empty($user)) {
throw new Exception('账号或密码错误');
}
//获取该应用秘钥
$app=App::get(['callback_url'=>$redirect]);
if (empty($app)) {
throw new Exception('应用非法');
}
//拿到该应用的秘钥
$key=$app['app_key'];
//制作jwt
$jwtBuilder = new Builder();
//设置加密对象
$signer = new Sha256();
//设置签发者
$jwtBuilder->setIssuer('xx9090950@gmail.com');
//设置签发时间
$jwtBuilder->setIssuedAt(time());
//设置当前时间不能早于设置时间
$jwtBuilder->setNotBefore(time() + 10);
//设置过期时间
$jwtBuilder->setExpiration(time() + 3600);
//设置uid
$jwtBuilder->set('uid', $user['id']);
//设置username
$jwtBuilder->set('name', $user['name']);
//使用算法签名
$jwtBuilder->sign($signer, $key);
//调用获取token方法
$token = $jwtBuilder->getToken();
return $token;
}
这里我使用的是jwt3.0 php的包。里面有对生成token的封装。实际上jwt的加密方式是公开的,你要是理解透彻,自己来做加密也是可以的。网上有很多教程。我就简单解释下jwt的构成
jwt的结构很简单,分为三层
- header层
header层就和http的header作用类似,主要是用于声明,声明自己的json类型,声明自己的加密方法 - payload层
payload里面会包含claim 也就是实体。我的理解就是一些我们需要传递的数据,当然官方有推荐使用的一些字段,比如iss(签发者),exp(过期时间)等等。在加上你需要传递的字段和数据就构成了payload层 - signature层
签名层就是将header和payload 按照header中约定的加密方法加上一个秘钥进行签名。解密方,也同时拥有这个秘钥可以使用同样的方式进行验签
在把这个生成好了的token,保存一份在登录中心的cookie。并且使用之前介绍过的拼接跳转url方法,将token赋值给code做重定向。这样业务方域名下就会在最初注册的回调地址下收到两个get形式的参数
- code(也就是包含用户id,name的jwt)
- redirect (用户最初访问的链接) //一波重定向后还是要让用户回到最初访问的地方
业务方只需对code解密验签,就可以拿到用户的登录信息。将这个code存入cookie。在每次请求的时候都带上这个cookie。就可以拿来做登录验证了。
写好cookie后重定向到用户最初访问的地址就完成了本次登录
好了。关于登录中心的介绍我就先写到这里。其实还有挺多需要完善的地方。比如退出登录。如果要彻底退出,则需要清除登录中心域名下的cookie。比如业务方仅仅需要账号接入,并不希望单点登录的情况。登录中心下就不存储cookie版本。看看有没有时间去写吧。现在写的这是一个demo版本。如果有时间,写完善后我会传到github上面去。
之后我会写一篇,关于接入方。接入登录中心的博客。来详细介绍下。关于接入的细节。谢谢

