1.Token 的用途

在很多计算机系统里面都少不了用户认证这一步骤, 最常见的认证就是账号密码认证, 也就是注册、登录这一流程。

在现实生活中, 人也需要认证, 大家应该都有个 身份证, 回想一下这个身份证是从哪里来的呢? 办过身份证的应该都知道, 一般情况下, 身份证需要本人带着 户口本公安局 (不知道现在改了木有?) 办理, 工作人员在核对了相关信息, 确认无误的情况下会给你颁发一个身份证, 有效期 一般是 10-20 年, 在一些需要认证的时候, 你就可以拿出身份证 校验 核对身份, 比如买火车票, 出国, 或者办理其它证件.

很多 Web 系统里面 token 就类似于身份证, 账号密码就相当于咱的户口本和本人, 需要核对账号密码后获取, 拿到 token 之后就可以使用一些需要认证的服务, 而且 token 也有有效期,和身份证一样, 理论上 token 必须是唯一。

2. 常见的 Web 认证方式

1.HTTP Basic Auth

这种方式在早期一些 Web 系统比较常见,就是那种在浏览器弹出一个框让你输账号密码那种,简单易用,但是缺点一个不安全,其账号密码其实是明文(base64encode)传输的,而且每次都得带上。另外就是太丑了。。。

2.Cookies\Session

这种认证方式其实就是类似我们最开始说的身份证这种,只需要输入一次账号密码,认证成功后,系统会将用户信息存入session,session 是服务器的本地存储功能,然后系统根据 session 生成一个唯一的 sessionid 以 cookies 的形式发送给浏览器。

cookies是浏览器本地存储,在这套机制里面的作用是用来存储 sessionid,你也可以不使用 cookies 存储,早期有些网站在一些不支持 cookies 的浏览器上面会把 sessionid 追加到 url 上面。

cookies 里面存储的 sessionid 其实就是相当于身份证编号,每次访问网站里面我们带着这个编号,服务器拿着编号就可以找到对应的 session 里面存储的信息,一般情况下里面会存储一些用户信息,比如 uid。

讲道理这套机制其实问题并不大,大部分时候都管用,但是 cookies 有一个毛病就是无法跨域,很多大公司有很多网站,这些网站域名可能还不一样。而且 cookies 对现在的手机 APP 支持不好,原生并不支持 cookies。最后,就是服务器存储 session 也需要一些开销,特别是用户特别多的情况下。还有其它缺点这里就不列出来了,很多文章都有写到。

但是其实我想说这套机制大部分情况下是够用的,特别是对于一些中小型网站来说,简单易用,快速开发。

3.JWT

一般说到 JWT 都会提到 token,在我的理解里面 token 其实就是一个字符串,它可以是 jwt token,也可以是 sessionid token,token 就是是一个携带认证信息的字符串。

网上关于介绍 JWT 的文章特别多,大同小异,我们这里也懒的再说一遍了,贴一个大神的教程,我觉得讲的挺清晰了,JSON Web Token 入门教程

简单的说,JWT 本质上是一种解决方案标准,该方案下一个 token 应该有 3 部分组成: Header、Payload、Signature, 其中前 2 部分差不多就是明文的,都是json 对象,里面存了一些信息,使用 base64urlencode 编码成一个字符串。最后的 Signature 是前面 2 个元素和secret一起加密之后的结果, 加密算法默认是 SHA256, 这个secret应该只有服务器知道,解密的时候需要用到。

最后生成的 token 是一个比较长的字符串,当用户登录成功之后可以把这个串返回给浏览器,浏览器下次请求的时候带着这个串就行了,问题来了,怎么带?很多文章说放到 cookies 里面,讲道理放到 cookies 里面那和 sessionid 有啥区别? 标准做法是放到 HTTP 请求的头信息 Authorization 字段里面。

服务器拿到这个串,首先把前面 2 段的 Header 和 Payload 使用 base64urldecode 解码出来,然后使用刚才使用的加密算法和 secret 校验一下是否和第 3 段的 signature 一样,如果不一样,则说明这个 Token 是伪造的,如果一样,就可以相信 Payload 里面的信息了,一般 Payload 里面会存放一些用户信息,比如 uid,如果 Payload 里面需要存放一些敏感信息,比如手机号,建议先加密 Payload。

PHP 实战

下面我将使用 PHP 构建一个简单的例子:

JWT 类:

<?php

namespace App;

