一、为什么需要单点登录

想象一下,你每天上班要登录十几个系统,每个系统都要输入账号密码。光是记住这些密码就够头疼了,更别提频繁切换带来的效率问题。这就是单点登录(SSO)要解决的问题 - 一次登录,处处通行。

在企业环境中,通常会有多个内部系统:OA、CRM、ERP等等。如果每个系统都维护自己的账号体系,不仅管理麻烦,用户体验也很差。SSO就像一把万能钥匙,解决了这个痛点。

二、OpenResty是什么

OpenResty不是个新东西,它本质上是在Nginx基础上打了个"增强补丁"。通过内置LuaJIT引擎,让我们可以用Lua脚本扩展Nginx的功能。就像给普通汽车装上赛车引擎,外表还是那个Nginx,但性能和处理能力完全不一样了。

特别适合做SSO是因为:

  1. 高性能 - 轻松应对高并发认证请求
  2. 灵活性 - Lua脚本可以自定义各种认证逻辑
  3. 轻量级 - 相比传统Java方案更节省资源

三、SSO的核心实现方案

3.1 基于Cookie的方案

最直观的做法是利用浏览器Cookie。认证中心颁发令牌后,各个子系统通过校验这个令牌来判断用户身份。

-- OpenResty Lua示例:校验Cookie中的token
location /protected {
    access_by_lua_block {
        local token = ngx.var.cookie_AuthToken
        if not token then
            ngx.redirect("/sso/login?return="..ngx.var.request_uri)
        end
        
        -- 调用认证中心验证token有效性
        local res = ngx.location.capture("/auth/verify?token="..token)
        if res.status ~= 200 then
            ngx.redirect("/sso/login")
        end
        
        -- 验证通过,设置用户信息到header
        ngx.req.set_header("X-User-Id", res.body.user_id)
    }
}

3.2 基于JWT的方案

JWT(JSON Web Token)是更现代的方案。令牌本身包含用户信息,避免了频繁查询用户数据库。

-- OpenResty Lua示例:JWT验证
local jwt = require "resty.jwt"

location /api {
    access_by_lua_block {
        local auth_header = ngx.var.http_Authorization
        if not auth_header then
            return ngx.exit(401)
        end
        
        -- 提取Bearer token
        local _, _, token = string.find(auth_header, "Bearer%s+(.+)")
        if not token then
            return ngx.exit(401)
        end
        
        -- 验证JWT签名
        local jwt_obj = jwt:verify("your-secret-key", token)
        if not jwt_obj.verified then
            return ngx.exit(403)
        end
        
        -- 检查过期时间
        if jwt_obj.payload.exp < os.time() then
            return ngx.exit(401)
        end
    }
}

四、完整实现示例

让我们看一个完整的SSO实现,包含认证中心和子系统集成。

4.1 认证中心实现

-- OpenResty Lua示例:认证中心核心逻辑
location /sso/login {
    content_by_lua_block {
        -- 检查是否已登录
        local token = ngx.var.cookie_AuthToken
        if token then
            local res = ngx.location.capture("/auth/verify?token="..token)
            if res.status == 200 then
                local return_url = ngx.var.arg_return or "/"
                return ngx.redirect(return_url)
            end
        end
        
        -- 显示登录页
        ngx.say([[
            <form action="/sso/auth" method="post">
                <input type="text" name="username">
                <input type="password" name="password">
                <input type="hidden" name="return" value="]]..(ngx.var.arg_return or "")..[[">
                <button type="submit">登录</button>
            </form>
        ]])
    }
}

location = /sso/auth {
    content_by_lua_block {
        -- 验证用户名密码
        ngx.req.read_body()
        local args = ngx.req.get_post_args()
        
        -- 这里应该是数据库验证,简化为示例
        if args.username ~= "admin" or args.password ~= "123456" then
            return ngx.redirect("/sso/login?error=1")
        end
        
        -- 生成token
        local token = ngx.md5(args.username .. os.time() .. math.random(1000))
        
        -- 存储token到Redis
        local redis = require "resty.redis"
        local red = redis:new()
        red:set("sso:token:"..token, args.username)
        red:expire("sso:token:"..token, 3600) -- 1小时过期
        
        -- 设置Cookie并跳转
        ngx.header["Set-Cookie"] = "AuthToken="..token.."; Path=/; HttpOnly"
        return ngx.redirect(args.return or "/")
    }
}

4.2 子系统集成

子系统只需要在Nginx配置中添加前置验证:

location / {
    access_by_lua_block {
        local token = ngx.var.cookie_AuthToken
        if not token then
            local return_url = ngx.var.scheme.."://"..ngx.var.host..ngx.var.request_uri
            return ngx.redirect("https://sso.yourdomain.com/login?return="..ngx.escape_uri(return_url))
        end
        
        -- 验证token有效性
        local http = require "resty.http"
        local httpc = http.new()
        local res, err = httpc:request_uri("https://sso.yourdomain.com/auth/verify", {
            method = "GET",
            query = {token = token}
        })
        
        if not res or res.status ~= 200 then
            local return_url = ngx.var.scheme.."://"..ngx.var.host..ngx.var.request_uri
            return ngx.redirect("https://sso.yourdomain.com/login?return="..ngx.escape_uri(return_url))
        end
    }
    
    proxy_pass http://your_backend;
}

五、技术优缺点分析

5.1 优势

  1. 性能卓越:OpenResty基于Nginx,轻松应对上万并发
  2. 开发高效:Lua脚本比传统Java开发快得多
  3. 资源节省:单服务器可同时承担反向代理和认证功能
  4. 灵活扩展:可以方便集成各种认证方式(LDAP、OAuth等)

5.2 局限性

  1. 学习曲线:需要同时了解Nginx和Lua
  2. 调试困难:Lua的错误提示不如Java等语言友好
  3. 生态局限:某些企业级功能需要自行实现

六、注意事项

  1. 安全性:一定要使用HTTPS,Cookie设置HttpOnly和Secure标志
  2. 会话管理:实现完善的token过期和续期机制
  3. 跨域问题:如果子系统在不同域名,需要考虑CORS方案
  4. 性能监控:对认证接口要做好监控和限流
  5. 灾备方案:Redis宕机时的降级处理策略

七、应用场景推荐

这种方案特别适合:

  • 企业内部多个系统统一认证
  • 微服务架构的API网关层认证
  • 需要高并发的Web应用认证
  • 快速实现原型验证的场景

不适合:

  • 需要复杂权限管理的系统
  • 已有成熟IAM系统的企业
  • 移动端App的认证场景

八、总结

通过OpenResty实现SSO,我们获得了一个高性能、灵活的解决方案。虽然需要掌握一些Lua和Nginx知识,但投入产出比非常高。对于中小型项目,这可能是最简单快速的SSO实现方案。

关键点回顾:

  1. 认证中心负责颁发和验证token
  2. 子系统通过拦截请求实现无感认证
  3. Redis存储会话状态保证高性能
  4. 完善的错误处理和跳转逻辑提升用户体验

随着业务发展,可以在此基础上逐步添加多因素认证、风险控制等高级功能,构建更完善的安全体系。