
在这个小教程的上篇,我们搭了一个非常简单的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_id
,redirect_url
和 scope
。假设在GitHub里注册App后拿到的Client ID和Client Secret分别叫 client_id
和 client_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的文档,玩一玩这个小封装,我们下篇再见。
网友评论