When it comes to API design and security, there are two main issues that must be addressed. If not handled properly, they can pose significant threats to the security and performance of the server.
Based on my experience, API design primarily needs to consider the following two aspects:

  1. Preventing malicious API calls.
  2. Encrypting data in API communication.

Since HTTP protocol is stateless, in MVC Web development, whether it is Java Web or PHP, session/cookie is often used to identify users (if you are not familiar with session-related content, please search it yourself). However, in the development mode of front-end and back-end separation, the session/cookie mode is not suitable, especially in the APP client where it is almost impossible to use session/cookie. Therefore, the industry generally uses token-based authentication.
Let’s use the analogy of a building, building management, and tenants to understand this approach. Our system is like a building, and the administrator is responsible for managing the building and tenants. Now we have 100 tenants, and each tenant is assigned their own room. Tenants are not allowed to enter other tenants’ rooms freely. When a tenant comes to register for the first time, the building management will require the tenant to present an ID card (username and password). After verifying the ID card (validating the password), the administrator will give the tenant a key (token) that belongs to them and record the key and room number. From then on, the tenant only needs to use their own key to enter and exit their own room. However, later on, the building management felt that holding the same key for a long time was somewhat insecure. For example, if the key was cloned or lost and picked up by someone else, there would be a certain level of risk. Therefore, the building management decided to set an expiration time for each allocated key, such as 30 days. When the key expires, it can no longer open the door lock, and the tenant must get a new key from the building management.
Returning to the main text, let us introduce two API examples to illustrate the development process. Now we have two APIs:

TIPS:

  • The following process is only a rough process and does not deal with specific details. The solutions are simplified in order to illustrate the problem. You can add more elements based on the general principles.
  • The use of “post” in pseudocode does not represent data submission using the POST method. It only represents the commonly mentioned “PO” data.
  • This article only provides a general solution for API design, such as the signature mechanism. The specific signature mechanism, such as the number of data items to be signed and whether to use MD5 or SHA1, can be decided independently.

The client needs to first implement the login page based on the server’s API documentation. For example, if the documentation requires submitting data in JSON format using the POST method, the pseudocode demonstration is as follows:

1
http.post('http://host.com/api/account/login', {"account":"zhangsan", "password":"123456"})

After receiving the data, the server verifies the username and password (checks the tenant’s ID) from the database. If incorrect, an error message is returned. If correct, the server needs to generate a token (issue the key) to the user and also record which user the token represents (record which tenant the key is given to). For example, if the user’s UID is 8 and the generated token is “abcdefg”, it means that the token “abcdefg” is assigned to tenant 8 (tenant 8 holds the key “abcdefg”). The client needs to save the token (the tenant holds the key) themselves.
When a user needs to access their own order, that is, when they need to access the API http://host.com/api/order/list, they need to include the token, because the server has recorded the relationship between the token and the user UID. Therefore, the server can determine who the current user is based on the token and return their order content. The pseudocode demonstration is as follows:

1
http.post('http://host.com/api/order/list', {"token": "abcdefg"})

By doing this, the basic client-server communication has been implemented. However, there are still significant security risks. If anyone discovers the server API address through packet capture or other means, it means that they can make arbitrary API calls, and our API may be misused. To prevent this kind of misuse, a signature mechanism is introduced. In other words, when accessing any API, the signature needs to be verified, and the process can only continue if the signature passes. Otherwise, an error message will be displayed. The pseudocode demonstration is as follows:

1
2
3
4
5
// Here, you can see that the signature mechanism is to hash the API using MD5
// Access the account login API
http.post('http://host.com/api/account/login', {"account":"zhangsan", "password":"123456"}).signature('api/account/login')
// Access the API for my orders
http.post('http://host.com/api/order/list', {"token": "abcdefg"}).signature('api/order/list')

After receiving the data on the server, the same signature method is used to calculate the signature and compare it with the signature passed by the client. Pseudo code demonstration as follows:

1
2
3
4
5
server_signature = md5( 'api/account/login' )
client_signature = http.get_post( 'signature' )
if ( server_signature != client_signature ) {
return 'signature error';
}

After implementing this signature mechanism, if the client is decompiled, the signature mechanism will be exposed. Therefore, another important element is introduced into the signature mechanism: timestamp. The introduction of timestamp serves two important purposes:

  1. Determining the timeliness of API access.
  2. Participating in the signature calculation.

If during a certain visit to http://host.com/api/order/list, the timestamp value is 123456789 and the signature is “xyz”, a malicious user records all the data and repeatedly calls it. If we compare the server time and the timestamp submitted by the user on the server side, and the difference between the two is much larger than a day or half an hour, we can directly return error messages such as “expired API access”.
At this point, it seems that the situation is already relatively well-established, and this signature system seems to be able to resist a large portion of malicious calls. In fact, in a truly complete API design, the API will be implemented by an API gateway, and one of the functions of the API gateway is anti-brushing and flow control, which can limit the maximum number of times a user, IP address, or device ID can access a certain API per second, according to different dimensions.
Up to now, for sensitive data including tokens, we have been transmitting them in plaintext. We need to encrypt sensitive data, and if the product manager now presents a third requirement: to add bank cards, the bank card number can be considered sensitive information.
As for the issue of data confidentiality, the first thing that comes to mind is HTTPS. However, when facing packet capture tools like Charles, HTTPS is actually useless. As long as the root certificate is configured, everything can be seen in plaintext. Therefore, besides the necessary HTTPS, we also need additional encryption mechanisms.

