In actual services, it is inevitable to interact and transfer data with third-party systems. So how to ensure data security during transmission (anti-theft)? In addition to the HTTPS protocol, can we add a set of common algorithms and specifications to ensure the security of transmission?

Here we will discuss some common API design security methods, may not be the best, there is a better way to implement, but this is my own experience to share.

I. Brief introduction of token

Token: An access Token used in an interface to identify the identity and credentials of the interface caller and reduce the number of times the user name and password are transferred. In general, the client (interface caller) needs to apply for an account for interface invocation from the server. The server provides an appId and a key, which are used for parameter signature. The key is saved to the client and security measures are required to prevent leakage.

The value of the Token is generally UUID. After generating the Token, the server needs to use the Token as a key and save some information associated with the Token as value in the cache server (Redis). When a request comes in, the server checks whether the Token exists in the cache server and invokes the interface. There is no return interface error, which is generally implemented through interceptors or filters. Tokens are classified into two types:

API Token: Used to access interfaces that do not require user login, such as login, registration, some basic data acquisition, etc. The interface Token needs to be exchanged with appId, timestamp and sign. Sign = encryption (timestamp+key) USER Token(USER Token): Used to access the interface that requires USER login, such as obtaining my basic information, saving, modifying, deleting, etc. To obtain a user token, you need to exchange the user name and password

Regarding the timeliness of tokens: Tokens can be one-time or valid for a certain period of time, depending on business needs.

Generally, it is better to use HTTPS for the interface. If HTTP is used, the Token mechanism is only a way to reduce the possibility of being hacked. In fact, it can only protect the superior from the villain.

Generally, token, TIMESTAMP, and sign parameters are passed as parameters in the interface at the same time, and each parameter has its own purpose.

Second, timestamp introduction

Timestamp: indicates the current timestamp when the client invokes the interface. The timestamp is used to prevent DoS attacks.

When the hacker hijacks the url of the request to DoS attack, every time the interface is called, the interface will judge the difference between the current system time of the server and the timestamp passed in the interface. If the difference exceeds a set time (say 5 minutes), the request will be intercepted. If within the set timeout period, You can’t stop DoS attacks. The timestamp mechanism can only reduce the DoS attack time and shorten the attack time. If the hacker changes the timestamp value, it can be handled by the sign signature mechanism.

DoS

DoS is short for Denial of Service. An attack that causes DoS is called a DoS attack. It is intended to disable a computer or network from providing normal services. The most common DoS attacks are computer network bandwidth attacks and connectivity attacks.

DoS attack is to point to intentionally attack network protocol implementation defects or directly run out of brutally attacked by brutal means the resources of the object, the goal is to make the target computer or network to provide normal service or access to resources, make the goal system service system stopped responding to collapse, even in the attack does not include invade the target servers or network equipment. These service resources include network bandwidth, file system space capacity, open processes, or allowed connections. This attack will lead to the lack of resources, no matter how fast the computer processing speed, memory capacity, network bandwidth speed can not avoid the consequences of this attack.

Pingflood: the attack sends a large number of Ping packets to the destination host in a short time, blocking the network or exhausting host resources.

Synflood: The attack with multiple random source host sends a SYN packet to the destination host address in the receipt of the destination host SYN ACK does not respond, so, the host to a large number of connection to the source host queue, and has always maintained the queue, because there is no receives an ACK causes consumption of resources and cannot provide service to normal requests.

Smurf: This attack sends a packet with a specific request (such as an ICMP response request) to the broadcast address of a subnet and masquerades the source address as the address of the intended host. All hosts on the subnet send packets to the attacked host in response to the broadcast packet request, and the host is attacked.

Land-based: An attacker sets both the source and destination IP addresses of a packet to the IP address of the target host, and then sends the packet to the attacked host in IP spoofing mode. This packet causes the attacked host to fall into an infinite loop as it attempts to establish a connection with the attacker, which greatly reduces the system performance.

