├── README.md ├── api.lua ├── handler.lua ├── jwt_parser.lua ├── redis.lua └── schema.lua /README.md: -------------------------------------------------------------------------------- 1 | # kong-jwt-plus 2 | ## 说明 3 | 实现了Jwt拦截,登录时调用第三方登录接口。登录后会生成JWT-token,把返回的jwt-token放入hearder里面,下次请求插件会解析出加密前的登录信息,放入hearder里面,然后再访问相应的业务系统 4 | 5 | ## 配置 6 | 7 | ##### 修改redis.lua下的redis配置 8 | * **redis_host** - Redis 的 Hostname or IP 9 | * **redis_port** - Redis Port (默认为 6379) 10 | * **redis_password** - Redis 密码 (默认为空) 11 | 12 | ##### 修改api.lua下的第三方登录接口配置 13 | * **ConsumerId** -在kong上面生成Consumer,命名为如下的:“psplocal”,跟登录地址配置时的名称一致,然后生成consumer对应的jwt token) 14 | * **ssobody** - 请求参数,根据具体项目做修改 15 | 16 | 17 | ## 使用 18 | 19 | ##### kong开启改插件(教程很多) 20 | 21 | 22 | ##### 登录接口 23 | * **地址**--kong:8001/sso/consumers/jwt/login 24 | * **方法**--Post 25 | * **参数**--请求参数,根据具体项目做修改(例子中是Phone,Captcha,ConsumerId) 26 | * **返回**--Jwt-token 27 | 28 | ##### hearder 29 | * **authorization**:Bearer + Jwt-token 30 | 31 | ##### 退出登录(包含踢下线) 32 | * **地址**--kong:8001/sso/consumers/jwt/forbid_login 33 | * **方法**--Post 34 | * **参数**--uid,根据具体项目做修改(例子中是第三方登录返回的UserInfo中的UserId) 35 | 36 | 37 | -------------------------------------------------------------------------------- /api.lua: -------------------------------------------------------------------------------- 1 | local crud = require "kong.api.crud_helpers" 2 | local singletons = require "kong.singletons" 3 | local http = require "resty.http" 4 | local cjson = require "cjson" 5 | local jwt_encoder = require "kong.plugins.jwt-plus.jwt_parser" 6 | local ngx_time = ngx.time 7 | local utils = require "kong.tools.utils" 8 | local redis = require "kong.plugins.jwt-plus.redis" 9 | local responses = require "kong.tools.responses" 10 | -- 设置token有效期,当前是第四天零点 11 | JWT_EXP_TIME = 345600 12 | 13 | SSO_API_MAP = { 14 | -- 登录接口的地址配置,当前的请求参数为ConsumerId(这个在kong上面生成Consumer,命名为如下的:“psplocal”) 15 | psplocal = { 16 | host = "http://localhost:15000/api/v1.0/login/admin", 17 | token = "Basic " 18 | }, 19 | psptest = { 20 | host = "http://XXXX:15000/api/v1.0/login/admin", 21 | token = "Basic " 22 | }, 23 | } 24 | 25 | 26 | local function ret(msg) 27 | 28 | return false,msg 29 | end 30 | 31 | 32 | 33 | local function load_secret(consumer_id) 34 | local rows, err = singletons.dao.jwt_secrets:find_all {consumer_id = consumer_id} 35 | if err then 36 | return nil, err 37 | end 38 | return rows[1] 39 | end 40 | 41 | local function sso_login(params) 42 | -- 通过consumer_id获取username既环境名 43 | local consumer_rows ,err = singletons.dao.consumers:find_all {id = params.ConsumerId} 44 | if next(consumer_rows) == nil or err then 45 | return ret('ConsumerId not exist') 46 | end 47 | 48 | local project_name = consumer_rows[1].username 49 | 50 | SSO_API = SSO_API_MAP[project_name] 51 | 52 | local httpc = http.new() 53 | local ssobody = '' 54 | 55 | if string.find(project_name, "psp") ~= nil then 56 | ssobody = '{"Phone":"' .. params.Phone .. '","Captcha":"' .. params.Captcha .. '"}' 57 | end 58 | 59 | 60 | local res, err = httpc:request_uri( 61 | SSO_API.host, 62 | { 63 | ssl_verify = ssl_verify or false, 64 | headers = { Authorization = SSO_API.token, 65 | ["Content-Type"] = "application/json;"}, 66 | method = "POST", 67 | body = ssobody, 68 | } 69 | ) 70 | if not res or err or 200 ~= res.status then 71 | 72 | return ret('登录失败,请检查登录信息') 73 | end 74 | 75 | local ret_info = cjson.decode(res.body) 76 | if ret_info.success == false then 77 | return ret(ret_info.msg) 78 | end 79 | 80 | 81 | 82 | 83 | local user_info = ret_info["resultData"]["user_info"] 84 | 85 | local data = {} 86 | 87 | 88 | 89 | 90 | local secret = load_secret(params.ConsumerId) 91 | 92 | if not secret then 93 | 94 | return ret('cant not find key.') 95 | end 96 | 97 | local temp_date = os.date("*t", os.time()) 98 | local expcount = os.time({year=temp_date.year, month=temp_date.month, day=temp_date.day, hour=0}) + JWT_EXP_TIME 99 | 100 | if string.find(project_name, "psp") ~= nil then 101 | data = { 102 | key = secret.key, 103 | uid = user_info.UserId, 104 | user_info = user_info, 105 | exp = expcount, 106 | token = user_info.UserId.."/"..secret.key.."/"..utils:random_string() 107 | } 108 | elseif string.find(project_name, "xiao") ~= nil then 109 | data = { 110 | key = secret.key, 111 | uid = user_info.uid, 112 | user_info = user_info, 113 | exp = expcount, 114 | token = user_info.uid.."/"..secret.key.."/"..utils:random_string() 115 | } 116 | end 117 | 118 | local new_token = jwt_encoder:encode_token(data, secret.secret) 119 | 120 | local resp,err = redis:save_newtoken(data.token) 121 | if err then 122 | return ret('can not save token') 123 | end 124 | 125 | return new_token 126 | end 127 | 128 | 129 | return { 130 | 131 | -- 完成密码登录验证 132 | -- 完成jwt返回 133 | -- 修改header头,增加uid 134 | 135 | ["/sso/consumers/jwt/login"] = { 136 | before = function(self, dao_factory, helpers) 137 | 138 | end, 139 | 140 | GET = function(self, dao_factory) 141 | return helpers.responses.send_HTTP_OK(self.params) 142 | end, 143 | 144 | POST = function(self, dao_factory, helpers) 145 | -- local jwt_token ,err= sso_login(self.params) 146 | local jwt_token,err = sso_login(self.params) 147 | if err then 148 | return responses.send(200,{resultCode = 400,resultMessage = err,resultData=null}) 149 | end 150 | return responses.send(200,{resultCode = 0,resultMessage = "0",resultData=jwt_token}) 151 | end 152 | 153 | }, 154 | 155 | ["/sso/consumers/jwt/forbid_login"] = { 156 | before = function(self, dao_factory, helpers) 157 | 158 | end, 159 | 160 | GET = function(self, dao_factory) 161 | return helpers.responses.send_HTTP_OK(self.params) 162 | end, 163 | 164 | POST = function(self, dao_factory, helpers) 165 | local tokens,err = redis:get_tokens_uid(self.params.uid) 166 | if err then 167 | return helpers.responses.send_HTTP_NOT_FOUND(err) 168 | end 169 | 170 | local res,err = redis:del_token(tokens) 171 | if err then 172 | return responses.send(200,{resultCode = 400,resultMessage = err,resultData=null}) 173 | end 174 | return responses.send(200,{resultCode = 0,resultMessage = "0",resultData=null}) 175 | end 176 | 177 | } 178 | 179 | } -------------------------------------------------------------------------------- /handler.lua: -------------------------------------------------------------------------------- 1 | local singletons = require "kong.singletons" 2 | local BasePlugin = require "kong.plugins.base_plugin" 3 | local responses = require "kong.tools.responses" 4 | local constants = require "kong.constants" 5 | local jwt_decoder = require "kong.plugins.jwt-plus.jwt_parser" 6 | local redis = require "kong.plugins.jwt-plus.redis" 7 | local string_format = string.format 8 | local ngx_re_gmatch = ngx.re.gmatch 9 | local cjson_decode = require("cjson").decode 10 | local ngx_set_header = ngx.req.set_header 11 | local get_method = ngx.req.get_method 12 | 13 | local JwtPlusHandler = BasePlugin:extend() 14 | 15 | JwtPlusHandler.PRIORITY = 1005 16 | JwtPlusHandler.VERSION = "0.1.0" 17 | 18 | --- Retrieve a JWT in a request. 19 | -- Checks for the JWT in URI parameters, then in the `Authorization` header. 20 | -- @param request ngx request object 21 | -- @param conf Plugin configuration 22 | -- @return token JWT token contained in request (can be a table) or nil 23 | -- @return err 24 | local function retrieve_token(request, conf) 25 | local uri_parameters = request.get_uri_args() 26 | 27 | for _, v in ipairs(conf.uri_param_names) do 28 | if uri_parameters[v] then 29 | return uri_parameters[v] 30 | end 31 | end 32 | 33 | local authorization_header = request.get_headers()["authorization"] 34 | if authorization_header then 35 | local iterator, iter_err = ngx_re_gmatch(authorization_header, "\\s*[Bb]earer\\s+(.+)") 36 | if not iterator then 37 | return nil, iter_err 38 | end 39 | 40 | local m, err = iterator() 41 | if err then 42 | return nil, err 43 | end 44 | 45 | if m and #m > 0 then 46 | return m[1] 47 | end 48 | end 49 | end 50 | 51 | function JwtPlusHandler:new() 52 | JwtPlusHandler.super.new(self, "jwt-plus") 53 | end 54 | 55 | local function load_credential(jwt_secret_key) 56 | local rows, err = singletons.dao.jwt_secrets:find_all {key = jwt_secret_key} 57 | if err then 58 | return nil, err 59 | end 60 | return rows[1] 61 | end 62 | 63 | local function load_consumer(consumer_id, anonymous) 64 | local result, err = singletons.dao.consumers:find { id = consumer_id } 65 | if not result then 66 | if anonymous and not err then 67 | err = 'anonymous consumer "' .. consumer_id .. '" not found' 68 | end 69 | return nil, err 70 | end 71 | return result 72 | end 73 | 74 | 75 | local function set_header_claims(key, value) 76 | local append_key = "X_XH_" .. key 77 | ngx_set_header(append_key, value) 78 | end 79 | 80 | 81 | 82 | local function set_claims(claims, jwt_secret) 83 | -- if not claims then 84 | -- claims = { 85 | -- uid = nil, 86 | -- account = nil, 87 | -- employee_no = nil 88 | -- } 89 | -- end 90 | 91 | if next(claims) ~= nil then 92 | 93 | 94 | for key, value in pairs(claims.user_info) do 95 | set_header_claims(key,value) 96 | 97 | end 98 | 99 | end 100 | 101 | ngx_set_header('Access-Control-Allow-Origin', '*') -- 增加跨域头 102 | -- ngx.ctx.authenticated_consumer = consumer 103 | if jwt_secret then 104 | ngx.ctx.authenticated_credential = jwt_secret 105 | ngx_set_header(constants.HEADERS.ANONYMOUS, nil) -- in case of auth plugins concatenation 106 | else 107 | ngx_set_header(constants.HEADERS.ANONYMOUS, true) 108 | end 109 | 110 | end 111 | 112 | local function do_authentication(conf) 113 | local token, err = retrieve_token(ngx.req, conf) 114 | 115 | if err then 116 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 117 | end 118 | 119 | local ttype = type(token) 120 | if ttype ~= "string" then 121 | if ttype == "nil" then 122 | return false, {status = 401} 123 | elseif ttype == "table" then 124 | return false, {status = 401, message = "Multiple tokens provided"} 125 | else 126 | return false, {status = 401, message = "Unrecognizable token"} 127 | end 128 | end 129 | 130 | -- Decode token to find out who the consumer is 131 | local jwt, err = jwt_decoder:new(token) 132 | if err then 133 | return false, {status = 401, message = "Bad token; " .. tostring(err)} 134 | end 135 | 136 | local claims = jwt.claims 137 | 138 | -- 判断密码是否变更或禁用 139 | local resp, err = redis:exist_token(claims.token) 140 | if err then 141 | return false,err 142 | end 143 | 144 | 145 | local jwt_secret_key = claims[conf.key_claim_name] 146 | if not claims or not jwt_secret_key then 147 | return false, {status = 401, message = "No mandatory '" .. conf.key_claim_name .. "' in claims"} 148 | end 149 | 150 | -- Retrieve the secret 151 | local jwt_secret_cache_key = singletons.dao.jwt_secrets:cache_key(jwt_secret_key) 152 | local jwt_secret, err = singletons.cache:get(jwt_secret_cache_key, nil, 153 | load_credential, jwt_secret_key) 154 | if err then 155 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 156 | end 157 | 158 | if not jwt_secret then 159 | return false, {status = 403, message = "No credentials found for given '" .. conf.key_claim_name .. "'"} 160 | end 161 | 162 | local algorithm = jwt_secret.algorithm or "HS256" 163 | 164 | -- Verify "alg" 165 | if jwt.header.alg ~= algorithm then 166 | return false, {status = 403, message = "Invalid algorithm"} 167 | end 168 | 169 | local jwt_secret_value = algorithm == "HS256" and jwt_secret.secret or jwt_secret.rsa_public_key 170 | if conf.secret_is_base64 then 171 | jwt_secret_value = jwt:b64_decode(jwt_secret_value) 172 | end 173 | 174 | if not jwt_secret_value then 175 | return false, {status = 403, message = "Invalid key/secret"} 176 | end 177 | 178 | 179 | -- Now verify the JWT signature 180 | if not jwt:verify_signature(jwt_secret_value) then 181 | return false, {status = 403, message = "Invalid signature"} 182 | end 183 | 184 | -- Verify the JWT registered claims 185 | local ok_claims, errors = jwt:verify_registered_claims(conf.claims_to_verify) 186 | if not ok_claims then 187 | return false, {status = 401, message = errors} 188 | end 189 | 190 | -- Retrieve the consumer 191 | local consumer_cache_key = singletons.dao.consumers:cache_key(jwt_secret.consumer_id) 192 | local consumer, err = singletons.cache:get(consumer_cache_key, nil, 193 | load_consumer, 194 | jwt_secret.consumer_id, true) 195 | if err then 196 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 197 | end 198 | 199 | -- However this should not happen 200 | if not consumer then 201 | return false, {status = 403, message = string_format("Could not find consumer for '%s=%s'", conf.key_claim_name, jwt_secret_key)} 202 | end 203 | 204 | 205 | set_claims(claims, jwt_secret) 206 | 207 | return true 208 | end 209 | 210 | 211 | function JwtPlusHandler:access(conf) 212 | JwtPlusHandler.super.access(self) 213 | 214 | -- check if preflight request and whether it should be authenticated 215 | if conf.run_on_preflight == false and get_method() == "OPTIONS" then 216 | -- FIXME: the above `== false` test is because existing entries in the db will 217 | -- have `nil` and hence will by default start passing the preflight request 218 | -- This should be fixed by a migration to update the actual entries 219 | -- in the datastore 220 | return 221 | end 222 | 223 | if ngx.ctx.authenticated_credential and conf.anonymous ~= "" then 224 | -- we're already authenticated, and we're configured for using anonymous, 225 | -- hence we're in a logical OR between auth methods and we're already done. 226 | return 227 | end 228 | 229 | local ok, err = do_authentication(conf) 230 | if not ok then 231 | if conf.anonymous ~= "" then 232 | -- get anonymous user 233 | local consumer_cache_key = singletons.dao.consumers:cache_key(conf.anonymous) 234 | local consumer, err = singletons.cache:get(consumer_cache_key, nil, 235 | load_consumer, 236 | conf.anonymous, true) 237 | if err then 238 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 239 | end 240 | set_claims(nil, nil) 241 | else 242 | -- return responses.send(err.status, err.message) 243 | return responses.send(err.status, {resultCode = err.status,resultMessage = err.message, resultData=null}) 244 | end 245 | end 246 | end 247 | 248 | -- function JwtPlusHandler:header_filter(conf) 249 | -- JwtPlusHandler.super.header_filter(self) 250 | -- ngx.header['Access-Control-Allow-Origin'] = '*' 251 | -- end 252 | 253 | 254 | return JwtPlusHandler 255 | -------------------------------------------------------------------------------- /jwt_parser.lua: -------------------------------------------------------------------------------- 1 | -- JWT verification module 2 | -- Adapted version of x25/luajwt for Kong. It provides various improvements and 3 | -- an OOP architecture allowing the JWT to be parsed and verified separatly, 4 | -- avoiding multiple parsings. 5 | -- 6 | -- @see https://github.com/x25/luajwt 7 | 8 | local json = require "cjson" 9 | local utils = require "kong.tools.utils" 10 | local crypto = require "crypto" 11 | local asn_sequence = require "kong.plugins.jwt.asn_sequence" 12 | 13 | local error = error 14 | local type = type 15 | local pcall = pcall 16 | local ngx_time = ngx.time 17 | local string_rep = string.rep 18 | local string_sub = string.sub 19 | local table_concat = table.concat 20 | local setmetatable = setmetatable 21 | local encode_base64 = ngx.encode_base64 22 | local decode_base64 = ngx.decode_base64 23 | 24 | --- Supported algorithms for signing tokens. 25 | local alg_sign = { 26 | ["HS256"] = function(data, key) return crypto.hmac.digest("sha256", data, key, true) end, 27 | --["HS384"] = function(data, key) return crypto.hmac.digest("sha384", data, key, true) end, 28 | --["HS512"] = function(data, key) return crypto.hmac.digest("sha512", data, key, true) end 29 | ["RS256"] = function(data, key) return crypto.sign('sha256', data, crypto.pkey.from_pem(key, true)) end, 30 | ["RS512"] = function(data, key) return crypto.sign('sha512', data, crypto.pkey.from_pem(key, true)) end, 31 | ["ES256"] = function(data, key) 32 | local pkeyPrivate = crypto.pkey.from_pem(key, true) 33 | local signature = crypto.sign('sha256', data, pkeyPrivate) 34 | 35 | local derSequence = asn_sequence.parse_simple_sequence(signature) 36 | local r = asn_sequence.unsign_integer(derSequence[1], 32) 37 | local s = asn_sequence.unsign_integer(derSequence[2], 32) 38 | assert(#r == 32) 39 | assert(#s == 32) 40 | return r .. s 41 | end 42 | } 43 | 44 | --- Supported algorithms for verifying tokens. 45 | local alg_verify = { 46 | ["HS256"] = function(data, signature, key) return signature == alg_sign["HS256"](data, key) end, 47 | --["HS384"] = function(data, signature, key) return signature == alg_sign["HS384"](data, key) end, 48 | --["HS512"] = function(data, signature, key) return signature == alg_sign["HS512"](data, key) end 49 | ["RS256"] = function(data, signature, key) 50 | local pkey = assert(crypto.pkey.from_pem(key), "Consumer Public Key is Invalid") 51 | return crypto.verify('sha256', data, signature, pkey) 52 | end, 53 | ["RS512"] = function(data, signature, key) 54 | local pkey = assert(crypto.pkey.from_pem(key), "Consumer Public Key is Invalid") 55 | return crypto.verify('sha512', data, signature, pkey) 56 | end, 57 | ["ES256"] = function(data, signature, key) 58 | local pkey = assert(crypto.pkey.from_pem(key), "Consumer Public Key is Invalid") 59 | assert(#signature == 64, "Signature must be 64 bytes.") 60 | local asn = {} 61 | asn[1] = asn_sequence.resign_integer(string_sub(signature, 1, 32)) 62 | asn[2] = asn_sequence.resign_integer(string_sub(signature, 33, 64)) 63 | local signatureAsn = asn_sequence.create_simple_sequence(asn) 64 | return crypto.verify('sha256', data, signatureAsn, pkey) 65 | end 66 | } 67 | 68 | --- base 64 encoding 69 | -- @param input String to base64 encode 70 | -- @return Base64 encoded string 71 | local function b64_encode(input) 72 | local result = encode_base64(input) 73 | result = result:gsub("+", "-"):gsub("/", "_"):gsub("=", "") 74 | return result 75 | end 76 | 77 | --- base 64 decode 78 | -- @param input String to base64 decode 79 | -- @return Base64 decoded string 80 | local function b64_decode(input) 81 | local remainder = #input % 4 82 | 83 | if remainder > 0 then 84 | local padlen = 4 - remainder 85 | input = input .. string_rep('=', padlen) 86 | end 87 | 88 | input = input:gsub("-", "+"):gsub("_", "/") 89 | return decode_base64(input) 90 | end 91 | 92 | --- Tokenize a string by delimiter 93 | -- Used to separate the header, claims and signature part of a JWT 94 | -- @param str String to tokenize 95 | -- @param div Delimiter 96 | -- @param len Number of parts to retrieve 97 | -- @return A table of strings 98 | local function tokenize(str, div, len) 99 | local result, pos = {}, 0 100 | 101 | for st, sp in function() return str:find(div, pos, true) end do 102 | result[#result + 1] = str:sub(pos, st-1) 103 | pos = sp + 1 104 | len = len - 1 105 | if len <= 1 then 106 | break 107 | end 108 | end 109 | 110 | result[#result + 1] = str:sub(pos) 111 | return result 112 | end 113 | 114 | --- Parse a JWT 115 | -- Parse a JWT and validate header values. 116 | -- @param token JWT to parse 117 | -- @return A table containing base64 and decoded headers, claims and signature 118 | local function decode_token(token) 119 | -- Get b64 parts 120 | local header_64, claims_64, signature_64 = unpack(tokenize(token, ".", 3)) 121 | 122 | -- Decode JSON 123 | local ok, header, claims, signature = pcall(function() 124 | return json.decode(b64_decode(header_64)), 125 | json.decode(b64_decode(claims_64)), 126 | b64_decode(signature_64) 127 | end) 128 | if not ok then 129 | return nil, "invalid JSON" 130 | end 131 | 132 | if header.typ and header.typ:upper() ~= "JWT" then 133 | return nil, "invalid typ" 134 | end 135 | 136 | if not header.alg or type(header.alg) ~= "string" or not alg_verify[header.alg] then 137 | return nil, "invalid alg" 138 | end 139 | 140 | if not claims then 141 | return nil, "invalid claims" 142 | end 143 | 144 | if not signature then 145 | return nil, "invalid signature" 146 | end 147 | 148 | return { 149 | token = token, 150 | header_64 = header_64, 151 | claims_64 = claims_64, 152 | signature_64 = signature_64, 153 | header = header, 154 | claims = claims, 155 | signature = signature 156 | } 157 | end 158 | 159 | -- For test purposes 160 | local function encode_token(data, key, alg, header) 161 | if type(data) ~= "table" then 162 | error("Argument #1 must be table", 2) 163 | end 164 | if type(key) ~= "string" then 165 | error(json.encode(data), 2) 166 | end 167 | if header and type(header) ~= "table" then 168 | error("Argument #4 must be a table", 2) 169 | end 170 | 171 | alg = alg or "HS256" 172 | 173 | if not alg_sign[alg] then 174 | error("Algorithm not supported", 2) 175 | end 176 | 177 | local header = header or {typ = "JWT", alg = alg} 178 | local segments = { 179 | b64_encode(json.encode(header)), 180 | b64_encode(json.encode(data)) 181 | } 182 | 183 | local signing_input = table_concat(segments, ".") 184 | local signature = alg_sign[alg](signing_input, key) 185 | segments[#segments+1] = b64_encode(signature) 186 | return table_concat(segments, ".") 187 | end 188 | 189 | --[[ 190 | 191 | JWT public interface 192 | 193 | ]]-- 194 | 195 | local _M = {} 196 | _M.__index = _M 197 | 198 | --- Instanciate a JWT parser 199 | -- Parse a JWT and instanciate a JWT parser for further operations 200 | -- Return errors instead of an instance if any encountered 201 | -- @param token JWT to parse 202 | -- @return JWT parser 203 | -- @return error if any 204 | function _M:new(token) 205 | if type(token) ~= "string" then 206 | error("Token must be a string, got " .. tostring(token), 2) 207 | end 208 | 209 | local token, err = decode_token(token) 210 | if err then 211 | return nil, err 212 | end 213 | 214 | return setmetatable(token, _M) 215 | end 216 | 217 | 218 | function _M:encode_token(data, key, alg, header) 219 | if type(data) ~= "table" then 220 | error("Argument #1 must be table", 2) 221 | end 222 | if type(key) ~= "string" then 223 | error("Argument #2 must be a table", 2) 224 | end 225 | if header and type(header) ~= "table" then 226 | error("Argument #4 must be a table", 2) 227 | end 228 | 229 | alg = alg or "HS256" 230 | 231 | if not alg_sign[alg] then 232 | error("Algorithm not supported", 2) 233 | end 234 | 235 | local header = header or {typ = "JWT", alg = alg} 236 | local segments = { 237 | b64_encode(json.encode(header)), 238 | b64_encode(json.encode(data)) 239 | } 240 | 241 | local signing_input = table_concat(segments, ".") 242 | local signature = alg_sign[alg](signing_input, key) 243 | segments[#segments+1] = b64_encode(signature) 244 | return table_concat(segments, ".") 245 | end 246 | 247 | --- Verify a JWT signature 248 | -- Verify the current JWT signature against a given key 249 | -- @param key Key against which to verify the signature 250 | -- @return A boolean indicating if the signature if verified or not 251 | function _M:verify_signature(key) 252 | return alg_verify[self.header.alg](self.header_64 .. "." .. self.claims_64, self.signature, key) 253 | end 254 | 255 | function _M:b64_decode(input) 256 | return b64_decode(input) 257 | end 258 | 259 | --- Registered claims according to RFC 7519 Section 4.1 260 | local registered_claims = { 261 | ["nbf"] = { 262 | type = "number", 263 | check = function(nbf) 264 | if nbf > ngx_time() then 265 | return "token not valid yet" 266 | end 267 | end 268 | }, 269 | ["exp"] = { 270 | type = "number", 271 | check = function(exp) 272 | if exp <= ngx_time() then 273 | return "token expired" 274 | end 275 | end 276 | } 277 | } 278 | 279 | --- Verify registered claims (according to RFC 7519 Section 4.1) 280 | -- Claims are verified by type and a check. 281 | -- @param claims_to_verify A list of claims to verify. 282 | -- @return A boolean indicating true if no errors zere found 283 | -- @return A list of errors 284 | function _M:verify_registered_claims(claims_to_verify) 285 | if not claims_to_verify then 286 | claims_to_verify = {} 287 | end 288 | local errors = nil 289 | local claim, claim_rules 290 | 291 | for _, claim_name in pairs(claims_to_verify) do 292 | claim = self.claims[claim_name] 293 | claim_rules = registered_claims[claim_name] 294 | if type(claim) ~= claim_rules.type then 295 | errors = utils.add_error(errors, claim_name, "must be a " .. claim_rules.type) 296 | else 297 | local check_err = claim_rules.check(claim) 298 | if check_err then 299 | errors = utils.add_error(errors, claim_name, check_err) 300 | end 301 | end 302 | end 303 | 304 | return errors == nil, errors 305 | end 306 | 307 | _M.encode = encode_token 308 | 309 | return _M 310 | -------------------------------------------------------------------------------- /redis.lua: -------------------------------------------------------------------------------- 1 | local redis = require "resty.redis" 2 | local ngx_log = ngx.log 3 | local cjson_decode = require("cjson").decode 4 | local cjson_encode = require("cjson").encode 5 | local redis_host = "localhost" 6 | local redis_port = 6379 7 | local redis_password = "" 8 | local _M = {} 9 | _M.__index = _M 10 | 11 | 12 | 13 | function _M.save_newtoken(self,token) 14 | local red = connectme() 15 | local ok, err = red:set(token,'0') 16 | if err then 17 | ngx_log(ngx.ERR, "[redis-log] can't save to Redis: ", err) 18 | return false, err 19 | end 20 | return true 21 | end 22 | 23 | function _M.exist_token(self,token) 24 | local red = connectme() 25 | local resp, err = red:keys(token) 26 | if err then 27 | return false, err 28 | end 29 | if next(resp) == nil then 30 | return false ,{status = 401, message = "登录信息失效"} 31 | end 32 | return resp 33 | end 34 | 35 | 36 | function _M.del_token(self,tokens) 37 | local red = connectme() 38 | red:init_pipeline() 39 | for i in pairs(tokens) do 40 | red:del(tokens[i]) 41 | end 42 | local ok, err = red:commit_pipeline() 43 | if not ok then 44 | return false, err 45 | end 46 | end 47 | 48 | function _M.get_tokens_uid(self,uid) 49 | local red = connectme() 50 | local tokens, err = red:keys(uid.."*") 51 | if next(tokens) == nil then 52 | return false, "token不存在" 53 | end 54 | return tokens 55 | end 56 | 57 | function connectme() 58 | local red = redis:new() 59 | red:set_timeout(1000) 60 | 61 | local ok, err = red:connect(redis_password, redis_port) 62 | 63 | if not ok then 64 | ngx_log(ngx.ERR, "[redis-log] failed to connect to Redis: ", err) 65 | return 66 | end 67 | 68 | local ok, err = red:auth(redis_password) 69 | if not ok then 70 | ngx_log(ngx.ERR, "failed to connect to Redis: ", err) 71 | return 72 | end 73 | 74 | 75 | return red 76 | end 77 | 78 | return _M -------------------------------------------------------------------------------- /schema.lua: -------------------------------------------------------------------------------- 1 | local utils = require "kong.tools.utils" 2 | 3 | local function check_user(anonymous) 4 | if anonymous == "" or utils.is_valid_uuid(anonymous) then 5 | return true 6 | end 7 | 8 | return false, "the anonymous user must be empty or a valid uuid" 9 | end 10 | 11 | return { 12 | no_consumer = true, 13 | fields = { 14 | uri_param_names = {type = "array", default = {"jwt"}}, 15 | key_claim_name = {type = "string", default = "key"}, 16 | secret_is_base64 = {type = "boolean", default = false}, 17 | claims_to_verify = {type = "array", enum = {"exp", "nbf"}}, 18 | anonymous = {type = "string", default = "", func = check_user}, 19 | run_on_preflight = {type = "boolean", default = true} 20 | }, 21 | } 22 | --------------------------------------------------------------------------------