使用JWT PHP 使用JWT-Json Web Token 生成Token 实现Token认证

为什么使用JWT?

官网 https://jwt.io/

3.0版本 https://github.com/lcobucci/jwt

安装 composer require lcobucci/jwt

随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。(此话引用:作者:aishenla 链接:https://www.jianshu.com/p/fe67b4bb6f2c)

Nginx实现JWT验证-基于OpenResty实现

介绍
权限认证是接口开发中不可避免的问题,权限认证包括两个方面

  1. 接口需要知道调用的用户是谁
  2. 接口需要知道该用户是否有权限调用

第1个问题偏向于架构,第2个问题更偏向于业务,因此考虑在架构层解决第1个问题,以达到以下目的

  1. 所有请求被保护的接口保证是合法的(已经认证过的用户)
  2. 接口可以从请求头中获取当前用户信息
  3. 每个请求都有uuid用于标识

JWT(JSON Web Token) 目前是应用最广的接口权限方案,具有无状态,跨系统,多语言多平台支持等特点,如果能在网关层实现JWT验证不仅可以避免代码入侵还能为整个后台提供统一的解决方案,目前客户网关使用Nginx,但社区版Nginx中没有JWT模块,自己实现不现实,因此选择OpenResty作为网关层, 据官网介绍,OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。本质上就是一个Nginx+Lua的集成软件.
整体架构如图:

使用JWT PHP 使用JWT-Json Web Token 生成Token 实现Token认证_第1张图片

实现

环境

[root@docker ~]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[root@docker ~]# more /proc/version
Linux version 3.10.0-693.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red Hat 4.
8.5-16) (GCC) ) #1 SMP Tue Aug 22 21:09:27 UTC 2017
安装OpenResty

OpenRestry安装很简单,可以在这里找到不同版本操作系统安装文档,本次使用的环境是CentOS Linux release 7.4

[root@docker ~]# yum install yum-utils
[root@docker ~]# yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
[root@docker ~]# yum install openresty
[root@docker ~]# yum install openresty-resty

系统默认安装在/usr/local/openresty/目录下,版本如下

[root@docker openresty]# cd /usr/local/openresty/bin/
[root@docker bin]# ./openresty -v
nginx version: openresty/1.13.6.2

可以将OpenResty目录加到PATH里,方便使用.

