This article mainly introduces the method of using LUA to write the authentication module of the Nginx server, such as the popular social application access and other functions, the need of friends can refer to the
Over the past two days, I have solved a very interesting problem. I use a nginx server as a proxy, you need to be able to add a certification layer to enable it to use external authentication sources (such as a Web application) to authenticate, if the user has an account in the external authentication source, you can pass the certification agent.
Demand List
I have considered several solutions, listed below:
Use a simple Python/flask module to do the proxy and verify.
A nginx module that uses subrequests for verification (Nginx can do this now)
Writing a Nginxren authentication module with LUA
Obviously, adding additional requests to the entire system will not do very well, as it will increase latency (especially if adding a request to every paging file is annoying). This means that we have excluded the Subrequest module. Python/flash solution seems to support the nginx is not good, so we also exclude it. There is only LUA, of course, nginx to the original biochemical support is good.
Because I don't want to be authenticated on every request on a server that expands, I decided to generate some tokens so that people could save it and present it to the server, and then the server would let the request pass. However, because the LUA module does not have a way of maintaining state (which I have found), we cannot store the cards anywhere. When you don't have more memory, how do you verify what the user is saying?
Solve the problem
The way to encrypt your signature is our Savior! We can use the user's username and expiration time data to add the signed cookies to the user, so it is easy to verify who each user is, and we do not need to have a token.
In Nginx, all we have to do is to configure Access_by_lua_file/our/file.lua directly at the specified location so that the specified location will protect our script. Now, let's write the code together:
The code is as follows:
--Some variable declarations.
Local cookie = Ngx.var.cookie_MyToken
Local HMAC = ""
Local timestamp = ""
Local Timestamp_time = 0
--Check that cookie exists.
if cookie = = Nil or Cookie:find (":") = = Nil Then
--internally rewrite the URL so we serve
--/auth/if there ' s no cookie.
Ngx.exec ("/auth/")
Else
--If There ' s a cookie, split off the HMAC signature
--and timestamp.
Local divider = Cookie:find (":")
HMAC = Cookie:sub (divider+1)
timestamp = cookie:sub (0, Divider-1)
End
--Verify that's the signature is valid.
If HMAC_SHA1 ("some very secret string", timestamp) ~= HMAC or Tonumber (timestamp) < Os.time () then
--If invalid, send To/auth/again.
Ngx.exec ("/auth/")
End
The code above can be run directly. We use some clear text to sign (in this case is a timestamp, of course you can use whatever you want), then we use Milvensen into HMAC (hash information authentication code), and then a signature is generated, so that users can not tamper with invalid information.
When the user tries to load a resource, we check that the signature in the cookie is valid and, if so, pass his request. Instead, we redirect them to a password-issuing server that verifies and gives them a signed password in case they do not.
Sharp you may find that the above code has a time leak. If you don't find it, don't be sad. Well, it might be a little sad.
Here's a LUA code that compares the equivalent of two strings at a constant time (and thus can stop any attack at any time unless I ignore something that is most likely):
Copy code code as follows:
function Compare_strings (str1, STR2)
--Constant-time string comparison function.
Local same = True
For i = 1, #str1 do
--If the two strings ' lengths are different, sub ()
---would just return nil for the remaining length.
C1 = Str1:sub (i,i)
C2 = str2:sub (i,i)
If C1 ~= C2 Then
Same = False
End
End
Return same
End
I've applied the time on the function to differentiate, as I know, this is an equivalent string at constant time. Strings of different lengths change slightly in time, perhaps because the Sub procedure Sub applies a different branch. Moreover, the C1~=C2 branch is obviously not a constant time, but in practice it is fairly close to constant, so our example will not have an impact. I prefer to use an XOR operation to determine whether the XOR result of two strings is 0, but Lua does not seem to include bits XOR operations. If I am wrong in this judgment, I am grateful for any correction.
Password Publishing Server
Now, we've written some great password-checking code, all we need to do is write a server to actually issue these passwords. I could have used Python and flask to write this server, but I still wanted to try it with go because I was a computer language boomer and the go looked "cool". It might be quicker to use python, but I'm happy to use go.
This server will pop up an HTTP Basic authentication form, check the account you entered, and if it is correct, it will give you a signed password that is suitable for one hours of proxy server access. In this way, you only need to verify the external service once, and subsequent authentication checks will be at the nginx level, and will be fairly fast.
Request processor
Writing a processor to eject a basic validation form is not difficult, but go has no perfect documentation, so I have to hunt myself a little. In fact, it's very simple, and finally, here is the HTTP Basic authentication Go code:
The code is as follows:
Func Handler (w http. Responsewriter, R *http. Request) {
If username: = Checkauth (R); Username = = "" {
W.header (). Set ("Www-authenticate", "Basic realm=" the Kingdom of Stavros "")
W.writeheader (401)
W.write ([]byte ("401 Unauthorizedn"))
} else {
Fmt. Printf ("Authenticated user%V.N", username)
Token: = GetToken ()
Settokencookie (W, token)
Fmt. fprintf (W, "")
}
}
Setting passwords and Cookies
Once we have verified a user, we need to set a cookie for their password. We just need to do the same thing we did with Lua, as above, just simpler, because go has a real encryption package inside the standard library. This code is as straightforward as it is, even if it is not fully documented:
The code is as follows:
Func GetToken () string {
Expiration: = Int (time. Now (). Unix ()) + 3600
Mac: = HMAC. New (SHA1. New, []byte ("some very secret string")
Mac. Write ([]byte (FMT). Sprintf ("%v", expiration))
Expectedmac: = Fmt. Sprintf ("%x", Mac.) Sum (nil))
Return FMT. Sprintf ("%v:%s", expiration, Expectedmac)
}
Func Settokencookie (w http. Responsewriter, token string) {
Rawcookie: = Fmt. Sprintf ("mytoken=%s", token)
Expire: = time. Now (). ADD (time. Hour)
Cookie: = http. cookie{"MyToken",
Token
"/",
". example.com",
Expire
Expire. Format (time. Unixdate),
3600,
False
True
Rawcookie,
[]string{rawcookie}}
http. Setcookie (W, &cookie)
}
Try to put them together
To complete this great segment of our portfolio, we only need a function to check the authentication information provided by the user, and we did it! Here is the code I draw from some libraries, currently it just checks a specific username/password combination, So integration with third party services is a job for the reader:
The code is as follows:
Func Checkauth (R *http. Request) String {
S: = Strings. SPLITN (R.header.get ("Authorization"), "", 2)
If Len (s)!= 2 | | S[0]!= "Basic" {
Return ""
}
B, err: = base64. Stdencoding.decodestring (S[1])
If Err!= nil {
Return ""
}
Pair: = strings. SPLITN (string (b), ":", 2)
If Len (pair)!= 2 {
Return ""
}
If pair[0]!= "username" | | PAIR[1]!= "password" {
Return ""
}
return pair[0]
}
Conclusion
I'm still quite fond of Nginx's LUA modules. It allows you to do some simple work on the Web server's request/response cycle, and it makes sense for some operations, such as checking the proxy server for validation. These things have been difficult for a Web server that is not programmable, so it is very likely that we will need to write our own HTTP proxy service.
The code above is fairly short and elegant, so I'm happy with everything above. I'm not sure how much extra time it adds to the response, but it's good to do a validation and I think it's worth it (and it should be fast enough, so it's not a problem).
Another advantage is that you can only open it with a single directive in Nginxlocationblock, so there is no configuration item to keep track of. I found that, overall, it was a very elegant solution, and I'm happy to learn that nginx can make me do something like this, maybe I need to do it in the future.