龙空技术网

给 Blog 加上鉴权 by using wechat

iyuhp 154

前言:

眼前我们对“nginx 鉴权”大约比较重视,看官们都需要分析一些“nginx 鉴权”的相关知识。那么小编在网上搜集了一些有关“nginx 鉴权””的相关文章,希望各位老铁们能喜欢,看官们快快来了解一下吧!

## 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...

支持通过 crypt() 方法加密: 通过 htpasswd 或者 openssl passwd命令apache 基于 MD5 的 apr1 加密方式基于 RFC 2307 的实现

现在我们通过 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='// orcurl iyuhp:admin@localhost:8088
Auth Request

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

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

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

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

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

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

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

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

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

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

location / {    auth_request        /auth;    root                /xxx/html}location /auth {    proxy_pass ; # 处理 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;
path : 指定 cache 缓存的位置levels[1] : 指定缓存的层级, 上面的配置可能最终长这样: path/a/bc/xxxbcakeys_zone : 相当于这个 cache 的 alias , 使用时, 就用这个名称。 10m 即缓存的大小。WeChat

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

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

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

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

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

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

3 - 4 步, 这里,返回 401 后,需要在 Nginx 中定义 error_page:error_page 401 =200 /login;

# 所以也可以这么做, 返回 403

error_page 401, 403 =200 /login;这样, 你返回 401 或者 403 http code 后, Nginx 就帮你重定向到了 /login4 - 5 步,这里, 你需要把原始的 URI 交给 server 记录下来,用于登录成功后的跳转:location /login {

proxy_pass ;

proxy_set_header X-Original-URI $request_uri; # 原始的 URI 请求, 交由 server 记录

}

​9 - 10 步, server 端需要记录下自己生成的 code ,之后验证 code 需要12 - 14 步, 这里 client 请求的,其实是另一个 URI (如 POST /login), 这个 URI 是可以公开访问,不需要鉴权的15 步, 这里要注意, 鉴权成功后,要设置 header ,添加 Location URI , 告诉浏览器要跳转。这里有个坑 [2] ,返回的 http code 需要注意。16 - 22 , 事实上就是前面介绍的两个部分的内容,不再赘述

贴下具体的配置:

location / {    auth_request        /auth;    error_page          401 =200 /login;    root                /xxx/html;}location /auth {    proxy_pass ;    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 ;    proxy_set_header X-Original-URI $request_uri;}

WeChat 对接

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

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

URL 需要是一个公网 ip 或者域名Token 你可以随意填写,只要你 server 端验证的 Token 和此处填写一致, 就好了。

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

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

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

token, timestamp, nonce 字典排序并拼接成一个字符串sha1 加密与 signature 比较是否一致

# 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 的 root tag 需要是 xml几乎所有的字段,都需要用 cdata 格式包裹如果你在上面选择了对消息加密,则给你的数据包,可能是长这样的:

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

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

Referenct[1]

请参考 proxy_cache_path

[2]

请参考 这里

301 :http 1.0 ,永久移除, POST --> GET302 : http 1.0 ,暂时移除, POST -> POST (我用的 chrome 会如此)303 : see other , POST -> GET307 : 临时重定向, POST -> POST308 : 永久重定向, POST -> POST

开始通过 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 。

标签: #nginx 鉴权