修改nginx.conf文件测试是否安装成功

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8080;
        location / {
            default_type text/html;
            content_by_lua '
                ngx.say("

hello, world

") '; } } } EOF [root@docker conf]# openresty -s reload [root@docker conf]# curl localhost:8080

hello, world

**

安装JWT模块

**
这里使用JWT官方推荐Lua实现库,项目地址为https://github.com/SkyLothar/…,有趣的是,这个项目的介绍是这么写的JWT For The Great Openresty,看来是为OpenResty量身定做.github上有安装教程,但一方面有些第三方库的安装文档没有提及,另一方面有些内容没有用到安装的时候可以跳过,这里将完整安装步骤重新整理了下.

在release页面https://github.com/SkyLothar/…下载项目源码,截止到目前最新版为v0.1.11
下载hmac源码,截止到目前项目还未release,只能下载项目里的源文件https://github.com/jkeys089/l…
在服务器创建目录/usr/local/openresty/nginx/jwt-lua/resty,将第1步压缩包中目录lua-resty-jwt-0.1.11/lib/resty/下的所有lua文件和第2步中的hmac.lua文件拷贝到该目录下,文件列表如下.

[root@docker resty]# pwd
/usr/local/openresty/nginx/jwt-lua/resty
[root@docker resty]# ll
total 60
-rwxr-xr-x. 1 root root 11592 Jul 18 10:40 evp.lua
-rw-r--r--. 1 root root  3796 Jul 18 10:40 hmac.lua
-rwxr-xr-x. 1 root root 27222 Jul 18 10:40 jwt.lua
-rwxr-xr-x. 1 root root 15257 Jul 18 10:40 jwt-validators.lua

修改nginx.conf验证是否生效

tee /usr/local/openresty/nginx/conf/nginx.conf <<-'EOF'
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        default_type text/plain;
        location = / {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
                    ".eyJmb28iOiJiYXIifQ" ..
                    ".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
                local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
                ngx.say(cjson.encode(jwt_obj))
            ';
        }
        location = /sign {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = jwt:sign(
                    "lua-resty-jwt",
                    {
                        header={typ="JWT", alg="HS256"},
                        payload={foo="bar"}
                    }
                )
                ngx.say(jwt_token)
            ';
        }
    }
}
EOF
[root@docker resty]# curl localhost:8080
{"signature":"VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY","reason":"everything is awesome~ :p","valid":true,"raw_header":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9","payload":{"foo":"bar"},"header":{"alg":"HS256","typ":"JWT"},"verified":true,"raw_payload":"eyJmb28iOiJiYXIifQ"}

验证通过,jwt模块安装完毕

自定义验证逻辑

上面jwt模块还无法用于生产环境,有几个问题没解决

jwt token目前是写死在配置文件里,生产需要从header Authorization中获取
验证失败目前返回是200生产需要返回401
需要配置反向代理并且将用户信息放在代理header上
创建文件/usr/local/openresty/nginx/jwt-lua/resty/nginx-jwt.lua

local jwt = require "resty.jwt"
local cjson = require "cjson"
--your secret
local secret = "5pil6aOO5YaN576O5Lmf5q+U5LiN5LiK5bCP6ZuF55qE56yR"

local M = {}

function M.auth(claim_specs)
    -- require Authorization request header
    local auth_header = ngx.var.http_Authorization

    if auth_header == nil then
        ngx.log(ngx.WARN, "No Authorization header")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Authorization: " .. auth_header)

    -- require Bearer token
    local _, _, token = string.find(auth_header, "Bearer%s+(.+)")

    if token == nil then
        ngx.log(ngx.WARN, "Missing token")
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "Token: " .. token)

    local jwt_obj = jwt:verify(secret, token)
    if jwt_obj.verified == false then
        ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason)
        ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end

    ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj))

    -- write the uid variable
    ngx.var.uid = jwt_obj.payload.sub
end

return M
修改配置文件nginx.conf
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_pass http://tomcat;
        }
    }
}

这里后台启动了一台tomcat并设置监听端口为80,tomcat上部署了一个示例的war包,代码逻辑较简单,就是输出所有的header,代码如下:

package asan.demo;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.*;
import javax.servlet.http.*;

public class JWTDemoService extends HttpServlet {
    private static final String CONTENT_TYPE = "text/html; charset=UTF-8";

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    public void service(HttpServletRequest request,
                        HttpServletResponse response) throws ServletException,
                                                             IOException {
        response.setContentType(CONTENT_TYPE);
        PrintWriter out = response.getWriter();
        Enumeration em=request.getHeaderNames();
        while(em.hasMoreElements()){
            String key=(String)em.nextElement();
            String value=(String)request.getHeaders(key).nextElement();
            out.println(String.format("%s ==> %s", key,value));
        }
        out.close();
    }
}

重启OpenResty测试,如果没有指定jwt token信息返回401

[root@docker conf]# curl http://localhost:8080/jwtdemo/service

401 Authorization Required

401 Authorization Required


