3
When developing the background API interface with SpringBoot, what are the interface insecurity factors? How is it usually solved? This article mainly introduces the insecure factors of API interfaces and common ways to ensure the security of interfaces , and focuses on how to sign interfaces . @pdai

Prepare knowledge points

It is recommended to understand from the perspective of the overall security system of the interface, such as what insecure factors exist, and knowledge points such as encryption and decryption.

What are the insecure factors of API interface?

Here are some unsafe factors from a system perspective:
  • Developer access to open interfaces

    • Is it a legitimate developer?
  • Multiple Client Access Interface

    • Is it a legitimate client?
  • User access interface

    • Is it a legitimate user?
    • Do you have permission to access the interface?
  • interface transmission

    • HTTP clear text transmission of data?
  • Other aspects

Common way to ensure interface security?

In view of the insecure factors of the above interfaces, here are some typical ways to ensure the security of interfaces.

AccessKey&SecretKey

This design is generally used to secure the development interface to ensure that it is a legitimate developer .
  • AccessKey: The developer's unique identifier
  • SecretKey: developer key

Take Alibaba Cloud related products as an example

Authentication and Authorization

from two perspectives

  • First: authentication and authorization , authentication is the legitimacy of the visitor, and authorization is the authority classification of the visitor;
  • Second: The authentication includes the authentication of the client and the authentication of the user ;
  • Authentication for clients

Typical is AppKey&AppSecret, or ClientId&ClientSecret, etc.

For example, the client credential mode of the oauth2 protocol

 https://api.xxxx.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET

The grant_type parameter equal to client_credentials indicates the client credentials method, client_id is the client id, and client_secret is the client secret.

After returning the token, access other interfaces through the token.

  • Authentication and authorization for users

For example, the authorization code mode (authorization code) and the password mode (resource owner password credentials) of the oauth2 protocol

 https://api.xxxx.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID&scope=read

The grant_type parameter is equal to password to indicate the password mode, client_id is the client id, username is the user name, and password is the password.

(PS: password mode is only used when authorization code mode is not available, here is just an example)

The optional parameter scope indicates the permission scope of the application. (For related development frameworks, please refer to spring security, Apache Shiro, SA-Token , etc.)

https

From the perspective of interface transmission security, prevent interface data from being transmitted in plaintext, see here for details

HTTP has the following security issues:

  • Use clear text to communicate, the content may be eavesdropped;
  • Without verifying the identity of the communicating party, the identity of the communicating party may be disguised;
  • The integrity of the message cannot be proven, and the message may be tampered with.

HTTPs is not a new protocol, but let HTTP communicate with SSL (Secure Sockets Layer) first, and then communicate with SSL and TCP, that is to say, HTTPs uses tunnels for communication.

By using SSL, HTTPs has encryption (anti-eavesdropping), authentication (anti-masquerading), and integrity protection (anti-tampering).

Interface signature (encryption)

Interface signature (encryption), mainly to prevent request parameters from being tampered with. Especially the interface with high security requirements, such as the interface in the payment field.
  • The main process of signing

First, we need to assign a private key to the client for URL signature encryption. The general signature algorithm is as follows:

1. First, alphabetically sort the request parameters by key and put them into an ordered set (for other parameters, please refer to the subsequent supplementary part);

2. Use & to connect the sorted array key-value pairs to form a parameter string for encryption;

3. Add the private key before or after the encrypted parameter string, then encrypt it with an encryption algorithm to get the sign, and then send it to the server along with the request interface.

E.g:
https://api.xxxx.com/token?key=value&timetamp=xxxx&sign=xxxx-xxx-xxx-xxxx

After the server receives the request, it uses the same algorithm to obtain the sign of the server, and compares whether the sign of the client is consistent. If the request is consistent, the request is valid; if it is inconsistent, the specified error message is returned.

  • Supplement: Sign what?
  1. It mainly includes the request parameters, which is the most important part. The purpose of the signature is to prevent the parameters from being tampered with, and it is necessary to sign the parameters that may be tampered with ;
  2. At the same time, it is considered that the source of the request parameters may be in the request path path, in the request header, and in the request body.
  3. If AppKey&AppSecret is assigned to the client, the signature calculation can also be added;
  4. Taking into account other idempotent, token invalidation, etc., the involved parameters will also be added to the signature, such as timestamp, serial number nonce, etc. (these parameters may come from the header)
  • Supplement: Signature Algorithm?

Generally involved in this piece, it mainly includes three points: key, signature algorithm, signature rule

  1. Key secret : The secret agreed by the front and back ends. It should be noted here that the front end may not be able to properly store the secret, such as SPA single-page applications;
  2. Signature algorithm : It does not have to be a symmetric encryption algorithm. Symmetry is to parse the sign in reverse. Here, the same algorithm and rules are used to calculate the sign, and compare whether the sign passed from the front end is consistent.
  3. Signature rules : such as multiple salt encryption, etc.;
PS: Some readers will ask, it is possible for us to obtain keys, algorithms and rules from some clients (such as obtaining keys, algorithms and rules from js generated by a front-end SPA single-page application), so what is the significance of the signature? I think signature is a means rather than an end. Signature is a means to increase the difficulty of attackers' attacks, at least it can resist most simple attacks, plus other prevention methods (serial number, timestamp, token, etc.) to further Just increase the difficulty of the attack.
  • Supplement: Is signing and encryption the same thing?

