diff --git a/conf/nginx.conf b/conf/nginx.conf index b5eb4be..bb52ada 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -75,9 +75,11 @@ http { token_endpoint = "http://localhost:9080yum/v1/oauth/v2/token", userinfo_endpoint = "http://localhost:9080yum/v1/oauth/v2/userinfo", --jwks_uri = "http://localhost:9080/jwks", -- 公钥端点(可选) - response_types_supported = {"code"}, - subject_types_supported = {"public"}, - id_token_signing_alg_values_supported = {"HS256"} + grant_types_supported = [ "authorization_code", "token", "refresh_token" ], -- 新增支持 refresh_token + response_types_supported = { "code" }, + subject_types_supported = { "public" }, + id_token_signing_alg_values_supported = { "HS256" }, + refresh_token_issuance_supported = true -- 声明支持颁发 refresh_token } ngx.header["Content-Type"] = "application/json" ngx.say(cjson.encode(config)) diff --git a/src/api/oauth/oauth.lua b/src/api/oauth/oauth.lua index b04681c..bf9a3cb 100644 --- a/src/api/oauth/oauth.lua +++ b/src/api/oauth/oauth.lua @@ -31,6 +31,12 @@ local routes = { methods = { "POST" }, handler = oauthService.userinfo, }, + --回收token + { + paths = { "/yum/v1/oauth/v2/logout" }, + methods = { "POST" }, + handler = oauthService.logout, + }, } -- 初始化路由 diff --git a/src/config.lua b/src/config.lua index aed3a0d..04b140e 100644 --- a/src/config.lua +++ b/src/config.lua @@ -11,6 +11,10 @@ local _M = { secret_key = "!@#$5412$#@!", -- 确保这个密钥足够安全并保密 + access_token_ttl = 10 * 60, --十分钟 + refresh_token_ttl = 7 * 24 * 3600, --7天 + id_token_ttl = 60 * 60, --1小时 + REDIS_PREFIX = 'Auth:', -- 配置redis数据库连接 REDIS = { diff --git a/src/service/oauth/oauth.lua b/src/service/oauth/oauth.lua index 3fa9555..e3444fb 100644 --- a/src/service/oauth/oauth.lua +++ b/src/service/oauth/oauth.lua @@ -169,6 +169,8 @@ local function authorizateCode(args) local new_access_token = token.generate_access_token(priv_key, user_id, client_id, scope) -- 生成新 Refresh Token(滚动刷新) local new_refresh_token = token.generate_refresh_token(priv_key, user_id, client_id, scope) + --创建用户和应用id的刷新token + client.createRefreshToken(user_id, client_id) -- 生存id_token local new_id_token = token.generate_id_token(priv_key, user_id, client_id, scope) --ngx.say("Generated JWT: ", jwt_obj) @@ -193,13 +195,38 @@ end -- 刷新令牌 local function authorizateRefresh(args) -- 1.校验必填参数验证数据是否符合json - local ok = validator.validateRefresh(args) - if not ok then + local res = validator.validateRefresh(args) + if not res then local result = resp:json(0x000001) resp:send(result) return end - -- 2. + -- 2.验证并消费 refresh_token(滚动刷新:生成新的 rt) + local rt_data, err = client.consumeRefreshToken(res.refresh_token, res.client_id, true) + if not rt_data then + ngx.log(ngx.ERR, "refresh_token 验证失败: ", err) + ngx.exit(ngx.HTTP_BAD_REQUEST) + end + + -- 生成新的 access_token 和 id_token + local userinfo = { + sub = rt_data.sub, + name = "Test User", + email = "test@example.com" + } + -- 3.生成新 Access Token + local new_access_token = token.generate_access_token(priv_key, user_id, client_id, scope) + -- 生存id_token + local new_id_token = token.generate_id_token(priv_key, user_id, client_id, scope) + --ngx.say("Generated JWT: ", jwt_obj) + local ret = {} + ret.access_token = new_access_token + ret.token_type = "Bearer" + ret.expires_in = 10 * 60 + ret.id_token = new_id_token + -- 4.返回结果 + local result = resp:json(ngx.HTTP_OK, ret) + resp:send(result) end -- 根据授权码获取Access-Token @@ -238,6 +265,8 @@ function _M:token() authorizateCode(args) elseif grant_type == "refresh_token" then authorizateRefresh(args) + else + ngx.exit(ngx.HTTP_BAD_REQUEST) end end @@ -289,10 +318,10 @@ function _M:userinfo() ngx.exit(ngx.HTTP_UNAUTHORIZED) end --通过用户id获取用户信息 - print("-- get jwt_obj.payload value --") - for key, value in pairs(jwt_obj.payload) do - print("jwt_obj.payload: ", key, " ", value) - end + --print("-- get jwt_obj.payload value --") + --for key, value in pairs(jwt_obj.payload) do + -- print("jwt_obj.payload: ", key, " ", value) + --end local user_id = jwt_obj.payload.sub local code, rest = oauthDao.getUser(user_id) --读取数据错误 @@ -313,4 +342,24 @@ function _M:userinfo() resp:send(result) end +--回收token +function _M:logout() + -- 假设从会话中获取用户 ID(sub) + local sub = "user123" -- 实际应从登录会话中获取 + + -- 遍历所有 refresh_token(生产环境建议用 Redis 哈希存储用户与 rt 的映射) + local keys = red:keys("oidc:refresh_token:*") + for _, key in ipairs(keys) do + local rt_data_str = red:get(key) + if rt_data_str and rt_data_str ~= ngx.null then + local rt_data = cjson.decode(rt_data_str) + if rt_data.sub == sub then + rt_data.revoked = true + red:set(key, cjson.encode(rt_data)) + red:expire(key, 0) -- 立即过期 + end + end + end +end + return _M \ No newline at end of file diff --git a/src/util/client.lua b/src/util/client.lua index a6ae153..a7d3d3f 100644 --- a/src/util/client.lua +++ b/src/util/client.lua @@ -6,6 +6,7 @@ local red = require("share.redis") local cjson = require("cjson.safe") +local conf = require("config") local _M = {} @@ -41,4 +42,77 @@ function _M.validate(client_id, redirect_uri) return client end +-- 生成随机 refresh_token(64字节,更长更安全) +local function generate_refresh_token() + local random_bytes = random.bytes(64, true) -- 强随机数 + return str.to_hex(random_bytes) +end + +local str = require "resty.string" +local random = require "resty.random" + +-- 存储 refresh_token(关联用户、客户端、过期时间) +function _M.createRefreshToken(sub, client_id) + local rt = generate_refresh_token() + local rt_data = { + sub = sub, -- 用户唯一标识 + client_id = client_id, -- 客户端ID + expires_at = ngx.time() + conf.refresh_token_ttl, + revoked = false -- 是否吊销 + } + + -- 存储在 Redis:key=oidc:refresh_token:{rt} + local ok, err = red:set("oidc:refresh_token:" .. rt, cjson.encode(rt_data)) + if not ok then + return nil, err + end + red:expire("oidc:refresh_token:"..rt, conf.refresh_token_ttl) -- 设置过期时间 + return rt +end + +-- 验证并消费 refresh_token(支持一次性或滚动刷新) +-- 若 rolling=true,则生成新的 refresh_token 并吊销旧的 +function _M.consumeRefreshToken(rt, client_id, rolling) + -- 获取存储的 refresh_token 数据 + local rt_data_str, err = red:get("oidc:refresh_token:" .. rt) + if not rt_data_str or rt_data_str == ngx.null then + return nil, "无效的 refresh_token" + end + + local rt_data = cjson.decode(rt_data_str) + + -- 验证客户端匹配 + if rt_data.client_id ~= client_id then + return nil, "客户端不匹配" + end + + -- 验证未过期 + if rt_data.expires_at < ngx.time() then + return nil, "refresh_token 已过期" + end + + -- 验证未被吊销 + if rt_data.revoked then + return nil, "refresh_token 已吊销" + end + + -- 滚动刷新:生成新的 refresh_token,吊销旧的 + local new_rt = nil + if rolling then + new_rt, err = _M.create(rt_data.sub, client_id) + if not new_rt then + return nil, "生成新 refresh_token 失败: " .. (err or "") + end + -- 吊销旧的 refresh_token + rt_data.revoked = true + red:set("oidc:refresh_token:" .. rt, cjson.encode(rt_data)) + red:expire("oidc:refresh_token:" .. rt, 0) -- 立即过期 + end + + return { + sub = rt_data.sub, + new_refresh_token = new_rt -- 新的 refresh_token(若滚动刷新) + } +end + return _M \ No newline at end of file diff --git a/src/util/token.lua b/src/util/token.lua index 1f41425..ec56567 100644 --- a/src/util/token.lua +++ b/src/util/token.lua @@ -21,13 +21,13 @@ local schema = { local obj = { header = { typ = "JWT", alg = "HS256" }, payload = { -- 自定义数据 - userid = "", -- 用户id + sub = "", -- 用户id username = "", -- 用户名 role_id = "", -- 角色id role_name = "", -- 角色名称 --iss = "your_issuer", -- 签发者 --sub = "1234567890", -- 主题 - exp = ngx.time() + 3600, -- 过期时间(例如:当前时间+1小时) + exp = ngx.time() + conf.access_token_ttl, -- 过期时间(例如:当前时间+1小时) iat = ngx.time() -- 签发时间 } } @@ -38,7 +38,7 @@ function _M.generateToken(userid, username, role_id, role_name) return "" end - obj.payload.userid = userid + obj.payload.sub = userid obj.payload.username = username obj.payload.role_id = role_id obj.payload.role_name = role_name @@ -93,10 +93,6 @@ function _M.authorizationToken(auth_header) return response end -local access_token_ttl = 10 * 60 --十分钟 -local refresh_token_ttl = 7 * 24 * 3600 --7天 -local id_token_ttl = 60 * 60 --1小时 - -- 生成 Access Token(简化为 JWT 格式) function _M.generate_access_token(priv_key, sub, client_id, scope) local now = ngx.time() @@ -104,7 +100,7 @@ function _M.generate_access_token(priv_key, sub, client_id, scope) --iss = OP_DOMAIN, sub = sub, aud = client_id, - exp = now + access_token_ttl, + exp = now + conf.access_token_ttl, iat = now, scope = scope, --"openid profile email" jti = ngx.md5(now .. math.random() .. client_id) -- 唯一标识 @@ -123,7 +119,7 @@ function _M.generate_refresh_token(priv_key, sub, client_id, scope) --iss = OP_DOMAIN, sub = sub, aud = client_id, - exp = now + refresh_token_ttl, + exp = now + conf.refresh_token_ttl, iat = now, scope = scope, --"openid profile email" jti = ngx.md5(now .. math.random() * 1000 .. client_id) @@ -142,7 +138,7 @@ function _M.generate_id_token(priv_key, sub, client_id, userinfo, scope) --iss = OP_DOMAIN, -- issuer:OP 域名 sub = sub, -- subject:用户唯一标识 aud = client_id, -- audience:客户端 ID - exp = now + id_token_ttl, -- 过期时间(1小时) + exp = now + conf.id_token_ttl, -- 过期时间(1小时) iat = now, -- 签发时间 nonce = ngx.var.nonce, -- 可选:防重放攻击 --name = userinfo.name, diff --git a/src/validator/oauth/oauth.lua b/src/validator/oauth/oauth.lua index 9c6333c..fb60023 100644 --- a/src/validator/oauth/oauth.lua +++ b/src/validator/oauth/oauth.lua @@ -87,10 +87,11 @@ local schemaRefresh = { type = "object", properties = { grant_type = { type = "string" }, + refresh_token = { type = "string" }, client_id = { type = "string" }, client_secret = { type = "string" }, }, - required = { "grant_type", "client_id", "client_secret" } + required = { "grant_type", "refresh_token", "client_id", "client_secret" } } --根据Refresh-Token刷新Access-Token