├── README.md ├── auth_req_headers.lua ├── auth_token.lua ├── handlefile ├── handle_cors.lua └── handle_request_provision.lua ├── redis_mcredis.lua └── tool_dns_server.lua /README.md: -------------------------------------------------------------------------------- 1 | token有效验证 2 | --- 3 | # 前言 4 | 5 | token系统,之前应项目需求写了token验证系统。基于nginx+lua+redis,通过redis设置token时效性来控制token的可用性。是一个完整的token系统,包含登录验证、token生成、token时效性控制、token验证、反向代理转发内部服务等功能。 6 | 此文并不打算封装完整的一个系统,因为后面小生在有空没事设计系统玩的时候,希望兼容第三方平台登录时,发现大多第三方都是有自己的一套登录授权与换取openid(或其他唯一的身份id)机制。此时如果多个第三方的登录由于差异性,又要在lua上去兼容和实现的话,显得很繁琐且扩展性不高。 7 | 故,此文只抽取了其中的token验证功能。 8 | 9 | # 环境 10 | 11 | * openresty(自带ngx_lua模块) 12 | * redis 13 | * linux或其他服务主机 14 | 15 | # 一、基本设计思路 16 | 17 | * 前端在请求头设置用户token, 18 | * nginx上通过lua获取token,配合redis验证并转换成系统用户的userid 19 | * 重写请求头再转发请求至内部服务。 20 | 21 | # 二、redis设计 22 | 23 | ## 1.用户ID->Token对应关系 24 | 25 | userid=> token (hash) 26 | ``` 27 | userid_(userid): { 28 | token:(token) 29 | } 30 | ``` 31 | 32 | ## 2.Token->用户ID对应关系 33 | 34 | token=>userid 35 | ``` 36 | token_(token): { 37 | userid:(userid) 38 | } 39 | ``` 40 | 41 | # Nginx+Lua+Redis实现token验证 42 | 43 | ## 1.lua脚本目录结构 44 | 45 | ```shell 46 | lmc:tokenGateway chao$ tree lua/ 47 | lua/ 48 | ├── auth_req_headers.lua # 请求头校验脚本,失败直接中断请求403 49 | ├── auth_token.lua # token处理脚本 50 | ├── handlefile 51 | │ ├── handle_cors.lua # 请求跨域脚本 52 | │ └── handle_request_provision.lua # 请求handle入口脚本 53 | ├── redis_mcredis.lua # redis操作工具封装(基本来自网络佚名前辈们封装,小生只是稍加修改,增加了redis域名解析,感谢前辈们的付出) 54 | └── tool_dns_server.lua # 域名解析,获取域名对应的ip,并设置缓存 55 | 56 | 1 directory, 6 files 57 | ``` 58 | 59 | ## 2.nginx配置 60 | 61 | * nginx.conf 62 | ```shell 63 | worker_processes 1; 64 | error_log logs/error.log info; 65 | 66 | events { 67 | worker_connections 1024; 68 | } 69 | 70 | upstream iotserver{ 71 | server unix:/run/gunicorn/iot.sock fail_timeout=0; 72 | } 73 | 74 | http { 75 | # lua custom path, use absolute path please. 76 | lua_package_path "/etc/nginx/lua/?.lua;;"; 77 | 78 | server { 79 | listen 8080; 80 | access_log logs/access.log; 81 | location @auth_success{ 82 | proxy_set_header Host $http_host; 83 | proxy_pass http://iotserver; 84 | } 85 | 86 | location / { 87 | # CORS 88 | header_filter_by_lua_file /etc/nginx/lua/handlefile/handle_cors.lua; 89 | if ($request_method = 'OPTIONS') { 90 | return 204; 91 | } 92 | 93 | access_by_lua_file /etc/nginx/lua/handlefile/handle_request_provision.lua; 94 | } 95 | } 96 | } 97 | 98 | ``` 99 | 100 | * token验证失败错误码 101 | 102 | | 错误码 | 描述 | 103 | | :----- | :-------- | 104 | | -1 | 系统错误 | 105 | | 10001 | token失效 | -------------------------------------------------------------------------------- /auth_req_headers.lua: -------------------------------------------------------------------------------- 1 | -- auth_req_headers.lua 2 | 3 | local _reqHeader = {} 4 | _reqHeader._VERSION = '1.0.0' 5 | _reqHeader._AUTHOR = 'Chao' 6 | 7 | -- 查询请求头是否合法(登录接口) 8 | function _reqHeader.is_headers_legal_notoken() 9 | local headers = ngx.req.get_headers() 10 | if not headers["language"] then 11 | return false 12 | else 13 | -- 14 | return true 15 | end 16 | return true 17 | end 18 | 19 | -- 查询请求头是否合法 20 | function _reqHeader.is_headers_legal() 21 | local headers = ngx.req.get_headers() 22 | if not headers["language"] then 23 | return false 24 | elseif not headers["token"] then 25 | return false 26 | else 27 | -- 28 | return true 29 | end 30 | return true 31 | end 32 | -- 验证此次请求头合法性 33 | -- 返回block结构休 34 | --[[ 35 | _result = { 36 | status = 0, 37 | msg = "", 38 | data = {} 39 | } 40 | status说明: 41 | 0:默认值 42 | 101:没有该账号 43 | 102:账号与密码错误 44 | --]] 45 | function _reqHeader.check_req_headers(self, is_has_token) 46 | -- 优先验证是请求头合法性 47 | if is_has_token then 48 | if not self:is_headers_legal() then 49 | return false 50 | end 51 | else 52 | if not self:is_headers_legal_notoken() then 53 | return false 54 | end 55 | end 56 | return true 57 | end 58 | 59 | function _reqHeader.new(self, login_entity) 60 | local login_entity = login_entity or {} 61 | setmetatable(login_entity, self) 62 | self.__index = self 63 | return login_entity 64 | end 65 | 66 | return _reqHeader -------------------------------------------------------------------------------- /auth_token.lua: -------------------------------------------------------------------------------- 1 | -- auth_token.lua 2 | -- 请求验证模块 3 | local cjson = require "cjson" 4 | local redis = require "redis_mcredis" 5 | local red = redis:new() 6 | 7 | local _reqnManager = {} 8 | _reqnManager._VERSION = '1.0.0' 9 | _reqnManager._AUTHOR = 'Chao' 10 | 11 | -- 判断Redis值是否为空 12 | local function is_redis_null( res ) 13 | if type(res) == "table" then 14 | for k,v in pairs(res) do 15 | if v ~= ngx.null then 16 | return false 17 | end 18 | end 19 | return true 20 | elseif res == ngx.null then 21 | return true 22 | elseif res == nil then 23 | return true 24 | end 25 | return false 26 | end 27 | 28 | -- 验证Auth Token合法性并返回User ID 29 | function _reqnManager.check_auth_token(self, requst_headers) 30 | local user_id, err = red:hget("token_"..requst_headers["token"], "userid") 31 | if is_redis_null(user_id) then 32 | ngx.log(ngx.INFO, string.format("Token:%s, 找不到对应UserID", requst_headers["token"])) 33 | return nil 34 | end 35 | return user_id 36 | end 37 | 38 | -- 验证入口,验证请求合法性 39 | -- 返回block结构休 40 | --[[ 41 | _result = { 42 | code = 0, 43 | msg = "", 44 | data = {} 45 | } 46 | code说明: 47 | 0:默认值 48 | 10001:token过期 49 | --]] 50 | function _reqnManager.check_requst_validity(self) 51 | local _result = { 52 | code = 0, 53 | msg = "", 54 | data = {} 55 | } 56 | local headers = ngx.req.get_headers() 57 | ngx.log(ngx.INFO, string.format("请求接口传入headers :%s\n", cjson.encode(headers))) 58 | -- 验证Token合法性并返回User ID 59 | local user_id = self:check_auth_token(headers) 60 | if not user_id then 61 | _result.code = 10001 62 | _result.msg = "token has been invalid" 63 | return _result, nil 64 | end 65 | ngx.req.set_header("userid", user_id); 66 | return _result, nil 67 | end 68 | 69 | 70 | 71 | function _reqnManager.new(self, req_entity) 72 | local req_entity = req_entity or {} 73 | setmetatable(req_entity, self) 74 | self.__index = self 75 | return req_entity 76 | end 77 | 78 | return _reqnManager -------------------------------------------------------------------------------- /handlefile/handle_cors.lua: -------------------------------------------------------------------------------- 1 | ngx.header["Access-Control-Allow-Origin"] = "*" 2 | ngx.header["Access-Control-Allow-Headers"] = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,language,token" 3 | 4 | if ngx.var.request_method == "OPTIONS" then 5 | ngx.header["Access-Control-Max-Age"] = "1728000" 6 | ngx.header["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE" 7 | ngx.header["Content-Length"] = "0" 8 | ngx.header["Content-Type"] = "text/plain, charset=utf-8" 9 | end -------------------------------------------------------------------------------- /handlefile/handle_request_provision.lua: -------------------------------------------------------------------------------- 1 | local cjson = require "cjson" 2 | -- 检查是否合法的请求头 3 | local auth_req_headers = require "auth_req_headers" 4 | local headerManager = auth_req_headers:new() 5 | 6 | local is_has_token = true 7 | if ngx.re.match(ngx.var.uri,"^(/iotapi/login).*$|(/iotapi/userauth).*$") then 8 | is_has_token = false 9 | end 10 | 11 | local headerResult = headerManager:check_req_headers(is_has_token) 12 | if not headerResult then 13 | ngx.log(ngx.INFO, "请求头不合法") 14 | ngx.exit(403) 15 | end 16 | 17 | if not is_has_token then 18 | -- no token request, exec @auth_success 19 | return ngx.exec("@auth_success") 20 | end 21 | 22 | -- 令牌验证 23 | local auth_token = require "auth_token" 24 | local tokenManager = auth_token:new() 25 | local checkResult, err = tokenManager:check_requst_validity() 26 | 27 | -- 处理验证结果 28 | local switch = { 29 | [10001] = function(response) 30 | -- Request Error The Token Is Timeout 31 | ngx.say(cjson.encode(response)) 32 | end 33 | } 34 | local s_case = switch[checkResult["code"]] 35 | if (s_case) then 36 | s_case(checkResult) 37 | else 38 | -- for case default, success 39 | return ngx.exec("@auth_success") 40 | end -------------------------------------------------------------------------------- /redis_mcredis.lua: -------------------------------------------------------------------------------- 1 | local redis_c = require "resty.redis" 2 | local dns_server_c = require "tool_dns_server" 3 | 4 | local _redisHost = "127.0.0.1" 5 | local _redisAddr = "127.0.0.1" 6 | local _redisPort = 6379 7 | local _redisPw = nil 8 | 9 | -- local _redisHost = "xxxxxxxx.redis.rds.aliyuncs.com" 10 | -- local _redisAddr = "127.0.0.1" 11 | -- local _redisPort = 6379 12 | -- local _redisPw = "pwd" 13 | 14 | local ok, new_tab = pcall(require, "table.new") 15 | if not ok or type(new_tab) ~= "function" then 16 | new_tab = function (narr, nrec) return {} end 17 | end 18 | 19 | 20 | local _M = new_tab(0, 155) 21 | _M._VERSION = '1.0.0' 22 | _M._AUTHOR = 'Chao' 23 | 24 | 25 | local commands = { 26 | "append", "auth", "bgrewriteaof", 27 | "bgsave", "bitcount", "bitop", 28 | "blpop", "brpop", 29 | "brpoplpush", "client", "config", 30 | "dbsize", 31 | "debug", "decr", "decrby", 32 | "del", "discard", "dump", 33 | "echo", 34 | "eval", "exec", "exists", 35 | "expire", "expireat", "flushall", 36 | "flushdb", "get", "getbit", 37 | "getrange", "getset", "hdel", 38 | "hexists", "hget", "hgetall", 39 | "hincrby", "hincrbyfloat", "hkeys", 40 | "hlen", 41 | "hmget", "hmset", "hscan", 42 | "hset", 43 | "hsetnx", "hvals", "incr", 44 | "incrby", "incrbyfloat", "info", 45 | "keys", 46 | "lastsave", "lindex", "linsert", 47 | "llen", "lpop", "lpush", 48 | "lpushx", "lrange", "lrem", 49 | "lset", "ltrim", "mget", 50 | "migrate", 51 | "monitor", "move", "mset", 52 | "msetnx", "multi", "object", 53 | "persist", "pexpire", "pexpireat", 54 | "ping", "psetex", "psubscribe", 55 | "pttl", 56 | "publish", --[[ "punsubscribe", ]] "pubsub", 57 | "quit", 58 | "randomkey", "rename", "renamenx", 59 | "restore", 60 | "rpop", "rpoplpush", "rpush", 61 | "rpushx", "sadd", "save", 62 | "scan", "scard", "script", 63 | "sdiff", "sdiffstore", 64 | "select", "set", "setbit", 65 | "setex", "setnx", "setrange", 66 | "shutdown", "sinter", "sinterstore", 67 | "sismember", "slaveof", "slowlog", 68 | "smembers", "smove", "sort", 69 | "spop", "srandmember", "srem", 70 | "sscan", 71 | "strlen", --[[ "subscribe", ]] "sunion", 72 | "sunionstore", "sync", "time", 73 | "ttl", 74 | "type", --[[ "unsubscribe", ]] "unwatch", 75 | "watch", "zadd", "zcard", 76 | "zcount", "zincrby", "zinterstore", 77 | "zrange", "zrangebyscore", "zrank", 78 | "zrem", "zremrangebyrank", "zremrangebyscore", 79 | "zrevrange", "zrevrangebyscore", "zrevrank", 80 | "zscan", 81 | "zscore", "zunionstore", "evalsha" 82 | } 83 | 84 | 85 | local mt = { __index = _M } 86 | 87 | 88 | local function is_redis_null( res ) 89 | if type(res) == "table" then 90 | for k,v in pairs(res) do 91 | if v ~= ngx.null then 92 | return false 93 | end 94 | end 95 | return true 96 | elseif res == ngx.null then 97 | return true 98 | elseif res == nil then 99 | return true 100 | end 101 | 102 | return false 103 | end 104 | 105 | 106 | -- change connect address as you need 107 | function _M.connect_mod( self, redis ) 108 | -- get redis addr 109 | local dns_manager = dns_server_c:new() 110 | local new_addr, new_hostname = dns_manager:get_addr(_redisHost) 111 | if not new_addr then 112 | new_addr = _redisAddr 113 | end 114 | ngx.log(ngx.INFO, string.format("Redis IP:%s", new_addr)) 115 | redis:set_timeout(self.timeout) 116 | return redis:connect(new_addr, _redisPort) 117 | end 118 | 119 | 120 | function _M.set_keepalive_mod( redis ) 121 | -- put it into the connection pool of size 100, with 60 seconds max idle time 122 | return redis:set_keepalive(60000, 1000) 123 | end 124 | 125 | 126 | function _M.init_pipeline( self ) 127 | self._reqs = {} 128 | end 129 | 130 | 131 | function _M.commit_pipeline( self ) 132 | local reqs = self._reqs 133 | 134 | if nil == reqs or 0 == #reqs then 135 | return {}, "no pipeline" 136 | else 137 | self._reqs = nil 138 | end 139 | 140 | local redis, err = redis_c:new() 141 | if not redis then 142 | return nil, err 143 | end 144 | 145 | local ok, err = self:connect_mod(redis) 146 | if not ok then 147 | ngx.log(ngx.ERR, "failed to connect Redis") 148 | return {}, err 149 | end 150 | -- auth 验证redis密码 151 | if _redisPw then 152 | local count, err = redis:get_reused_times() 153 | if 0 == count then 154 | ok, err = redis:auth(_redisPw) 155 | if not ok then 156 | ngx.log(ngx.ERR, "failed to auth Redis") 157 | return nil, err 158 | end 159 | elseif err then 160 | ngx.log(ngx.INFO, "failed to get reused times") 161 | return nil, err 162 | end 163 | end 164 | 165 | redis:init_pipeline() 166 | for _, vals in ipairs(reqs) do 167 | local fun = redis[vals[1]] 168 | table.remove(vals , 1) 169 | 170 | fun(redis, unpack(vals)) 171 | end 172 | 173 | local results, err = redis:commit_pipeline() 174 | if not results or err then 175 | return {}, err 176 | end 177 | 178 | if is_redis_null(results) then 179 | results = {} 180 | ngx.log(ngx.WARN, "is null") 181 | end 182 | -- table.remove (results , 1) 183 | 184 | self.set_keepalive_mod(redis) 185 | 186 | for i,value in ipairs(results) do 187 | if is_redis_null(value) then 188 | results[i] = nil 189 | end 190 | end 191 | 192 | return results, err 193 | end 194 | 195 | 196 | function _M.subscribe( self, channel ) 197 | local redis, err = redis_c:new() 198 | if not redis then 199 | return nil, err 200 | end 201 | 202 | local ok, err = self:connect_mod(redis) 203 | if not ok or err then 204 | ngx.log(ngx.ERR, "failed to connect Redis") 205 | return nil, err 206 | end 207 | -- auth 验证redis密码 208 | if _redisPw then 209 | local count, err = redis:get_reused_times() 210 | if 0 == count then 211 | ok, err = redis:auth(_redisPw) 212 | if not ok then 213 | ngx.log(ngx.ERR, "failed to auth Redis") 214 | return nil, err 215 | end 216 | elseif err then 217 | ngx.log(ngx.INFO, "failed to get reused times") 218 | return nil, err 219 | end 220 | end 221 | 222 | local res, err = redis:subscribe(channel) 223 | if not res then 224 | return nil, err 225 | end 226 | 227 | local function do_read_func ( do_read ) 228 | if do_read == nil or do_read == true then 229 | res, err = redis:read_reply() 230 | if not res then 231 | return nil, err 232 | end 233 | return res 234 | end 235 | 236 | redis:unsubscribe(channel) 237 | self.set_keepalive_mod(redis) 238 | return 239 | end 240 | 241 | return do_read_func 242 | end 243 | 244 | 245 | local function do_command(self, cmd, ... ) 246 | if self._reqs then 247 | table.insert(self._reqs, {cmd, ...}) 248 | return 249 | end 250 | 251 | local redis, err = redis_c:new() 252 | if not redis then 253 | return nil, err 254 | end 255 | 256 | local ok, err = self:connect_mod(redis) 257 | if not ok or err then 258 | ngx.log(ngx.ERR, "failed to connect Redis") 259 | return nil, err 260 | end 261 | -- auth 验证redis密码 262 | if _redisPw then 263 | local count, err = redis:get_reused_times() 264 | if 0 == count then 265 | ok, err = redis:auth(_redisPw) 266 | if not ok then 267 | ngx.log(ngx.ERR, "failed to auth Redis") 268 | return nil, err 269 | end 270 | elseif err then 271 | ngx.log(ngx.INFO, "failed to get reused times") 272 | return nil, err 273 | end 274 | end 275 | 276 | local fun = redis[cmd] 277 | local result, err = fun(redis, ...) 278 | if not result or err then 279 | -- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err) 280 | return nil, err 281 | end 282 | 283 | if is_redis_null(result) then 284 | result = nil 285 | end 286 | 287 | self.set_keepalive_mod(redis) 288 | 289 | return result, err 290 | end 291 | 292 | 293 | function _M.new(self, opts) 294 | opts = opts or {} 295 | local timeout = (opts.timeout and opts.timeout * 1000) or 1000 296 | local db_index= opts.db_index or 0 297 | 298 | for i = 1, #commands do 299 | local cmd = commands[i] 300 | _M[cmd] = 301 | function (self, ...) 302 | return do_command(self, cmd, ...) 303 | end 304 | end 305 | 306 | return setmetatable({ 307 | timeout = timeout, 308 | db_index = db_index, 309 | _reqs = nil }, mt) 310 | end 311 | 312 | 313 | return _M -------------------------------------------------------------------------------- /tool_dns_server.lua: -------------------------------------------------------------------------------- 1 | -- tool_dns_server.lua 2 | -- 工具类@动态获取指定域名对应IP 3 | local _mc_dns_server = {} 4 | 5 | local pcall = pcall 6 | local io_open = io.open 7 | local ngx_re_gmatch = ngx.re.gmatch 8 | 9 | local ok, new_tab = pcall(require, "table.new") 10 | if not ok then 11 | new_tab = function (narr, nrec) return {} end 12 | end 13 | 14 | local _dns_servers = new_tab(5, 0) 15 | 16 | local _read_file_data = function(path) 17 | local f, err = io_open(path, 'r') 18 | if not f or err then 19 | return nil, err 20 | end 21 | 22 | local data = f:read('*all') 23 | f:close() 24 | return data, nil 25 | end 26 | 27 | local _read_dns_servers_from_resolv_file = function() 28 | local text = _read_file_data('/etc/resolv.conf') 29 | 30 | local captures, it, err 31 | it, err = ngx_re_gmatch(text, [[^nameserver\s+(\d+?\.\d+?\.\d+?\.\d+$)]], "jomi") 32 | 33 | for captures, err in it do 34 | if not err then 35 | _dns_servers[#_dns_servers + 1] = captures[1] 36 | end 37 | end 38 | end 39 | 40 | _read_dns_servers_from_resolv_file() 41 | 42 | local require = require 43 | local ngx_re_find = ngx.re.find 44 | local lrucache = require "resty.lrucache" 45 | local resolver = require "resty.dns.resolver" 46 | local cache_storage = lrucache.new(200) 47 | local _cacheExpireTime = 300 48 | 49 | local _is_addr = function(hostname) 50 | return ngx_re_find(hostname, [[\d+?\.\d+?\.\d+?\.\d+$]], "jo") 51 | end 52 | 53 | 54 | -- 获取IP 55 | function _mc_dns_server.get_addr(self, hostname) 56 | if _is_addr(hostname) then 57 | return hostname, hostname 58 | end 59 | 60 | local addr = cache_storage:get(hostname) 61 | if addr then 62 | return addr, hostname 63 | end 64 | 65 | local r, err = resolver:new({ 66 | nameservers = _dns_servers, 67 | retrans = 5, -- 5 retransmissions on receive timeout 68 | timeout = 2000, -- 2 sec 69 | }) 70 | 71 | if not r then 72 | return nil, hostname 73 | end 74 | 75 | local answers, err = r:query(hostname, {qtype = r.TYPE_A}) 76 | 77 | if not answers or answers.errcode then 78 | return nil, hostname 79 | end 80 | 81 | for i, ans in ipairs(answers) do 82 | if ans.address then 83 | cache_storage:set(hostname, ans.address, _cacheExpireTime) 84 | return ans.address, hostname 85 | end 86 | end 87 | 88 | return nil, hostname 89 | end 90 | 91 | function _mc_dns_server.new(self, dns_entity) 92 | local dns_entity = dns_entity or {} 93 | setmetatable(dns_entity, self) 94 | self.__index = self 95 | return dns_entity 96 | end 97 | 98 | return _mc_dns_server --------------------------------------------------------------------------------