Strictly speaking not the same thing:

  1. The signature is calculated by calculating the sign according to the specified algorithm and rules for the parameters, and finally the front and back ends use the same algorithm to calculate whether the sign is consistent to prevent parameter tampering, so you can see that the parameter is in plaintext, but an extra calculated value is added. sign.
  2. Encryption is to encrypt the parameters of the request and decrypt the back-end; at the same time, in some cases, the returned response is also encrypted, and the front-end decrypts; there is a process of encryption and decryption, so the idea must be the form of symmetric encryption + time Stamp interface timeliness, etc.
  • Supplement: Where is the signature placed?

The signature can be placed in the request parameters (path, body, etc.), or more elegantly in the HEADER, such as X-Sign (usually third-party header parameters start with X-)

  • Supplement: How does the big factory open platform do it? Which can you learn from?

Take Tencent Open Platform as an example, please refer to the description of Tencent Open Platform's third-party application signature parameter sig

Implementation case

This example is implemented by AOP intercepting custom annotations, which mainly depends on the idea of implementation (the purpose of signature is to prevent parameters from being tampered with, and it is necessary to sign parameters that may be tampered with). @pdai

define annotations

 package tech.pdai.springboot.api.sign.config.sign;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author pdai
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Signature {
}

AOP interception

Here you can see that all parameter points that may be modified by the user need to be signed according to the rules

 package tech.pdai.springboot.api.sign.config.sign;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;

import cn.hutool.core.text.CharSequenceUtil;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.ContentCachingRequestWrapper;
import tech.pdai.springboot.api.sign.config.exception.BusinessException;
import tech.pdai.springboot.api.sign.util.SignUtil;

/**
 * @author pdai
 */
@Aspect
@Component
public class SignAspect {

    /**
     * SIGN_HEADER.
     */
    private static final String SIGN_HEADER = "X-SIGN";

    /**
     * pointcut.
     */
    @Pointcut("execution(@tech.pdai.springboot.api.sign.config.sign.Signature * *(..))")
    private void verifySignPointCut() {
        // nothing
    }

    /**
     * verify sign.
     */
    @Before("verifySignPointCut()")
    public void verify() {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String sign = request.getHeader(SIGN_HEADER);

        // must have sign in header
        if (CharSequenceUtil.isBlank(sign)) {
            throw new BusinessException("no signature in header: " + SIGN_HEADER);
        }

        // check signature
        try {
            String generatedSign = generatedSignature(request);
            if (!sign.equals(generatedSign)) {
                throw new BusinessException("invalid signature");
            }
        } catch (Throwable throwable) {
            throw new BusinessException("invalid signature");
        }
    }

    private String generatedSignature(HttpServletRequest request) throws IOException {
        // @RequestBody
        String bodyParam = null;
        if (request instanceof ContentCachingRequestWrapper) {
            bodyParam = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
        }

        // @RequestParam
        Map<String, String[]> requestParameterMap = request.getParameterMap();

        // @PathVariable
        String[] paths = null;
        ServletWebRequest webRequest = new ServletWebRequest(request, null);
        Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        if (!CollectionUtils.isEmpty(uriTemplateVars)) {
            paths = uriTemplateVars.values().toArray(new String[0]);
        }

        return SignUtil.sign(bodyParam, requestParameterMap, paths);
    }

}

Request encapsulation

 package tech.pdai.springboot.api.sign.config;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

@Slf4j
public class RequestCachingFilter extends OncePerRequestFilter {

    /**
     * This {@code doFilter} implementation stores a request attribute for
     * "already filtered", proceeding without filtering again if the
     * attribute is already there.
     *
     * @param request     request
     * @param response    response
     * @param filterChain filterChain
     * @throws ServletException ServletException
     * @throws IOException      IOException
     * @see #getAlreadyFilteredAttributeName
     * @see #shouldNotFilter
     * @see #doFilterInternal
     */
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestWrapper = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestWrapper = new ContentCachingRequestWrapper(request);
        }
        try {
            filterChain.doFilter(requestWrapper, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

register

 package tech.pdai.springboot.api.sign.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {
    @Bean
    public RequestCachingFilter requestCachingFilter() {
        return new RequestCachingFilter();
    }

    @Bean
    public FilterRegistrationBean requestCachingFilterRegistration(
            RequestCachingFilter requestCachingFilter) {
        FilterRegistrationBean bean = new FilterRegistrationBean(requestCachingFilter);
        bean.setOrder(1);
        return bean;
    }
}

implement interface

 package tech.pdai.springboot.api.sign.controller;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tech.pdai.springboot.api.sign.config.response.ResponseResult;
import tech.pdai.springboot.api.sign.config.sign.Signature;
import tech.pdai.springboot.api.sign.entity.User;

/**
 * @author pdai
 */
@RestController
@RequestMapping("user")
public class SignTestController {

    @Signature
    @PostMapping("test/{id}")
    public ResponseResult<String> myController(@PathVariable String id
            , @RequestParam String client
            , @RequestBody User user) {
        return ResponseResult.success(String.join(",", id, client, user.toString()));
    }

}

interface test

body parameter

Without X-SIGN

If X-SIGN is wrong

If X-SIGN is correct

Sample source code

https://github.com/realpdai/tech-pdai-spring-demos

more content

Say goodbye to fragmented learning, one-stop systematic learning without routines Back-end development: Java full-stack knowledge system (https://pdai.tech)


pdai
70 声望158 粉丝