美文网首页Nginx高端成长之路Lua程序员
一个OpenResty里OAuth 2认证的轮子(中)

一个OpenResty里OAuth 2认证的轮子(中)

作者: 槑菜干超人 | 来源:发表于2017-06-30 19:15 被阅读565次
精巧的Lua

在这个小教程的上篇,我们搭了一个非常简单的Docker容器,并把OpenResty跑起来了。在中篇我们来开始探索一下OpenResty的一些用法。作为一个Python程序员,我可能会不时地拿出Flask来做对比。当然,first things first,最重要的还是祭出OpenResty官方文档

首先我们在之前的 nginx.conf 配置里,看到了这么一句 ngx.say(...),这就相关于往请求方发信息,有点Flask里的返回一个Response对象的意思。不过这里并没有那么多面向对象设计的条条框框,一个请求结束,甭管是JSON还是HTML,都是返回一串东西。

我想大家多少都有玩过一些Nginx的基本配置,知道大概怎么写location block,Apache也是差不多一个路数。我们现在就来写这个Demo服务的登陆接口。这个接口的行为就是,在被访问的时候,返回OAuth平台对应的跳转登陆网址,所以会是一个30X的跳转请求。通过查询文档,我们看到对应的OpenResty API是 ngx.redirect。再通过查询GitHub的登陆说明,取code的API在https://github.com/login/oauth/authorize,必填的参数为 client_idredirect_urlscope。假设在GitHub里注册App后拿到的Client ID和Client Secret分别叫 client_idclient_secret,那我们的这个新location block就可以这么写:

location /auth/github/login {
  content_by_lua_block {
    local params = ngx.encode_args({
      client_id = client_id,
      redirect_uri = 'http://test.myoauth.com/auth-cb/github/login',
      scope = 'user',
    })
    local url = 'https://github.com/login/oauth/authorize?' .. params
    ngx.log(ngx.DEBUG, url)
    return ngx.redirect(url)
  }
}

稍微解释一下两个地方:ngx.encode_arg 会把一个Lua的table进行URL路径转义,变成请求参数字符串;ngx.log 经过我们一开始的配置,日志等级为DEBUG,统一写到 resty/logs/error.log 这个文件里。现在请干掉容器进程再up一遍,然后访问 http://localhost/auth/github/login

$ curl -I http://localhost/auth/github/login
HTTP/1.1 302 Moved Temporarily
Server: openresty/1.11.2.3
Date: Fri, 30 Jun 2017 06:39:29 GMT
Content-Type: text/html
Content-Length: 167
Connection: keep-alive
Location: https://github.com/login/oauth/authorize?redirect_uri=http%3A%2F%2Ftest.myoauth.com%2Fauth-callback%2Flogin%2Fcallback&scope=user

浏览器里打开就会被跳转到GitHub的登录页。登录以后跳转回来说找不到DNS?当然了,那个回调URL是我乱写的,去host文件里加成127.0.0.1就会连到本机,就会出一个404咯。好啦,接下来的东西就是自己读文档啦!

……

逗你的,不过应该通过这几个入门例子大家已经知道怎么搜文档找API了。

现在我们已经可以跳转到OAuth平台,让用户在平台上登陆,然后平台会重新把用户导回来,并带上一串 &code=blahblahblah。这个 code 就是OAuth登陆的第一步,获取认证码;我们接下来会用认证码来换取令牌符;最后再用令牌符去获得用户信息。

一个很自然的问题就是,接下来的步骤这么复杂,难道都直接把Lua代码强行写在这个Nginx配置里?有没有办法把模块分出来?

答案是肯定的,不过我要配置一个指令,在Nginx配置的http block里加上一句 lua_package_path '$prefix/lua/?.lua;;';$prefix 代表Nginx的启动位置,就是我们的 resty 文件夹。这样我们就可以再创建一个 resty/lua 文件夹,把我们的Lua代码放在里面,再从Nginx的配置里引用到它们了。

另一个问题,每次修改代码和配置,现在都需要重启一下容器来重启OpenResty,有没有在开发阶段省事一些的方法?答案也是肯定的。如果把代码写到外部的Lua脚本里再引用进来的话,可以同样在http block里加上这一句 lua_code_cache off;,就可以让每个请求独立运行一遍Lua脚本。默认的行为是在一次运行之后,OpenResty会把Lua脚本Cache住,LuaJIT也会做对应的JIT编译优化,来保证线上服务的性能。但是我们在开发阶段需要的是代码热重载,就把这个脚本缓存先关掉了。

