Autenticación de firma de la interfaz API de Java

Autenticación de firma de la interfaz API de Java

Cuando desarrollamos programas, definitivamente desarrollaremos algunas interfaces API para que otros accedan. Por supuesto, algunas de estas interfaces pueden estar abiertas, o pueden ser accesibles solo después de iniciar sesión, es decir, solo se puede acceder a ellas después de que la autenticación del Token sea exitosa. Entonces, la pregunta es, ¿nuestras interfaces abiertas no están siempre expuestas? ¿Cómo garantizar la seguridad de estas interfaces?
Este artículo resolverá los problemas anteriores a través de la autenticación de firma de la interfaz API.

¿Qué es la autenticación de firma de interfaz?

Esto puede verse como, por ejemplo, si solicito una cuenta oficial de WeChat o un mini programa, la información básica de la cuenta oficial incluirá AppId y AppSecret. Nuestros usuarios deben guardar estos dos datos, especialmente AppSecret no debe estar expuesto para garantizar la seguridad. Cuando necesitamos solicitar a WeChat que autorice el inicio de sesión, debemos unir el enlace de solicitud de acuerdo con las reglas de WeChat. El enlace de solicitud contendrá información como AppId, AppSecret, etc. Al unir el enlace de acuerdo con las reglas, podemos solicitar correctamente WeChat, de lo contrario, la solicitud fallará.
Y lo que tenemos que hacer es implementar la verificación de firma de la interfaz API de nuestro propio programa de acuerdo con este principio de WeChat.

Reglas de parámetros de firma de interfaz

Los siguientes parámetros deben llevarse en el encabezado de cada solicitud:

  1. appKey: equivalente a appId, una identificación de la fuente de la solicitud.
  2. sign: Firma, calculada por reglas de firma.
  3. t: Marca de tiempo, calculando la diferencia entre esta marca de tiempo y la hora actual del servidor para evitar problemas de reproducción de solicitudes.

Fórmula y reglas de cálculo de firma:

sign=MD5(data+AppSecret+t)
donde data es la concatenación de parámetros de solicitud, y las reglas son las siguientes:

  1. Formulario de transferencia de parámetros de ruta: como /api/user/{userId}/{mobile}, parámetros únicos o múltiples, ordenados por la posición de los parámetros en la dirección. Luego, la concatenación de cadenas de data=userId+mobile.
  2. Paso de parámetros en forma de objeto, es decir, en forma json: es necesario ordenar en orden ascendente del diccionario de acuerdo con los atributos en el objeto, y luego empalmar los valores de los atributos en este orden. Por ejemplo, la clase de usuario es la siguiente
@Data
class User{
    
    
    private String userId;
    private String mobile;
}

Luego, el cálculo de los datos es: ID de usuario, dos nombres de atributos móviles se ordenan de acuerdo con el diccionario. El orden es mobile->userId, si mobile=17612345678;userId=123, el orden de empalme de datos es 17612345678123. Y firme = MD5 (17612345678123 + AppSecret + t). El parámetro de empalme del signo de interrogación es el mismo.
3. Pasar parámetros en forma de lista: los contenidos de la lista deben empalmarse secuencialmente.

Código

El siguiente código contiene algunas clases de herramientas hechas a sí mismas, como AssertUtils, LocalCacheUtils, etc.

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Stream;

@Slf4j
@Component
public class VerifySignUtils {
    
    

    @Autowired
    private RedisRepository redisRepository;
   // 设置请求重放的时间差
    private final long SIGN_EXPIRE = 1000 * 10;

