Column
❈
Tea guest furu , Python Chinese community columnist
Blog:
http://www.jianshu.com/p/537b356d34c9
❈
Today's article introduces an API authentication method suitable for restful+json. This method is based on jwt and adds some improvements borrowed from oauth2.0.
First of all, we must understand that authentication and authentication are different. Authentication is to determine the legitimacy of a user, and authentication is to determine whether the user's authority level can perform subsequent operations. What I'm talking about here only includes certification. There are several ways to authenticate:
This is the basic authentication carried in the http protocol, which is a simple authentication method. The principle is to add a string of username and password (formatted as "username:password", encoded in base64) in the header of each request.
This method is equivalent to binding "username:password" as an open certificate. There are several problems: ①Every request requires a username and password. If the connection does not use SSL/TLS, or the encryption is cracked , The user name and password are basically exposed; ②The user’s login status cannot be cancelled; ③The certificate will not expire unless the password is changed.
Generally speaking, the characteristic of this method is that it is simple but not safe.
Store the authentication result in the client's cookie, and check the identity information in the cookie as the authentication result. This method is characterized by its convenience, requires only one authentication, and can be used multiple times; you can also log out and set the expiration time; there are even ways (such as setting httpOnly) to avoid XSS attacks. But its shortcomings are very obvious. The use of cookies is a stateful service.
The JWT protocol seems to have been widely used, JSON Web Token-a token-based json format web authentication method. The basic principle is that the server issues a token in json format when the user name and password are passed for the first authentication. Subsequent client requests carry this token, and the server only needs to parse this token to determine the identity and legitimacy of the client. The JWT protocol only specifies the format of this protocol ( RFC7519 ), and its sequence generation method is described in the JWS protocol ( https://tools.ietf.org/html/rfc7515), which is divided into three parts:
①Declare the type, here is jwt
②Declare that the encryption algorithm usually directly uses HMAC SHA256, a common header is this:
{'typ':'JWT','alg':'HS256'}
Then base64 encode it.
The payload is where the actual effective usage information is placed. JWT defines several things, including:
① Statements registered in the standard, such as issuer, recipient, effective time (exp), time stamp (iat, issued at), etc.; it is an official recommendation but not required;
②Public statement;
③Private declaration;
A common payload is this:
{'user_id': 123456,'user_role': admin,'iat': 1467255177}
In fact, the content in the payload is free, and you can add it according to your own development needs. Ps. There is a small problem. The result of token sequence generation using TimedJSONWebSignatureSerializer of itsdangerous package, exp is in the header. This seems to violate the rules of the jwt agreement.
The serialized secreate key and salt key are stored. This part requires the base64-encrypted header and base64-encrypted payload to use the string composed of the connection. Then the salted secret combination is encrypted through the encryption method declared in the header, and then constitutes the third part of jwt.
The target scenario is a back-end system with separated front-end and back-end for operation and maintenance. Although it is used on the intranet, it also has certain confidentiality requirements.
① The API is a stateless interface of restful+json, and the authentication is required to be the same mode
②Scalable horizontally
③Lower database pressure
④The certificate can be cancelled
⑤The certificate can be automatically extended
⑥Select JWT.
The python module itsdangerous is used here. This module can do a lot of coding work, one of which is to implement the JWS token sequence. genTokenSeq This function is used to generate tokens. Among them, TimedJSONWebSignatureSerializer is used to generate the sequence. Here, the secret_key key and the salt value are read from the configuration file, of course, it can also be written directly here. Expires_in is the timeout interval. This interval is recorded in seconds and can be set directly here. I chose to set it as the formal parameter of the method (because this function is also used to solve the problem 2 mentioned below).
# serializer for JWTfrom itsdangerous import TimedJSONWebSignatureSerializer as Serializer """ token is generated as the JWT protocol. JSON Web Tokens(JWT) are an open, industry standard RFC 7519 method """ def genTokenSeq(self, expires): s = Serializer( secret_key=app.config['SECRET_KEY'], salt=app.config['AUTH_SALT'], expires_in=expires) timestamp = time.time() return s.dumps( {'user_id': self.user_id,'user_role': self.role_id,'iat': timestamp}) # The token contains userid, user role and the token generation time. # u can add sth more inside, if needed. #'iat' means'issued at'. claimed in JWT.
Using this Serializer can help us deal with header and signature issues. We only need to use s.dumps to write the contents of the payload. Here I am going to write three values in each token: user id, user role id and current time ('iat' is one of the JWT standard registration statements).
Suppose the information I write is {"iat": 1467271277.131803, "user_id": "46501228343b11e6aaa6a45e60ed5ed5f973ba0fcf783bb8ade34c7b492d9e55", "user_role": 3}
The token generated by the above method is
eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ2NzM0MTQ3NCwiaWF0IjoxNDY3MzM3ODc0fQ.eyJpYXQiOjE0NjczMzc4NzQuNzE3MDYzLCJ1c2VyX2lkIjoiNDY1MDEyMjgzNDNiMTFlNmFhYTZhNDVlNjBlZDVlZDVmOTczYmEwZmNmNzgzYmI4YWRlMzRjN2I0OTJkOWU1NSIsInVzZXJfcm9sZSI6M30.23QD0OwLjdioKu5BgbaH2gHT2GoMz90n8VZcpvdyp7U
It is composed of "header.payload.signature".
The analysis needs to use the same serializer, configure the same secret key and salt, and use the loads method to parse the token. itsdangerous provides a variety of exception handling classes, which are also very convenient to use. If it is SignatureExpired, it can directly return to expire; if it is BadSignature, it represents all other signature errors, so it is divided into:
① The payload can be read: then the message is a message whose content has been tampered with and the message body encryption process is correct, and the secret key and salt are likely to be leaked;
②The payload cannot be read: The message body is directly tampered with, and the secret key and salt should still be safe.
The above content is written as a function to verify the user token. If implemented in python flask, you can consider changing this function to a decorator decorating paint. Put the decorator @ in front of all methods that need to verify the token, and the code can be more elegant.
# serializer for JWTfrom itsdangerous import TimedJSONWebSignatureSerializer as Serializer# exceptions for JWTfrom itsdangerous import SignatureExpired, BadSignature, BadData# Class xxx# after definition of your class, here goes the auth method: def tokenAuth(token): # token decoding s = Serializer( secret_key=api.app.config['SECRET_KEY'], salt=api.app.config['AUTH_SALT']) try: data = s.loads(token) # token decoding faild # if it happend a plenty of times, there might be someone # trying to attact your server, so it should be a warning. except SignatureExpired: msg ='token expired' app.logger.warning(msg) return [None, None, msg] except BadSignature, e: encoded_payload = e.payload if encoded_payload is not None: try: s.load_payload(encoded_payload) except BadData: # the token is tampered. msg ='token tampered' app.logger.warning(msg) return [None, None, msg] msg ='badSignature of token' app.logger.warning(msg) return [None, None, msg] except: msg ='wrong token with unknown reason' app.logger.warning(msg) return [None, None, msg] if ('user_id' not in data) or ('user_role' not in data): msg ='illegal payload inside' app.logger.warning(msg) return [None, None, msg] msg ='user(' + data['user_id'] +') logged in by token.'# app.logger.info(msg) userId = data['user_id'] roleId = data['user_role'] return [userId, roleId, msg]
The mechanism of inspection and judgment is as follows:
1. Use the encrypted class, then use it for decryption (using the previous key and salt value), and save the result into data;
2. If the SignatureExpired exception is caught, it means that the token has expired according to the expired setting in the token, and it returns'token expired';
3. If it is other BadSignature anomaly, it should be divided into: 4. If the payload is still complete, then parse the payload. If the BadData anomaly is caught, it means that the token has been tampered with and return'token tampered'; 5. If the payload is incomplete, go directly Return'badSignature of token';
6. If none of the above exceptions are correct, it can only return the unknown exception'wrong token with unknown reason';
7. Finally, if the data can be parsed normally, take out the data in the payload and verify whether there is legal information in the payload (here is the json data of the user_id and user_role keys). If the data is illegal, return'illegal payload inside' '. Once this happens, it means that the key and salt are likely to be leaked.
The above method can achieve basic JWT authentication, but there are other problems in the actual development process:
After the token is generated, it is invalidated by expire. After the issuance of the token, it is impossible to withdraw the modification. Therefore, the change of the validity period of the token is a difficult problem, which is reflected in the following two problems:
How to solve the problem of changing the validity period of the token, I have seen a lot of discussions on the Internet, mainly focusing on the following:
Here, the author draws on the third-party authentication protocol Oauth2.0 ( RFC6749 ), which takes another approach: refresh token, a token used to update the token. After the user authenticates for the first time, two tokens are issued:
In this way, two different types of tokens can be set with different validity periods. For example, access tokens are valid for only one hour, while refresh tokens can be one month. The logout of the api is achieved through the expiration of the access token (the front end can directly discard this token to log out). During the lifetime of the refresh token, you can execute the refresh token to apply for a new access token when accessing the api (the front end can save this refresh) The token and access token are updated to achieve the effect of automatic extension). Refresh token cannot be renewed any longer. After expiration, you need to log in with your username and password again.
The idea of this approach is to divide the certificate into three levels:
In this way, the validity of the certificate and the validity of the certificate are considered together. ps. As mentioned earlier, when creating a token, expire_in (recommended field of jwt, timeout interval) is used as a formal parameter of the function, in order to use this function to generate access token and refresh token, and the expire_in time of the two is different .
We made a JWT authentication module: (access token is'token' in the following code, refresh token is'rftoken' in the code)
client --user name password---> server client <--token, rftoken-- server
client --request (carrying token) --> server client <---result----server
client ---request (with token) --> server client <----msg:token expired-- server
client-request new token (carry rftoken) -> server client <-----new token----- server
client-request a new token (carry rftoken) -> server client <----msg:rftoken expired--- server
If you design a front end for this certification, you need: