1, Abstract
In the actual business development process, we often encounter the need to carry out technical docking with the third party Internet Co, such as Alipay payment docking, WeChat payment docking, High German map query docking and other services. If you are an entrepreneurial Internet, most of them may be connected to other companies api interface.
When your company's size comes up, some companies may start to talk to you for technology docking and turn to you to provide API interfaces. At this time, how should we design and ensure the security of API interfaces?
2, Scheme introduction
There are two most commonly used schemes:
- token scheme
- Interface signature
2.1. token scheme
Among them, the token scheme is one of the most widely used interface authentication schemes on the web side. Here we briefly introduce the token scheme.
We can clearly see the following steps of token implementation from the above figure:
- 1. After the user logs in successfully, the server will generate a unique valid certificate for the user. This valid value is called token
- 2. When a user requests another service interface every time, it needs to bring a token in the request header
- 3. When the server receives the client business interface request, it will verify the legitimacy of the token. If it is illegal, it will prompt the client; If it is legal, it will enter the business processing process.
In the actual use process, when the user logs in successfully, the generated token is time effective when stored in redis. It is generally set to 2 hours, and it will automatically expire after 2 hours. At this time, we need to log in again and obtain a valid token again.
token scheme is the most widely used scheme in current business projects, and it is very practical. It can effectively prevent hackers from capturing packets and crawling data.
But the token scheme also has some disadvantages! The most obvious thing is that when interfacing with a third-party company, when your interface requests are very large, the token suddenly fails, and a large number of interface requests will fail.
I have a deep understanding of this. I remember that in the early days of joint debugging with a medium and large Internet company, the interface docking scheme they provided me was the token scheme. At that time, our company requested a large number of errors from their interfaces during the peak traffic, because the token failed. When the token failed, we would call them to refresh the token interface, After the refresh is completed, a large number of request failure logs will appear during the interval between token failure and re refresh. Therefore, I do not recommend you to adopt the token scheme in the actual API docking process.
2.2. Interface signature
Interface signature, as the name suggests, is to sign parameters through some signature rules, and then put the signed information into the request header. After the server receives the request from the client, it only needs to produce the corresponding signature string according to the established rules and compare it with the signature information of the client. If it is consistent, it will enter the business processing process; If it fails, you will be prompted that the signature verification fails.
In the interface signature scheme, there are mainly four core parameters:
- 1. Appid refers to the application ID, and appsecret is matched with it. It refers to the application key, which is used for data signature and encryption. Different docking projects are assigned different appid and appsecret to ensure data security
- 2. timestamp refers to the time stamp. If the difference between the requested time stamp and the time stamp in the server is within 5 minutes, it is a valid request. If it is not within this range, it is an invalid request
- 3. nonce refers to the temporary serial number, which is used to prevent repeated submission verification
- 4. Signature represents the signature field, which is used to judge whether the interface request is valid.
The generation rules of signature are divided into two steps:
- Step 1: md5 encrypt and sign the request parameters once
//Step 1: string parameter 1 = request method + request URL relative address + request Body string; String parameter 1 encryption result = MD5 (parameter 1)
- Step 2: perform another md5 encryption signature on the signature result of step 1
//Step 2: string parameter 2 = appsecret + timestamp + nonce + parameter 1 encryption result; String parameter 2 encryption result = MD5 (parameter 2)
Parameter 2: the encryption result is the final signature string we want.
The interface signature scheme is still very stable, especially in the case of a large number of interface requests.
In other words, you can see the interface signature as a supplement to the pairwise token scheme.
However, if you want to extend the interface signature scheme to front and rear end docking, the answer is: not suitable.
Because the signature calculation is very complex. Secondly, it is easy to disclose appsecret!
Having said so much, let's practice it with the program!
2, Procedural practice
2.1. token scheme
As mentioned above, the key point of the token scheme is that when the user logs in successfully, we only need to generate the corresponding token and return it to the front end. We need to bring the token the next time we request the business interface.
Specific practices can also be divided into two types:
- The first method: use uuid to generate the token, then store the token in redis, and set the validity period to 2 hours
- The second is to use JWT tools to generate tokens, which can be cross platform and naturally support distribution. In fact, the essence is to use timestamp + key to generate a token.
Next, we introduce the second implementation.
First, write a jwt tool.
public class JwtTokenUtil { //Define the return header of Token: public static final String AUTH_HEADER_KEY = "Authorization"; / / token prefix public static final String token_ PREFIX = "Bearer "; / / signature key (public static final String key = "q3t6w9z $C & F") J@NcQfTjWnZr4u7x "; / / the default validity period is # 2Our # public # static # final # Long # EXPIRATION_TIME = 1000L*60*60*2; / * * * create token * @ param # content * @ return * / public # static # String createToken(String # content) {return # token# prefix + JWT.create() . withSubject(content) . withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) . sign(Algorithm.HMAC512(KEY)); } / * * * verify token * @ param} token * / public} static String verifyToken(String token) throws {Exception {try {return} JWT.require(Algorithm.HMAC512(KEY)). build(). verify(token.replace(TOKEN_PREFIX, "")) . getSubject(); } catch (TokenExpiredException E) {throw new Exception("token has expired, please log in again", e); } catch (JWTVerificationException E) {throw new Exception("token verification failed!", e); } }}
Then, when logging in, we generate a token and return it to the client.
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){ //... Parameter validity verification / / obtain user information from the database # user # dbuser = userservice selectByUserNo(userDto.getUserNo); //.... User and password verification / / create a token and put it in the response header # usertoken # usertoken = new # UserToken(); BeanUtils.copyProperties(dbUser,userToken); String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken)); response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token); / / define the returned result: UserVo result = new UserVo(); BeanUtils.copyProperties(dbUser,result); return result;} One click Copy
Finally, write a unified interceptor to verify whether the token passed in by the client is valid.
@Slf4jpublic class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Get token final String token = request from HTTP request header getHeader(JwtTokenUtil.AUTH_HEADER_KEY); / / if it is not mapped to a method, directly use {if(!(handler {instanceof} HandlerMethod)) {return} true; } / / if it is a method probe, directly use if (httpmethod. Options. Equals (request. Getmethod()) {response.setStatus(HttpServletResponse.SC_OK); return true; } / / if the method has jwtignore annotation, directly use} handlermethod} handlermethod = (HandlerMethod) handler; Method method=handlerMethod.getMethod(); if (method.isAnnotationPresent(JwtIgnore.class)) { JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class); if(jwtIgnore.value()){ return true; } } LocalAssert. Istringempty (token, "token is empty, authentication failed!"); / / verify and get the internal information of token String usertoken = jwttokenutil verifyToken(token); / / put the token into the local cache {webcontextutil setUserToken(userToken); return true; After the @ http {request. Exception() method of the HTTP} token, remove the @ http {token exception() method of the HTTP} token; }}
When generating a token, we can store some basic user information, such as user ID and user name, into the token. In this way, after the token authentication is passed, we only need to analyze the information inside to obtain the corresponding user ID, which can save the operation of querying some basic information in the database.
At the same time, in the process of using, try not to store sensitive information, because it is easy to be analyzed by hackers!
2.2. Interface signature
In the same way, from the perspective of server verification, we can first write a signature interceptor to verify whether the parameters passed in by the client are legal. As long as one item is illegal, an error will be prompted.
The specific code practice is as follows:
public class SignInterceptor implements HandlerInterceptor { @Autowired private AppSecretService appSecretService; @Autowired private RedisUtil redisUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Appid = final string appid = request getHeader("appid"); if(StringUtils.isEmpty(appId)) {throw new "CommonException("appid cannot be empty "); } String appSecret = appSecretService.getAppSecretByAppId(appId); if(StringUtils.isEmpty(appSecret)) {throw new "CommonException(" illegal appid "); } / / timestamp validation} final string timestamp = request getHeader("timestamp"); if(StringUtils.isEmpty(timestamp)) {throw new "CommonException("timestamp cannot be empty "); } / / more than 5 minutes, illegal request} long diff = system currentTimeMillis() - Long.parseLong(timestamp); if(Math.abs(diff) > 1000 * 60 * 5) {throw new "CommonException("timestamp expired "); } / / temporary serial number to prevent repeated submission} final string nonce = request getHeader("nonce"); if(StringUtils.isEmpty(nonce)) {throw new "CommonException("nonce cannot be empty "); } / / verify signature} final string signature = request getHeader("signature"); if(StringUtils.isEmpty(nonce)) {throw "new" CommonException("signature cannot be empty"); } final String method = request.getMethod(); final String url = request.getRequestURI(); final String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8")); String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret); if(!signature.equals(signResult)) {throw new "CommonException(" signature verification failed "); } / / check whether the request is repeated} string key = appid + "" + timestamp + "_" + nonce; if(redisUtil.exist(key)) {throw new "CommonException(" the current request is being processed, please do not submit it again "); } / / set 5 minutes} redisutil save(key, signResult, 5*60); request.setAttribute("reidsKey",key); } @ Override public void after completion (HttpServletRequest request, httpservlet response, Object handler, Exception ex) throws Exception {/ / after the request is processed, the cache is removed. String value = request.getattribute ("requestkey"); if(!StringUtils.isEmpty(value)){ redisUtil.remove(value); } } }
Signature tool class SignUtil:
public class SignUtil { /** * Signature calculation * @ param. method * @ param. url * @ param. body * @ param. timestamp * @ param. nonce * @ param. appSecret * @ return*/ public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){ //First layer signature: string requeststr1 = method + url + body + appSecret; String signResult1 = DigestUtils.md5Hex(requestStr1); / / layer 2 Signature: string requeststr2 = appSecret + timestamp + nonce + signresult1; String signResult2 = DigestUtils.md5Hex(requestStr2); return signResult2; }}
Signature calculation can be changed to hamc method, and the idea is roughly the same.
3, Summary
The token and interface signature scheme described above can protect the interface provided externally and prevent others from tampering with the request or simulating the request.
However, there is a lack of security protection for the data itself, that is, the requested parameters and returned data may be intercepted and obtained by others, and these data are clear text, so as long as they are intercepted, the corresponding business data can be obtained.
In this case, it is recommended to encrypt the request parameters and return parameters, such as RSA, AES and other encryption tools.
At the same time, in the production environment, https transmission can play a good role in security protection!