## Basic Auth

Nginx 的 basic auth , 基于 ngx_http_auth_basic_module 模块。该模块是 builtin 模块,就是你安装 Nginx 的时候,它就一起装好了。

Basic Auth ,是一种简单的鉴权方式,不怎么安全。一般的 Basic Auth header 长这样:

Authorization: Basic $(base64_encode(username:password))

客户端通过对 username 和 password 进行 base64 加密, 放入 header 中。服务端获取 header 中的 Authorization , 然后 decode 鉴权。

在 nginx 中,通过 auth_basic 和 auth_basic_user_file 进行配置,该语法支持的上下文有

http, server, location, limit_except

具体配置如下:

server { listen 8088; location / { auth_basic "closed site"; auth_basic_user_file conf/htpasswd; root /xxx/html; index index.html index.htm; } error_page 404 /404.html; access_log logs/blog.access.log; }

关于 auth_basic 后面的字符串,要看各大浏览器是不是给面子了。我用 chrome 就不会显示这个,用 edge 会显示。

auth_basic_user_file 是用来存储账号密码的。官方支持使用 "HTTP Basic Authentication" 协议来验证用户名和密码。

emmm...

现在我们通过 htpasswd 生成一个:

# 直接在控制带输出 htpasswd -nbd iyuhp admin # 输出到文本 htpasswd -bdc passwd iyuhp admin # 再次追加到文本 htpasswd -nbd iyuhp admin | tee -a passwd

这个时候,优雅的重启一波 nginx:

sudo nginx -s reload

然后访问一波, 发现已经会弹出一个用来登录的弹窗了。

如果通过命令行访问,可以直接通过 username:password@url访问:

curl 'localhost:8088' --header 'Authorization: Basic aXl1aHA6YWRtaW4=' // or curl iyuhp:admin@localhost:8088


Auth Request

可能我有一个后端程序,所以我完全可以把这些 "繁重" 的用户密码管理,交给后台就好了。

哦,我并不是说搞个程序来生成账号密码,然后写到 auth_basic_user_file 文件中。我的意思是,这个校验的工作,让我来吧,Nginx 你去搞别的去!

这样做的好处是, 我可以方便的管理我的账号体系(这里所谓的账号体系,是一个夸张的说法,你一个小小的 blog ,还账号体系...)

emmm... 不管怎样,这看起来是件有趣的事情。

Nginx 自是能考虑到这些,已然提供了一个语法 auth_request , 意思是,你如果要访问这里,可以,得先通过我的验证才行。

auth_request uri | off; # off 表示关闭上文的验证,就是我这里我说了算,上级不要说话了 # 该语法支持的上下文为: http, server, location

那么我们要怎么搞呢? 别急, 我先整张图:

登录和鉴权(给Blog加上鉴权)(1)

  1. client 向 Nginx 请求资源, 因为配置了 auth_request , Nginx 会先先 /auth 请求权限
  2. /auth 将请求转发给 Backend
  3. Backend 懵了, 你啥都没有,请求啥? 麻溜的返回 401 。注意 , 这里必须返回 401 (403 应该也可以,没有试... ),返回之前, 需要添加 http header:Header().Add("WWW-Authenticate", "Basic realm=\"up to you\"")
  4. 第四步, 第五步,第六步,则是将最终的结果反馈给 client。 client 收到 Basic Auth , 弹出弹窗,让你填用户名密码。
  5. 于是 client 这次携带 basic auth 再次请求 Nginx , 同样, 该请求最终被 Backend 处理。 Backend 一看,呦,原来是哥们啊, 得, 给你个 http code 200 吧。
  6. location /auth 一看, http code 200 , 赶紧缓存起来。 proxy_cache_valid 200 2h; 的意思就是对 http code 为 200 的请求,缓存 2h 。
  7. location / 一看,行, 你小子通过 auth 验证了, 这次的资源请求就放行吧。

通过上述步骤, 我们成功通过和 Backend 交互,并获得了指定资源的访问权。

关于 auth_request 的更多内容请看 这里 。 附上一份配置:

