对外提供API,通过appId、appSecret、sign秘钥对接口做鉴权

一、背景

在接口开发过程中,我们通常不能暴露一个接口给第三方随便调用,要对第三方发来参数进行校验,看是不是具有访问权限。

名词介绍:

1、appId: 应用id,用户自定义命名,如:*-access-token

2、appSecret:安全密钥,服务端通过uuid生成,然后发送给调用方

3、sign:开发生成签名的工具类,发送给调用方

鉴权步骤:

1、客户端使用提供的工具,传参appId、时间戳、appSecret生成sign签名

2、发送appId、13位系统时间戳、签名、请求参数给服务端

3、服务端收到appId、时间戳、签名参数后,根据appId查询数据库,获取用户appSecret安全密钥。同样根据appId、时间戳、appSecret生成sign,判断用户传参的sign是否相等。

 

二、具体代码

1、鉴权工具类

package com.test.utils;

import java.security.MessageDigest;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;

import lombok.extern.slf4j.Slf4j;

/**
 * @datetime 2022-11-02 上午11:00
 * @desc 接口校验工具类
 *  生成有序map,签名,验签
 *  通过appId、timestamp、appSecret做签名
 * @menu
 */
@Slf4j
public class SignUtil {

    /**
     * 生成签名sign
     * 加密前:appId=wx123456789&timestamp=1583332804914&key=7214fefff0cf47d7950cb2fc3b5d670a
     * 加密后:E2B30D3A5DA59959FA98236944A7D9CA
     */
    public static String createSign(SortedMap<String, String> params, String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es =  params.entrySet();
        Iterator<Map.Entry<String,String>> it =  es.iterator();
        //生成
        while (it.hasNext()){
            Map.Entry<String,String> entry = it.next();
            String k = entry.getKey();
            String v = entry.getValue();
            if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
            }
        }
        sb.append("key=").append(key);
        String sign = MD5(sb.toString()).toUpperCase();
        return sign;
    }

    /**
     * 校验签名
     */
    public static Boolean isCorrectSign(SortedMap<String, String> params, String key){
        String sign = createSign(params,key);
        String requestSign = params.get("sign").toUpperCase();
        log.info("通过用户发送数据获取新签名:{}", sign);
        return requestSign.equals(sign);
    }
    
    /**
     * md5常用工具类
     */
    public static String MD5(String data){
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte [] array = md5.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 生成uuid
     */
    public static String generateUUID(){
        String uuid = UUID.randomUUID().toString().replaceAll("-","").substring(0,32);
        return uuid;
    }

    public static void main(String[] args) {
        //第一步:生成uuid,用作appSecret
//        System.out.println(SignUtil.generateUUID());

        //第二步:用户端发起请求,生成签名后发送请求
        String appSecret = "7214fefff0cf47d7950cb2fc3b5d670a";
        String appId = "wx123456789";
        String timestamp = "1583332804914";
        //生成签名
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put("appId", appId);
        sortedMap.put("timestamp", timestamp);
        System.out.println("签名:"+SignUtil.createSign(sortedMap, appSecret));

        //第三步:校验签名
        //1.校验时间戳
        long requestTime = Long.valueOf(timestamp);
        // 时间查过20秒,则认为接口为重复调用,返回错误信息
        long nowTime = new Date().getTime();
        int seconds = (int) ((nowTime - requestTime)/1000);
        if(Math.abs(seconds) > 86400) {
            System.out.println("访问已过期,请检查服务器时间!");
            return;
        }
        //2.组装参数,
        SortedMap<String, String> sortedMap12 = new TreeMap<>();
        sortedMap12.put("appId", appId);
        sortedMap12.put("timestamp", timestamp);
        sortedMap12.put("sign", "");
        //3.校验签名
        Boolean flag = SignUtil.isCorrectSign(sortedMap12, appSecret);
        if(flag){
            System.out.println("签名验证通过");
        }else {
            System.out.println("签名验证未通过");
        }
    }
}

2、提供给用户的生成签名工具类

import java.security.MessageDigest;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import 上面的工具类包下.SignUtil;

/**
 * @datetime 2022-11-02 下午2:45
 * @desc
 * @menu
 */
public class SignUtilTest {

    /**
     * 生成签名sign
     * 加密前:appId=wx123456789&timestamp=1583332804914&key=7214fefff0cf47d7950cb2fc3b5d670a
     * 加密后:E2B30D3A5DA59959FA98236944A7D9CA
     */
    public static String createSign(SortedMap<String, String> params, String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> es =  params.entrySet();
        Iterator<Map.Entry<String,String>> it =  es.iterator();
        //生成
        while (it.hasNext()){
            Map.Entry<String,String> entry = it.next();
            String k = entry.getKey();
            String v = entry.getValue();
            if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
            }
        }
        sb.append("key=").append(key);
        String sign = MD5(sb.toString()).toUpperCase();
        return sign;
    }

    /**
     * md5常用工具类
     */
    public static String MD5(String data){
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte [] array = md5.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        //第二步:用户端发起请求,生成签名后发送请求
        String appSecret = "";
        String appId = "";
        long nowTime = new Date().getTime();
        //生成签名
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put("appId", appId);
        sortedMap.put("timestamp", String.valueOf(nowTime));
        System.out.println("appId:"+appId+" 时间戳:"+nowTime+" 签名:"+ SignUtil.createSign(sortedMap, appSecret));
    }
}