Ping of Death: According to the TCP/IP specification, the maximum length of a packet is 65536 bytes. Although the length of a packet cannot exceed 65,536 bytes, the superposition of multiple fragments of a packet does. When a host receives a packet with a length greater than 65536 bytes, it is attacked by a Ping of Death attack, which causes the host to break down.

Teardrop: When IP packets are transmitted over a network, they can be broken into smaller pieces. An attacker can implement a TearDrop attack by sending two (or more) packets. The first packet has an offset of 0 and length N, and the second packet has an offset of less than N. To merge these segments, the TCP/IP stack allocates an unusually large amount of resources, resulting in a lack of system resources and even machine reboots.

PingSweep: Uses ICMP Echo to poll multiple hosts.

Iii. Introduction of Sign

Nonce: the random value is randomly generated by the client and passed as a parameter. The random value is intended to increase the variability of the sign signature. Random values are generally a combination of numbers and letters, with a length of 6 digits. There are no fixed rules for the composition and length of random values.

sign: It is generally used for parameter signature to prevent parameter from being tampered illegally. The most common is to modify important sensitive parameters such as amount. The value of sign is generally to sort all non-empty parameters in ascending order and then splicing together +token+key+timestamp+nonce(random number), and then use some encryption algorithm for encryption. Pass sign as a parameter in the interface, or you can place sign in the request header.

In the process of network transmission, if the interface is hijacked by a hacker, and the parameter values are modified, and then the interface is called again, although the parameter values are modified, the hacker does not know how sign is calculated, what values are composed of sign, and in what order they are spliced together. The most important thing is don’t know what will be the key for the signature string, so a hacker can tamper with the value of the parameter, but can not modify the value of the sign when the server before the call interface will be calculated according to the rules to sign out the sign value then and interface sign of the value of the parameter, if the same parameter value has not been tampered with, if the range, Indicates that the parameter has been tampered with illegally, and the interface is not executed.

Fourth, to prevent repeated submission

For some important operations that need to be prevented from repeated submission by the client (such as non-idemidemsive important operations), the specific method is to save sign as key to Redis when the request is submitted for the first time and set the timeout time, which is the same as the difference set in Timestamp.

When the same request is accessed for the second time, it will first detect whether Redis has the sign. If it does, it will prove that it has repeatedly submitted, and the interface will not continue to call. If sign is deleted in the cache server due to the expiration time, when the URL requests the server again, the expiration time of token is the same as the expiration time of sign, and the expiration time of sign also means the token expires. In this case, the same URL accessing the server will be blocked due to the token error. This is why the expiration time of sign and token should be the same. The refusal to call again mechanism ensures that the URL is intercepted and cannot be used by others (such as fetching data).

You can customize annotations to indicate which interfaces need to prevent repeated submissions.

Note: all security measures are sometimes too complex and need to be tailored according to the actual situation of the project. For example, you can only use the signature mechanism to ensure that the information will not be tampered with, or only use the Token mechanism to provide targeted services. How to cut it depends on the actual situation of the project and the requirements for interface security.

Five, the use process

1. The interface caller (client) applies for an interface invocation account from the interface provider (server). After the application is successful, the interface provider will give the interface caller an appId and a key parameter

2. The client invokes the API token on the server with parameters appId, timestamp and sign, where sign= encryption (appId + timestamp + key)

3. The client uses the API_token to access interfaces that do not require login

4. When accessing the interface that the user needs to log in to, the client redirects to the login page and invokes the login interface using the user name and password. The login interface returns a usertoken

The function of sign is to prevent parameter tampering. When the client calls the server, it needs to pass the sign parameter. When the server responds to the client, it can also return a sign to verify whether the returned value is tampered illegally. The sign algorithm passed by the client and the sign algorithm responded by the server may differ.

Six, sample code

  1. dependency
  1. RedisConfiguration