openresty/1.13.6.2
指定jwt token [root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ' HTTP/1.1 200 Server: openresty/1.13.6.2 Date: Wed, 18 Jul 2018 05:52:13 GMT Content-Type: text/html;charset=UTF-8 Content-Length: 298 Connection: keep-alive uid ==> yaya host ==> tomcat connection ==> close user-agent ==> curl/7.29.0 accept ==> */* authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODkyNzE3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkyODcxN30.W5UXlwKHSrpUAYbfoF-fTBTS9Enm1wsvCKNQm0yLSfQ

从结果上看,后台服务已经获取到uid这个header
至于请求用到jwt token可以从任意平台生成只要保证secret一样即可,根据官网介绍,该库目前支持到jwt生成算法如图:
使用JWT PHP 使用JWT-Json Web Token 生成Token 实现Token认证_第2张图片

uuid生成

为每个请求生成唯一的uuid码可以将网关层上的请求和应用层的请求关联起来,对排查问题,接口统计都非常有用.

创建文件/usr/local/openresty/nginx/jwt-lua/resty/uuid.lua

local M  = {}
local charset = {}  do -- [0-9a-zA-Z]
    for c = 48, 57  do table.insert(charset, string.char(c)) end
    for c = 65, 90  do table.insert(charset, string.char(c)) end
    for c = 97, 122 do table.insert(charset, string.char(c)) end
end
function M.uuid(length)
        local res = ""
        for i = 1, length do
                res = res .. charset[math.random(1, #charset)]
        end
        return res
end
return M
修改配置文件nginx.conf
worker_processes  1;
error_log logs/error.log info;
events {
    worker_connections 1024;
}
http {
    upstream tomcat{
     server localhost:80;
    }
    lua_package_path "/usr/local/openresty/nginx/jwt-lua/?.lua;;";
    server {
        listen 8080;
        set $uid '';
        set $uuid '';
        location / {
            access_by_lua '
            local jwt = require("resty.nginx-jwt")
            jwt.auth()
            local u = require("resty.uuid")
            ngx.var.uuid = u.uuid(64)
        ';
            default_type application/json;
            proxy_set_header uid $uid;
            proxy_set_header uuid $uuid;
            proxy_pass http://tomcat;
        }
    }
}

重启OpenResty,测试

[root@docker conf]# openresty -s reload
[root@docker conf]# curl -i http://localhost:8080/jwtdemo/ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE'
HTTP/1.1 200
Server: openresty/1.13.6.2
Date: Wed, 18 Jul 2018 08:05:45 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 372
Connection: keep-alive

uid ==> yaya
uuid ==> nhak5eLjQZ73yhAyHLTgZnSBeDa8pa1p3pcpBFvJ4Mv1fkY782UgVr8Islheq03l
host ==> tomcat
connection ==> close
user-agent ==> curl/7.29.0
accept ==> */*
authorization ==> Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ5YXlhIiwiaWF0IjoxNTMxODk0MDA3LCJpc3MiOiJ5YXlhIiwic3ViIjoieWF5YSIsImV4cCI6MTUzMTkzMDAwN30.vQvpQpIHCmK5QBgIoRR8jhIGeYlHOMYySIr4gHvoZFE

可以看到,多了一个uuid的请求头

示例

获取token

setIssuer("suspn.com") //发布者
        ->setAudience("suspn.com") //接收者
        ->setId("abc", true) //对当前token设置的标识
        ->setIssuedAt(time()) //token创建时间
        ->setExpiration(time() + 60) //过期时间
        ->setNotBefore(time() + 5) //当前时间在这个时间前,token不能使用
        ->set('uid', 30061); //自定义数据

//设置签名
$builder->sign($signer, $secret);
//获取加密后的token,转为字符串
$token = (string)$builder->getToken();
var_dump($token);

验证token

parse($token);
    //验证token合法性
    if (!$parse->verify($signer, $secret)) {
        invalidToken('Invalid token');
    }

    //验证是否已经过期
    if ($parse->isExpired()) {
        invalidToken('Already expired');
    }

    //获取数据
    var_dump($parse->getClaims());

} catch (Exception $e) {
    //var_dump($e->getMessage());
    invalidToken('Invalid token');
}

function invalidToken($msg) {
    header('HTTP/1.1 403 forbidden');
    exit($msg);
}

你可能感兴趣的:(Api,php)