前言:
现在人的账号越来越多, 大家对新注册账号越来越反感. 这时候, 当一个公司有多个产品时, 打通各个应用壁垒, 使用一个统一的登录中心. 实现一号全网通, 或是只需登录一次的单点登录. 都是非常有必要的. 今天介绍的就是如何打造一款可以无限接入的登录中心. 在这个登录中心中, 我选择使用了 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 上面去.
之后我会写一篇, 关于接入方. 接入登录中心的博客. 来详细介绍下. 关于接入的细节. 谢谢
来源: http://www.jianshu.com/p/80b3efacfc66