@Configuration public class RedisConfiguration { @Bean public JedisConnectionFactory jedisConnectionFactory(){ return new JedisConnectionFactory(); }

Public RedisTemplate<String, String> RedisTemplate (){RedisTemplate<String, String> redisTemplate = new StringRedisTemplate(); redisTemplate.setConnectionFactory(jedisConnectionFactory()); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; }Copy the code

}

  1. TokenController

@Slf4j @RestController @RequestMapping(“/api/token”) public class TokenController {

@Autowired private RedisTemplate redisTemplate; /** * API Token * * @param sign * @return */ @PostMapping("/api_token") public ApiResponse<AccessToken> apiToken(String appId, @RequestHeader("timestamp") String timestamp, @RequestHeader("sign") String sign) { Assert.isTrue(! StringUtils.isEmpty(appId) && ! StringUtils.isEmpty(timestamp) && ! Stringutils.isempty (sign), "parameter error "); long reqeustInterval = System.currentTimeMillis() - Long.valueOf(timestamp); Assert.isTrue(reqeustInterval < 5 * 60 * 1000, "Request expired, please re-request "); Secret AppInfo AppInfo = new AppInfo("1", "12345678954556"); String signString = timestamp + appId + appInfo.getkey (); // 2. String signature = MD5Util.encode(signString); log.info(signature); Assert.istrue (signature.equals(sign), "signature error "); assert.istrue (signature.equals(sign)," signature error "); AccessToken AccessToken = this.saveToken(0, appInfo, null); return ApiResponse.success(accessToken); } @NotRepeatSubmit(5000) @PostMapping("user_token") public ApiResponse<UserInfo> userToken(String username, UserInfo UserInfo = new UserInfo(username, "81255cb0dca1a5f304328a70ac85dcbd", "111111"); String pwd = password + userInfo.getSalt(); String passwordMD5 = MD5Util.encode(pwd); Assert.istrue (passwordmd5.equals (userinfo.getPassword ()), "password error "); assert.istrue (passwordmd5.equals (userinfo.getPassword ())); // 2. Save Token AppInfo AppInfo = new AppInfo("1", "12345678954556"); AccessToken accessToken = this.saveToken(1, appInfo, userInfo); userInfo.setAccessToken(accessToken); return ApiResponse.success(userInfo); } private AccessToken saveToken(int tokenType, AppInfo appInfo, UserInfo userInfo) { String token = UUID.randomUUID().toString(); Calendar = calendar.getInstance (); // Token valid for 2 hours calendar.setTime(new Date()); calendar.add(Calendar.SECOND, 7200); Date expireTime = calendar.getTime(); / / 4. Save the token ValueOperations < String, TokenInfo > operations. = redisTemplate opsForValue (); TokenInfo tokenInfo = new TokenInfo(); tokenInfo.setTokenType(tokenType); tokenInfo.setAppInfo(appInfo); if (tokenType == 1) { tokenInfo.setUserInfo(userInfo); } operations.set(token, tokenInfo, 7200, TimeUnit.SECONDS); AccessToken accessToken = new AccessToken(token, expireTime); return accessToken; } public static void main(String[] args) { long timestamp = System.currentTimeMillis(); System.out.println(timestamp); String signString = timestamp + "1" + "12345678954556"; String sign = MD5Util.encode(signString); System.out.println(sign); System.out.println("-------------------"); signString = "password=123456&username=1&12345678954556" + "ff03e64b-427b-45a7-b78b-47d9e8597d3b1529815393153sdfsdfsfs" + timestamp + "A1scr6"; sign = MD5Util.encode(signString); System.out.println(sign); }Copy the code

}