If this API is http://host.com/api/bankcard/create, then the encryption requirement is that when a user adds a bank card, if the data is intercepted, the bank card number should not be exposed in plain text. We need to introduce an encryption scheme to encrypt sensitive data.
The encryption scheme is not within the scope of this article, so I will directly choose the AES advanced encryption method. AES requires an encryption key to encrypt the content. Here is some pseudocode for demonstration:

1
2
3
4
5
6
7
8
9
// Encrypting the password
password = '123456'
// Content to be encrypted
message = 'Hello World!'
// Encrypting the content using the password
enc_message = encrypt(password, message)
// Decrypting
dec_message = decrypt(password, enc_message)
print dec_message // Hello World!

Below, we will introduce the encryption mechanism and then walk through the complete flow of the business logic. It is estimated that some students may already be confused.

1
2
// Step 1: Client executes login
http.post('http://host.com/api/account/login', { "account": "zhengsan", "password": "123456" }).signature("api/account/login" + "timestamp")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Step 2: Server receives login request, checks signature and timestamp validity, and executes login business logic

server_timestamp = get_timestamp()
client_timestamp = get_post('timestamp')
if (server_timestamp - client_timestamp > 30) {
return 'Expired API access'
}

server_signature = signature('api/account/login' + client_timestamp)
client_signature = get_post('signature')
if (server_signature != client_signature) {
return 'Signature error'
}

// Verify password and return token
password = get_post('password')
account = get_post('account')
server_password = get_password_by_account('account')
if (password == server_password) {
// Generate an AES encryption password
enc_password = "1a2b3c4d5f6g7h8i9j0k"
// Generate the original token
token = "0k9j8h7i6g5f4c3b2c1az9y8x7"
// Server records the mapping of token and uid
set(token, uid)
// The last step is crucial, return the AES encryption password and token to the client
return enc_password, token
}
1
2
3
4
5
6
7
8
9
10
11
// Step 3: Client receives encrypted password and token after login, then saves them

token = get('token')
enc_password = get('enc_password')
// Save these two items
save(token, enc_password)

// Encrypt the bank card number and submit it
bank_card = '666777888999'
enc_bank_card = encrypt(enc_password, bank_card)
http.post("http://host.com/api/bankcard/create", { enc_bank_card, enc_password, token }).signature('api/bankcard/create' + timestamp)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Step 4: Server receives data, verifies API signature and timestamp validity, and finally decrypts data and saves it to the database
// Pseudocode for verifying signature and timestamp validity omitted...

// Get the token, encrypted bank card, and encrypted password sent by the client
token = get_post('token')
enc_bankcard = get('enc_bankcard')
enc_password = get('enc_password')
bankcard = decrypt(enc_password, enc_bankcard)

// Using the token, find the uid according to the corresponding relationship
uid = get_uid_by_token('token')

// Save uid and bank card to the database
save(uid, bankcard)

Summary:

  • The signature mechanism is to prevent malicious use of the API, including the API itself.
  • Encryption is to ensure the security of sensitive data, which can include the token.
  • The token itself is unrelated to encryption; it is only an identifier for user identification.
  • As long as the client is decompiled, the encryption method and signature mechanism will be exposed, so security requires cooperation from both sides.

FAQ:

  • How is the relationship between token and uid implemented? Handled through Redis. You can consider using a hash data structure, with the token as the key and the complete user information stored in the hash.
  • How are the token or AES encryption/decryption passwords passed? It is better not to use the GET method and instead use the POST method. Alternatively, you can include this information in the HTTP headers or HTTP body. Personally, I usually put the signature, token, timestamp, and enc_password in the HTTP headers, and API parameters in the HTTP body.
  • I feel like it’s dangerous to pass enc_password as plaintext. You can actually avoid using the complete enc_password. You can negotiate with the client to define a rule, such as removing the first three characters and the last two characters of enc_password, and using the remaining characters as the encryption/decryption password.
  • Should the token itself be encrypted? Is it necessary to encrypt all parameters submitted to the API? It can be encrypted. You can even use the decrypted token in the signature calculation to create more complex signature rules. As for encrypting all submitted parameters, it is possible. If all API parameters are encrypted, the eavesdropper cannot easily determine the exact parameter names being submitted through packet sniffing. For example, if the original submission was in plaintext as { “account”: “zhangsan” }, if this API is encrypted, the JSON would be encrypted as something like “abcdekkadadfad==”. The eavesdropper would not easily know the exact parameter name “account” and therefore would find it difficult to write some scripts.