class Jwt
{
private $alg = ‘sha256’;

<span class="hljs-keyword">private</span> $secret = <span class="hljs-string">"123456"</span>;

<span class="hljs-comment">/**
 * alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getHeader</span><span class="hljs-params">()</span>
</span>{
    $header = [
        <span class="hljs-string">'alg'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;alg,
        <span class="hljs-string">'typ'</span> =&gt; <span class="hljs-string">'JWT'</span>
    ];

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;base64urlEncode(json_encode($header, JSON_UNESCAPED_UNICODE));
}

<span class="hljs-comment">/**
 * Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用,这里可以存放私有信息,比如uid
 * <span class="hljs-doctag">@param</span> $uid int 用户id
 * <span class="hljs-doctag">@return</span> mixed
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPayload</span><span class="hljs-params">($uid)</span>
</span>{
    $payload = [
        <span class="hljs-string">'iss'</span> =&gt; <span class="hljs-string">'admin'</span>, <span class="hljs-comment">//签发人</span>
        <span class="hljs-string">'exp'</span> =&gt; time() + <span class="hljs-number">600</span>, <span class="hljs-comment">//过期时间</span>
        <span class="hljs-string">'sub'</span> =&gt; <span class="hljs-string">'test'</span>, <span class="hljs-comment">//主题</span>
        <span class="hljs-string">'aud'</span> =&gt; <span class="hljs-string">'every'</span>, <span class="hljs-comment">//受众</span>
        <span class="hljs-string">'nbf'</span> =&gt; time(), <span class="hljs-comment">//生效时间</span>
        <span class="hljs-string">'iat'</span> =&gt; time(), <span class="hljs-comment">//签发时间</span>
        <span class="hljs-string">'jti'</span> =&gt; <span class="hljs-number">10001</span>, <span class="hljs-comment">//编号</span>
        <span class="hljs-string">'uid'</span> =&gt; $uid, <span class="hljs-comment">//私有信息,uid</span>
    ];

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">$this</span>-&gt;base64urlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
}

<span class="hljs-comment">/**
 * 生成token,假设现在payload里面只存一个uid
 * <span class="hljs-doctag">@param</span> $uid int
 * <span class="hljs-doctag">@return</span> string
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">genToken</span><span class="hljs-params">($uid)</span>
</span>{
    $header  = <span class="hljs-keyword">$this</span>-&gt;getHeader();
    $payload = <span class="hljs-keyword">$this</span>-&gt;getPayload($uid);

    $raw   = $header . <span class="hljs-string">'.'</span> . $payload;
    $token = $raw . <span class="hljs-string">'.'</span> . hash_hmac(<span class="hljs-keyword">$this</span>-&gt;alg, $raw, <span class="hljs-keyword">$this</span>-&gt;secret);

    <span class="hljs-keyword">return</span> $token;
}


<span class="hljs-comment">/**
 * 解密校验token,成功的话返回uid
 * <span class="hljs-doctag">@param</span> $token
 * <span class="hljs-doctag">@return</span> mixed
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyToken</span><span class="hljs-params">($token)</span>
</span>{
    <span class="hljs-keyword">if</span> (!$token) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
    $tokenArr = explode(<span class="hljs-string">'.'</span>, $token);
    <span class="hljs-keyword">if</span> (count($tokenArr) != <span class="hljs-number">3</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }
    $header    = $tokenArr[<span class="hljs-number">0</span>];
    $payload   = $tokenArr[<span class="hljs-number">1</span>];
    $signature = $tokenArr[<span class="hljs-number">2</span>];

    $payloadArr = json_decode(<span class="hljs-keyword">$this</span>-&gt;base64urlDecode($payload), <span class="hljs-keyword">true</span>);

    <span class="hljs-keyword">if</span> (!$payloadArr) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }

    <span class="hljs-comment">//已过期</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">isset</span>($payloadArr[<span class="hljs-string">'exp'</span>]) &amp;&amp; $payloadArr[<span class="hljs-string">'exp'</span>] &lt; time()) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }

    $expected = hash_hmac(<span class="hljs-keyword">$this</span>-&gt;alg, $header . <span class="hljs-string">'.'</span> . $payload, <span class="hljs-keyword">$this</span>-&gt;secret);

    <span class="hljs-comment">//签名不对</span>
    <span class="hljs-keyword">if</span> ($expected !== $signature) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
    }

    <span class="hljs-keyword">return</span> $payloadArr[<span class="hljs-string">'uid'</span>];
}

<span class="hljs-comment">/**
 * 安全的base64 url编码
 * <span class="hljs-doctag">@param</span> $data
 * <span class="hljs-doctag">@return</span> string
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">base64urlEncode</span><span class="hljs-params">($data)</span>
</span>{
    <span class="hljs-keyword">return</span> rtrim(strtr(base64_encode($data), <span class="hljs-string">'+/'</span>, <span class="hljs-string">'-_'</span>), <span class="hljs-string">'='</span>);
}

<span class="hljs-comment">/**
 * 安全的base64 url解码
 * <span class="hljs-doctag">@param</span> $data
 * <span class="hljs-doctag">@return</span> bool|string
 */</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">base64urlDecode</span><span class="hljs-params">($data)</span>
</span>{
    <span class="hljs-keyword">return</span> base64_decode(str_pad(strtr($data, <span class="hljs-string">'-_'</span>, <span class="hljs-string">'+/'</span>), strlen($data) % <span class="hljs-number">4</span>, <span class="hljs-string">'='</span>, STR_PAD_RIGHT));
}

}

复制代码

测试:

<?php
$jwt = new \App\Jwt();

// 获取 token
$token = $jwt->genToken(1);

// 解密 token
$uid = $jwt->verifyToken($token);

var_dump($uid);

复制代码

以上代码仅供参考,实际应用的话最好找个现成的库,不推荐重复造轮子,jwt 的思想是通用的,不分语言,github 上面有很多。。。这里贴一个 PHP 的库: firebase/php-jwt

最后再说说 session 和 jwt 的选择问题,网上随便搜搜就可以看到很多文章比较这 2 者优劣,总结就是各有利弊,实际上很多公司既不是 session,也不是 jwt,可能就是自己搞的类似 jwt token 这样的一个字符串,然后放在 cookies 里面,只要这个串能够代表一个用户都可以。

  • php

    PHP(PHP:Hypertext Preprocessor)是一种在电脑上执行的脚本语言,主要是用途在于处理动态网页,也包含了命令列执行接口(command line interface),或者产生图形使用…

    21 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...