  1. WebMvcConfiguration

@Configuration public class WebMvcConfiguration extends WebMvcConfigurationSupport {

private static final String[] excludePathPatterns  = {"/api/token/api_token"};


@Autowired
private TokenInterceptor tokenInterceptor;


@Override
public void addInterceptors(InterceptorRegistry registry) {
    super.addInterceptors(registry);
    registry.addInterceptor(tokenInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns(excludePathPatterns);
}
Copy the code

} 5. TokenInterceptor @Component public class TokenInterceptor extends HandlerInterceptorAdapter {

@Autowired private RedisTemplate redisTemplate; /** ** @param request * @param Response * @param Target method accessed by the handler * @return * @throws Exception */ @override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); String timestamp = request.getHeader("timestamp"); // Random String nonce = request.getheader ("nonce"); String sign = request.getHeader("sign"); Assert.isTrue(! StringUtils.isEmpty(token) && ! StringUtils.isEmpty(timestamp) && ! Stringutils.isempty (sign), "parameter error "); / / get timeout NotRepeatSubmit NotRepeatSubmit = ApiUtil. GetNotRepeatSubmit (handler); long expireTime = notRepeatSubmit == null ? 5 * 60 * 1000 : notRepeatSubmit.value(); Long reqeustInterval = System.currentTimemillis () - long-.valueof (timestamp); Assert.isTrue(reqeustInterval < expireTime, "Request timeout, please re-request "); / / 3. Check whether the Token is ValueOperations < String, TokenInfo > tokenRedis = redisTemplate. OpsForValue (); TokenInfo tokenInfo = tokenRedis.get(token); Assert.notNull(tokenInfo, "token error "); // 4. Verify the signature (add all the parameters, // Request parameter + token + timestamp + nonce String signString = APIUtil. concatSignString(request) +  tokenInfo.getAppInfo().getKey() + token + timestamp + nonce; String signature = MD5Util.encode(signString); boolean flag = signature.equals(sign); Assert.isTrue(flag, "signature error "); assert. isTrue(flag," signature error "); If (notRepeatSubmit!) {if (notRepeatSubmit! = null) { ValueOperations<String, Integer> signRedis = redisTemplate.opsForValue(); boolean exists = redisTemplate.hasKey(sign); Assert.isTrue(! Exists, "do not repeat submission "); signRedis.set(sign, 0, expireTime, TimeUnit.MILLISECONDS); } return super.preHandle(request, response, handler); }Copy the code

}

  1. MD5Util —-MD5 tool class that encrypts and generates digital signatures

public class MD5Util {

private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
        "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };


private static String byteArrayToHexString(byte b[]) {
    StringBuffer resultSb = new StringBuffer();
    for (int i = 0; i < b.length; i++)
        resultSb.append(byteToHexString(b[i]));


    return resultSb.toString();
}


private static String byteToHexString(byte b) {
    int n = b;
    if (n < 0)
        n += 256;
    int d1 = n / 16;
    int d2 = n % 16;
    return hexDigits[d1] + hexDigits[d2];
}


public static String encode(String origin) {
    return encode(origin, "UTF-8");
}
public static String encode(String origin, String charsetname) {
    String resultString = null;
    try {
        resultString = new String(origin);
        MessageDigest md = MessageDigest.getInstance("MD5");
        if (charsetname == null || "".equals(charsetname))
            resultString = byteArrayToHexString(md.digest(resultString
                    .getBytes()));
        else
            resultString = byteArrayToHexString(md.digest(resultString
                    .getBytes(charsetname)));
    } catch (Exception exception) {
    }
    return resultString;
}
Copy the code

}

  1. @notrepeatSubmit —– Custom annotations to prevent duplicate submissions.

/ * *

  • Prohibit double submission

/ @target (ElementType.METHOD) @Retention(retentionPolicy.runtime) public @interface NotRepeatSubmit { **/ long value() default 5000; }

  1. AccessToken

@Data @AllArgsConstructor public class AccessToken { /** token */ private String token;

/** private Date expireTime;Copy the code

}

  1. AppInfo

@Data @NoArgsConstructor @AllArgsConstructor public class AppInfo { /** App id / private String appId; /* API key */ private String key; }

  1. TokenInfo

@data public class TokenInfo {/** Token types: API :0, user:1 */ private Integer tokenType;

/** AppInfo */ private AppInfo; /** Other user data */ private UserInfo UserInfo;Copy the code

}