location / { auth_request /auth; root /xxx/html } location /auth { proxy_pass http://localhost:1234/auth; # 处理 auth 的后端 URL # 关闭不必要的数据传递 proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; # proxy cache 设置 proxy_cache auth_cache; proxy_cache_valid 200 2h; proxy_cache_key $host$request_uri$cookie_session; }

使用 proxy_cache 前,需要先定义下 proxy_cache_path , 该语法的上下文是 http ,也就说,你需要在 http 层先定义该值, 比如:

proxy_cache_path path levels=1:2 keys_zone=one:10m;


WeChat

我现在又觉得, 搞啥账号体系,我一个小小的 blog , 太耗时间了!

于是你想到了用验证码来搞好了。

那自然而然的想到了微信,想到利用公众号, 来搞个自动获取验证码呗。整个流程如下:

登录和鉴权(给Blog加上鉴权)(2)

  1. client 发起一个需要鉴权的资源请求
  2. Nginx 通过 auth_request 配置,转发给 Server
  3. Server 告诉 Nginx , 需要重定向到 login 界面获取验证码,然后再提交
  4. Nginx 告诉 client ,同时 client 展示 login 界面
  5. client 通过扫码关注公众号,输入预设指令(如: yzm 等) ,获取验证码
  6. 微信把获取验证码的请求转发给 server , server 生成并存储该验证码(设置验证码失效时间等),同时发送给微信
  7. client 获取验证码, 填写提交,server 验证通过, 告知 Nginx 放行

下面再来看下更详细的流程图:

登录和鉴权(给Blog加上鉴权)(3)

这张图就很详细了,这里主要说几个注意的点:

贴下具体的配置:

location / { auth_request /auth; error_page 401 =200 /login; root /xxx/html; } location /auth { proxy_pass http://localhost:1234/auth; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; proxy_cache auth_cache; proxy_cache_valid 200 2h; # proxy key, 参见 reference [3] proxy_cache_key $host$request_uri$cookie_session; # 如果有 session 丢失的问题, 可以参考这条配置 # proxy_cookie_path ~*^/.* /; } location /login { proxy_pass http://localhost:1234/login; proxy_set_header X-Original-URI $request_uri; }

WeChat 对接

关于 测试账号 的东西,我也了解的不多,因为我只对接了一个 。

登录微信公众号后, [开发] - [基本配置] - [服务器配置] ,配置服务器地址、令牌、消息加解密密钥(optional)

提交时, 微信会先发一个验证请求,看看你的 server 是不是好的:

GET /handle?signature=xxx&echostr=123455×tamp=123456&nonce=1234

验证部分的文档, 在 这里 。主要步骤是:

# golang 部分代码 ary := []string(token, timestamp, nonce) sort.Strings(ary) encryptor := sha1.New() io.WriteString(encryptor, strings.Join(ary,"")) mySignature := fmt.Sprintf("%x", encryptor.Sum(nil)) // compare

文档说的是, 如果一致,就把 URI 中 echostr 原样返回。

嗯, 我返回了, 你特么说我验证失败, Token 有问题啥的???

为啥, 因为我返回了一个 string 类型的字符串! F**k , 这导致微信接收的时候, 在原值上多了一对双引号。

哎, 我最终返回了一个 int 类型的 echostr ,虽然我不知道这个 echostr 是不是会存在 str ...


这一步完成后, 当你通过公众号发送消息时, 微信就会把这个消息转发给你的 server 。它的格式如下:

<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <FromUserName><![CDATA[fromUser]]></FromUserName> <CreateTime>1348831860</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[this is a test]]></Content> <MsgId>1234567890123456</MsgId> </xml>


这就是在 server 端定义几个结构体,解析下就好了:

type CdataString struct { Value string `xml:",cdata"` } ​ type MsgXml struct { XMLName xml.Name `xml:"xml"` ToUserName CdataString `xml:"ToUserName"` FromUserName CdataString `xml:"FromUserName"` CreateTime int64 `xml:"CreateTime"` MsgType CdataString `xml:"MsgType"` Content CdataString `xml:"Content"` MsgId int64 `xml:"MsgId,omitempty"` }

需要注意三个点:

<xml> <ToUserName><![CDATA[toUser]]></ToUserName> <Encrypt> <![CDATA[QC1jqzkTmPbaCkcB7ruVHe6k=...]]> </Encrypt> </xml>

你需要用你的 EncodingAESKey 先解密才行, 这里 是微信的文档, 不再详述。至此, 你就可以愉快的通过微信公众号,来玩转你的 blog 了。是不是很有趣? 可怜我在五一的美好假期里,熬了一个通宵搞这东西...

Referenct

[1]

请参考 proxy_cache_path

[2]

请参考 这里

开始通过 POST /login 进行重定向时, 总是用 POST 方法调用 Location 的地址,导致 Method Not Allowed. 此时使用的 http code 是 307,后来用 302 , 最后改为 303 搞定

[3]

对于使用哪种变量作为 proxy_cache 的 key ,我想了很久。

最开始用一堆诸如 $host$remote_addr$request_uri , 后来发现一个问题,对于同一个局域网下的用户, 只要 $request_uri 一致, 那这个 cache 就是公用的, 这明显不科学啊!

后来 google 了一把, 决定用 session 作为 proxy_cache 的 key 。这要求你在鉴权的时候,在 cookie 中写入你的 session,然后通过 $cookie_session 获取即可。

当然,你可以往 cookie 里写 anything else ,如 wtf ,然后你就可以用 $cookie_wtf 作为你的 proxy key 了, 完美。

所以, 这里的 cookie_xxx , 值得是 client 的端 cookie 中的一个 key 。

,