Compare commits

...

2 Commits

9 changed files with 162 additions and 26 deletions

View File

@ -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))

View File

@ -31,6 +31,12 @@ local routes = {
methods = { "POST" },
handler = oauthService.userinfo,
},
--回收token
{
paths = { "/yum/v1/oauth/v2/logout" },
methods = { "POST" },
handler = oauthService.logout,
},
}
-- 初始化路由

View File

@ -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 = {

View File

@ -88,6 +88,9 @@ function _M.getApplicationByUserid(user_id, client_id, client_secret)
return applicationDao.getApplicationByUserid(user_id, client_id, client_secret)
end
function _M.addApplicationToken(client_id, ret)
return applicationTokenDao.addApplicationToken(client_id, ret)
end
function _M.updateApplicationToken(client_id, ret)
return applicationTokenDao.updateApplicationToken(client_id, ret)
end

View File

@ -15,7 +15,7 @@ local _M = {}
--判断应用是否存在
local function isExistApplicationToken(client_id)
--根据应用id进行验证应用是否存在
local code, res = applicationTokenModel:find(client_id)
local code, res = applicationTokenModel:where("application_id", "=", client_id):get()
if code ~= 0 or res == nil then
return false
end
@ -38,7 +38,7 @@ function _M.getApplicationToken(id)
end
--增加应用信息到数据表
function _M.addSystemApplicationToken(client_id, jsonData)
function _M.addApplicationToken(client_id, jsonData)
--根据应用进行验证是否存在
local code, res = applicationTokenModel:where("application_id", "=", client_id):get()
if code ~= 0 or res == nil then
@ -68,12 +68,13 @@ end
function _M.updateApplicationToken(id, jsonData)
--根据用户id进行验证用户是否存在
local ok = isExistApplicationToken(id)
--用户不存在则返回
if ok == false then
return 0x000001,nil
jsonData.application_id = id
return applicationTokenModel:create(jsonData)
end
--对数据内容进行更
jsonData.update_time = ngx.time()
return applicationTokenModel:where('application_id', '=', id):update(jsonData)
end

View File

@ -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)
@ -179,7 +181,7 @@ local function authorizateCode(args)
ret.refresh_token = new_refresh_token
ret.id_token = new_id_token
-- 6.将生成的数据存储到数据库中
local code, res = oauthDao.addSystemApplicationToken(client_id, ret)
local code, res = oauthDao.updateApplicationToken(client_id, ret)
if code ~= 0 then
local result = resp:json(0x000001)
resp:send(result)
@ -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()
-- 假设从会话中获取用户 IDsub
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

View File

@ -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_token64字节更长更安全
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 -- 是否吊销
}
-- 存储在 Rediskey=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

View File

@ -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, -- issuerOP 域名
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,

View File

@ -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