  1. UserInfo

@data public class UserInfo {/** private String username; /* Private String mobile; /* email/private String email; /* Private String password; /* salt */ private String salt;

private AccessToken accessToken;


public UserInfo(String username, String password, String salt) {
    this.username = username;
    this.password = password;
    this.salt = salt;
}
Copy the code

}

  1. ApiCodeEnum

/ * *

  • Error codes can use pure numbers, use different intervals to identify a type of error, use pure characters, or use prefix + number
  • Error code: ERR + Number
  • Info(I) Error(E) Warning(W)
  • Or business module + error number
  • TODO error code design
  • Alipay uses two codes and two MSG (docs.open.alipay.com/api_1/alipa…)

*/ public enum ApiCodeEnum {SUCCESS(“10000”, “SUCCESS “), UNKNOW_ERROR(“ERR0001”), PARAMETER_ERROR(“ERR0002″,” error “), TOKEN_EXPIRE(“ERR0003″,” authentication expired “), REQUEST_TIMEOUT(“ERR0004″,” request timeout “), SIGN_ERROR(“ERR0005″,” signature error “), REPEAT_SUBMIT(“ERR0006″,” please do not operate frequently “),;

/** code */ private String code; /** result */ private String MSG; ApiCodeEnum(String code, String msg) { this.code = code; this.msg = msg; } public String getCode() { return code; } public String getMsg() { return msg; }Copy the code

}

  1. ApiResult

@Data @NoArgsConstructor @AllArgsConstructor public class ApiResult {

/** code */ private String code; /** result */ private String MSG;Copy the code

}