3、服务端系统对控制层做切面,拦截部分url请求

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
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.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import lombok.extern.slf4j.Slf4j;
//省略内部使用包
@Slf4j
@Aspect
@Component
public class ExtApiAspect {
     /**
     * 系统接入管理
     */
    @Resource
    SystemAccessMapper systemAccessMapper;
    
     /**
     * 不拦截路径
    */
    private static final Set<String> FILTER_PATHS = Collections.unmodifiableSet(new HashSet<>(
            Arrays.asList("/api/extApi/test")));
    /**
     * 匹配ExtApiController下所有方法
     */
    @Pointcut("execution(public * com.test.ExtApiController.*(..))")
    public void webLog() {
    }
    
    /**
     * 1、控制层处理完后返回给用户前,记录日志
     * 3、调用处理方法获取结果后,再调用本方法proceed
     */
    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        // 打印下返回给用户数据
        log.info("RequestId={}, Response Args={}, Time-Consuming={} ms", RequestThreadLocal.getRequestId(),
                 JsonUtil.toJSON(result), System.currentTimeMillis() - startTime);
        return result;
    }
    
     /**
     * 2、调用控制层方法前执行
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        RequestThreadLocal.setRequestId(getUUID());

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //不拦截打印日志的方法
        String path = request.getRequestURI().substring(request.getContextPath().length()).replaceAll("[/]+$", "");
        if(FILTER_PATHS.contains(path)){
            return;
        }

        //请求参数
        Object[] args = joinPoint.getArgs();
        //请求的方法参数值 JSON 格式 null不显示
        if (args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                //请求参数类型判断过滤,防止JSON转换报错
                if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
                    continue;
                }
                StringBuilder logInfo = new StringBuilder();
                logInfo.append("RequestId=").append(RequestThreadLocal.getRequestId()).append(SystemConstants.LOG_SEPARATOR)
                        .append("  RequestMethod=").append(request.getRequestURI()).append(SystemConstants.LOG_SEPARATOR)
                        .append("  Args=").append(args[i].toString());
                log.info(logInfo.toString());
                JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(args[i]));
                if (jsonObject != null) {
                    String appId = jsonObject.getString("appId");
                    String timestamp = jsonObject.getString("timestamp");
                    String sign = jsonObject.getString("sign");
                    if(StringUtils.isEmpty(appId) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(sign)){
                        throw new CustomException(CodeEnum.EXT_API_PARAMETER_ERROR);
                    }
                    //1.时间戳校验,大于一天不处理
                    long requestTime = Long.valueOf(timestamp);
                    long nowTime = new Date().getTime();
                    int seconds = (int) ((nowTime - requestTime)/1000);
                    if(Math.abs(seconds) > 86400) {
                        throw new CustomException(CodeEnum.EXT_API_TIMEOUT);
                    }

                    LambdaQueryWrapper<SystemAccessDTO> queryWrapper = new LambdaQueryWrapper<>();
                    //!!!!!安全考虑,省略查询条件
                    queryWrapper.last(" limit 1");
                    SystemAccessDTO systemAccessDTO = systemAccessMapper.selectOne(queryWrapper);
                    if(systemAccessDTO == null){
                        throw new CustomException(CodeEnum.EXT_API_AUTH_ERROR);
                    }
                    //3.校验签名
                    SortedMap<String, String> sortedMap = new TreeMap<>();
                    sortedMap.put("appId", appId);
                    sortedMap.put("timestamp", timestamp);
                    sortedMap.put("sign", sign);
                    Boolean flag = SignUtil.isCorrectSign(sortedMap, systemAccessDTO.getAppSecret());
                    if(flag){
                        log.info("外部系统接入,信息认证正确"+appId);
                    }else{
                        throw new CustomException(CodeEnum.EXT_API_AUTH_ERROR);
                    }
                }
            }
        }else{
            throw new CustomException(CodeEnum.PARAMETER_ERROR);
        }
    }
     /**
     * 4、调用控制层方法后,说明校验通过不处理
     */
    @After("webLog()")
    public void doAfter(JoinPoint joinPoint) throws Throwable {
        log.info("外部接口调用鉴权成功" + RequestThreadLocal.getRequestId());
    }

    /**
     * 5、处理完后
     */
    @AfterReturning(pointcut = "webLog()", returning = "result")
    public void AfterReturning(JoinPoint joinPoint, RestResponse result) {
        result.success(RequestThreadLocal.getRequestId());
        RequestThreadLocal.remove();
    }

    /**
     * 异常处理
     */
    @AfterThrowing("webLog()")
    public void afterThrowing() {
        RequestThreadLocal.remove();
    }

    /**
     * 生成uuid
     */
    public static String getUUID() {
        return UUID.randomUUID().toString().trim();
    }
}

 

三、模拟请求

请求post:http://127.0.0.1:8080/test/test

请求json

{"appId":"appid","timestamp":"1667384136520","sign":"sign","ids":[接口其它请求]}

 

 

 

 

参考文档:https://blog.csdn.net/it1993/article/details/104682832/

posted @ 2022-11-03 09:32  黑水滴  阅读(4800)  评论(0编辑  收藏  举报