现在我们有了一个代码模块化的机制,优先考虑的当然是创建一个OAuth相关的模块:

-- resty/lua/oauth.lua

local cjson = require('cjson.safe')
cjson.encode_empty_table_as_object(false)

local M = {}
local _conf = nil

local function code_url()
  local params = ngx.encode_args({
    client_id = _conf.client_id,
    redirect_uri = _conf.redirect_uri,
    scope = _conf.scope,
  })
  return _conf.code_endpoint .. '?' .. params
end

function M.get_code()
  return ngx.redirect(code_url())
end

function M.get_profile(code)
  ngx.say(cjson.encode({ code = code })
end

function M.init(conf)
  _conf = conf
end

return M

用一个 M 元表来装住所有暴露出去的接口是Lua常用的一种模块定义方式。cjson 是OpenResty提供的一个JSON工具库。这里把OAuth平台跳转回来的回调函数也定义好了,虽然什么都没做,只是把code原样返回。现在我们要在Nginx配置里找一个合适的时候初始化这个模块,在对应的请求到来的时候调用它们:

http {
  include /usr/local/openresty/nginx/conf/mime.types;
  lua_package_path '$prefix/lua/?.lua;;';
  lua_code_cache off;

  # 就是这里了,init 的时候也 init 我们的模块
  init_by_lua_block {
    require('oauth').init({
      code_endpoint = 'https://github.com/login/oauth/authorize',
      token_endpoint = 'https://github.com/login/oauth/access_token',
      profile_endpoint = 'https://api.github.com/user',
      client_id = 'client_id',
      client_secret = 'client_secret',
      redirect_uri = 'http://test.myoauth.com/auth-cb/github/login',
      scope = 'user',
    })
  }

  server {
    listen 80;

    location / {
      content_by_lua_block {
        ngx.say('hello world')
      }
    }

    location /auth/github/login {
      content_by_lua_block {
        return require('oauth').get_code()
      }
    }

    location /auth-cb/github/login {
      content_by_lua_block {
        return require('oauth').get_profile(ngx.var.arg_code)
      }
    }
  }
}

接下来我们要面对的问题就比较复杂了:拿到临时的谁码以后,我们需要在服务器里发两个请求,一个用来换取令牌符,一个用来查询用户信息。这时我们就会需要之前Dockerfile里最下面那行用OPM安装的resty-http库来发起服务器端的网络请求。OpenResty的库文档和就是代码仓库里的README,大家可以直接去项目主页围观。不过我习惯对这个库做一个浅封装来统一我的错误处理:

-- resty/lua/requests.lua

local http = require('resty.http')
local cjson = require('cjson.safe')
cjson.encode_empty_table_as_object(false)

local errors = {
  UNAVAILABLE = 'upstream-unavailable',
  QUERY_ERROR = 'query-failed'
}

local M = { errors = errors }

local function request(method)
  return function(url, payload, headers)
    headers = headers or {}
    headers['Content-Type'] = 'application/json'
    local httpc = http.new()
    local params = { headers = headers, method = method }
    if method == 'GET' then params.query = payload
    else params.body = payload end
    local res, err = httpc:request_uri(url, params)
    if err then
      ngx.log(ngx.ERR, table.concat(
        {method .. ' fail', url, payload}, '|'
      ))
      return nil, nil, errors.UNAVAILABLE
    else
      if res.status >= 400 then
        ngx.log(ngx.ERR, table.concat({
          method .. ' fail code', url, res.status, res.body,
        }, '|'))
        return res.status, res.body, errors.QUERY_ERROR
      else
        return res.status, res.body, nil
      end
    end
  end
end

M.jget = request('GET')
M.jput = request('PUT')
M.jpost = request('POST')

return M

上面这段脚本用了一个函数式编程里很常用的模式叫柯里化。Lua的精巧就在于它用类C的语法实现了Scheme里最核心的函数式思想,和ES5一样,提供了一个很好的语言核心;但胜过ES5的地方又在于它很早就支持了协程,让它能如丝般顺滑地被集成到Nginx的异步事件循环中……

啊扯远了,我把这个封装过的工具库叫 requests.lua(简直不要脸——用Python的童鞋轻喷)。它也是我们在下篇里会继续扩展的 oauth.lua 里要调用的一个基础库。大家可以先尝试着读一读lua-resty-http的文档,玩一玩这个小封装,我们下篇再见。

相关文章

网友评论

    本文标题:一个OpenResty里OAuth 2认证的轮子(中)

    本文链接:https://www.haomeiwen.com/subject/biobcxtx.html