  1. ApiUtil ——- this reference alipay encryption algorithm written. I just copied it.

public class ApiUtil {

* @param Request * @return */ public static String concatSignString(HttpServletRequest Request) { Map<String, String> paramterMap = new HashMap<>(); request.getParameterMap().forEach((key, value) -> paramterMap.put(key, value[0])); Set<String> keySet = paramtermap.keyset (); String[] keyArray = keySet.toArray(new String[keySet.size()]); Arrays.sort(keyArray); StringBuilder sb = new StringBuilder(); For (String k: keyArray) {if (k. quals("sign")) {continue; } if (paramtermap.get (k).trim().length() > 0) {// The parameter value is null, Do not participate in the signature sb. Append (k), append (" = "). Append (paramterMap. Get (k). The trim ()), append (" & "); } } return sb.toString(); } public static String concatSignString(Map<String, String> map) { Map<String, String> paramterMap = new HashMap<>(); map.forEach((key, value) -> paramterMap.put(key, value)); Set<String> keySet = paramtermap.keyset (); String[] keyArray = keySet.toArray(new String[keySet.size()]); Arrays.sort(keyArray); StringBuilder sb = new StringBuilder(); for (String k : KeyArray) {if (paramtermap.get (k).trim().length() > 0) {// The parameter value is null. Do not participate in the signature sb. Append (k), append (" = "). Append (paramterMap. Get (k). The trim ()), append (" & "); } } return sb.toString(); } @param Handler * @return */ public static NotRepeatSubmit getNotRepeatSubmit(Object) handler) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); NotRepeatSubmit annotation = method.getAnnotation(NotRepeatSubmit.class); return annotation; } return null; }Copy the code

}

  1. ApiResponse

@data @slf4j public class ApiResponse {/** result */ private ApiResult result;

/** data */ private T data; /** sign */ private String sign; public static <T> ApiResponse success(T data) { return response(ApiCodeEnum.SUCCESS.getCode(), ApiCodeEnum.SUCCESS.getMsg(), data); } public static ApiResponse error(String code, String msg) { return response(code, msg, null); } public static <T> ApiResponse response(String code, String msg, T data) { ApiResult result = new ApiResult(code, msg); ApiResponse response = new ApiResponse(); response.setResult(result); response.setData(data); String sign = signData(data); response.setSign(sign); return response; } private static <T> String signData(T data) {// TODO query key String key = "12345678954556"; Map<String, String> responseMap = null; try { responseMap = getFields(data); } catch (IllegalAccessException e) { return null; } String urlComponent = ApiUtil.concatSignString(responseMap); String signature = urlComponent + "key=" + key; String sign = MD5Util.encode(signature); return sign; } /** * @param data Reflects the object and obtains the field name and value of the object. * @throws IllegalArgumentException * @throws IllegalAccessException */ public static Map<String, String> getFields(Object data) throws IllegalAccessException, IllegalArgumentException { if (data == null) return null; Map<String, String> map = new HashMap<>(); Field[] fields = data.getClass().getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; field.setAccessible(true); String name = field.getName(); Object value = field.get(data); if (field.get(data) ! = null) { map.put(name, value.toString()); } } return map; }Copy the code

}

Seven, ThreadLocal

ThreadLocal is a global context within a thread. Is the memory shared between methods in a single thread, from which each method can fetch and modify values.

Actual cases:

When the API is called, a token parameter is passed, and usually an interceptor is written to verify whether the token is valid. We can find the corresponding User information by using the token. If the token is valid, then the User information is stored in ThreadLocal. In this way, the user’s information can be accessed at any layer of controller, Service, or DAO. The effect is similar to the Request scope on the Web.

The traditional way is to access a variable in a method, you can pass a parameter to a method, and if multiple methods need to use it, each method needs to pass a parameter; If you use ThreadLocal all methods do not need to pass this parameter, and each method can access this value through ThreadLocal.

ThreadLocalUtil.set(“key”, value); T value = ThreadLocalUtil. Get (“key”); Get the value

ThreadLocalUtil

public class ThreadLocalUtil { private static final ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() { @Override protected Map<String, Object> initialValue() { return new HashMap<>(4); }};

public static Map<String, Object> getThreadLocal(){ return threadLocal.get(); } public static <T> T get(String key) { Map map = (Map)threadLocal.get(); return (T)map.get(key); } public static <T> T get(String key,T defaultValue) { Map map = (Map)threadLocal.get(); return (T)map.get(key) == null ? defaultValue : (T)map.get(key); } public static void set(String key, Object value) { Map map = (Map)threadLocal.get(); map.put(key, value); } public static void set(Map<String, Object> keyValueMap) { Map map = (Map)threadLocal.get(); map.putAll(keyValueMap); } public static void remove() { threadLocal.remove(); } public static <T> Map<String,T> fetchVarsByPrefix(String prefix) { Map<String,T> vars = new HashMap<>(); if( prefix == null ){ return vars; } Map map = (Map)threadLocal.get(); Set<Map.Entry> set = map.entrySet(); for( Map.Entry entry : set){ Object key = entry.getKey(); if( key instanceof String ){ if( ((String) key).startsWith(prefix) ){ vars.put((String)key,(T)entry.getValue()); } } } return vars; } public static <T> T remove(String key) { Map map = (Map)threadLocal.get(); return (T)map.remove(key); } public static void clear(String prefix) { if( prefix == null ){ return; } Map map = (Map)threadLocal.get(); Set<Map.Entry> set = map.entrySet(); List<String> removeKeys = new ArrayList<>(); for( Map.Entry entry : set ){ Object key = entry.getKey(); if( key instanceof String ){ if( ((String) key).startsWith(prefix) ){ removeKeys.add((String)key); } } } for( String key : removeKeys ){ map.remove(key); }}Copy the code

}

Summary: This is the third party data interface interaction process commonly used in some parameters and use examples, I hope to help you a little.

Of course, in order to ensure more security,RSA,RSA2, AES and other encryption methods can be added to ensure more security of data, but the only disadvantage is that encryption and decryption cost CPU resources.

Generally RSA+AES combination! Principle: AES encryption parameters, RSA encryption AES key, the back end uses RSA to decrypt the AES key, and then uses AES to decrypt the parameters transmitted by the front end

There is no absolute security, only relative security

This article reprinted: cnblogs.com/jurendage/p/12653865.html