    public boolean verifySign(String appKey, String sign, String t, Object object) {
    
    
    // 参数判空,校验
        log.info("传入的时间戳:{}", t);
        if (null == appKey) {
    
    
            log.error("没有传入appKey");
            return false;
        }
        long now = System.currentTimeMillis();
        // 校验请求重放
        if (Long.parseLong(t) < now - SIGN_EXPIRE) {
    
    
            log.error("sign失效!传入时间戳{};当前时间戳:{}", t, now);
            return false;
        }
        // 取出缓存的AppId和AppSecret配置表,因为是对接多个程序,是以配置表的形式实现的
        Map<Integer, String> appInfos = Optional.ofNullable(LocalCacheUtils.get(BaseConstants.APP_CACHE_NAME))
                .map(it -> (Map<Integer, String>) it).orElseGet(() ->
                        (Map<Integer, String>) LocalCacheUtils.setExpire(BaseConstants.APP_CACHE_NAME, Optional
                                        .ofNullable(redisRepository.get(BaseConstants.APP_CACHE_NAME))
                                        .map(it -> (Map<Integer, String>) it).orElseGet(null),
                                BaseConstants.LOCAL_CACHE_APP_INFO_EXPIRE));
        String secret = appInfos.get(Integer.parseInt(appKey));
        if (null == secret) {
    
    
            log.error("appKey错误");
            return false;
        }
        // 根据请求,计算拼接参数,即data的拼接
        String objectFields;
        if (object instanceof Object[]) {
    
    
            StringBuilder builder = new StringBuilder();
            for (Object o : ((Object[]) object)) {
    
    
                builder.append(o);
            }
            objectFields = builder.toString();
        } else if (object instanceof List) {
    
    
            StringBuilder builder = new StringBuilder();
            ((List) object).forEach(it -> builder.append(getObjectFields(it)));
            objectFields = builder.toString();
        } else if (object instanceof String || object instanceof Long || object instanceof Integer || object instanceof Boolean) {
    
    
            objectFields = object.toString();
        } else {
    
    
            objectFields = getObjectFields(object);
        }
        log.info("参数按顺序拼接:{}", objectFields);
        // 计算sign签名的值
        String tempSign = Md5Utils.getMD5((objectFields + secret + t).getBytes()).toUpperCase();
        log.info("计算出的sign:{}\t传入的sign:{}", tempSign, sign);
        // 校验传入的签名和服务端计算的签名是否一致,不一致则,签名认证失败
        if (!tempSign.equals(sign)) {
    
    
            log.error("计算验签与传入的验签不符");
            return false;
        }
        return true;
    }
	// 以下为通过反射拼接对象参数的方法
    private String getObjectFields(Object object) {
    
    
        final Field[] fields = object.getClass().getDeclaredFields();
        final TreeMap<String, Object> treeMap = new TreeMap<>();
        Stream.of(fields).map(Field::getName).forEach(it -> treeMap.put(it, getFieldValueByName(it, object)));
        final StringBuilder builder = new StringBuilder();
        treeMap.forEach((k, v) -> builder.append(v));
        return builder.toString();
    }

    private Object getFieldValueByName(String fieldName, Object o) {
    
    
        try {
    
    
            String firstLetter = fieldName.substring(0, 1).toUpperCase();
            String getter = "get" + firstLetter + fieldName.substring(1);
            final Method method = o.getClass().getMethod(getter, new Class[]{
    
    });
            final Object value = method.invoke(o, new Object[]{
    
    });
            if (null == value) {
    
    
                return "";
            }
            if (value instanceof List) {
    
    
                return ((List) value).stream().map(it -> {
    
    
                    if (it instanceof String || it instanceof Long || it instanceof Integer || it instanceof Boolean) {
    
    
                        return it;
                    } else {
    
    
                        return getObjectFields(it);
                    }
                }).reduce((it1, it2) -> it1 + "" + it2).get().toString();
            }
            return value;
        } catch (Exception e) {
    
    
            return "";
        }
    }
}

El código anterior no es fijo y también se puede modificar y configurar de acuerdo con sus propias reglas de firma.

El código de llamada es el siguiente y se llama configurando AOP en el controlador:

@Aspect
@Slf4j
@Configuration
public class LogRecordAspect {
    
    

    @Autowired
    private VerifySignUtils verifySignUtils;

    @Autowired
    private AopLogUtil aopLogUtil;

    @Value("${spring.profiles.active}")
    private String active;

    private static final List<String> activeList = Arrays.asList("prod","test");

    ThreadLocal<Long> startTime = new ThreadLocal<Long>();

    @Pointcut("execution(* com.xx.xx.xx.controller..*.*(..))")
    public void webLog() {
    
    
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
    
    
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        List<Object> collect = aopLogUtil.verifySignLog(joinPoint);
        String appKey = request.getHeader("appKey");
        String sign = request.getHeader("sign");
        String t = request.getHeader("t");

        boolean flag;
        // get请求与其它请求进行区分,如果有delete,put请求,需要另加
        if ("GET".equals(request.getMethod())) {
    
    
            flag = verifySignUtils.verifySign(appKey, sign, t, collect.toArray());
        } else {
    
    
            flag = verifySignUtils.verifySign(appKey, sign, t, collect.get(0));
        }
        if (!flag && activeList.contains(active)) {
    
    
            log.error("验签失败!");
            throw new BaseException(R.SERVICE_VERIFY_SIGN_ERROR, "");
        }
    }

    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) {
    
    
        // 处理完请求,返回内容
        log.warn("开始响应:RESPONSE: {} ", ret);
        log.warn("响应时间: {} ms", System.currentTimeMillis() - startTime.get());
    }
}

Supongo que te gusta

Origin blog.csdn.net/wFitting/article/details/109672944
Recomendado
Clasificación