欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android应用安全防护实践一网络请求参数签名校验(二)

程序员文章站 2022-03-05 11:57:29
...

这个东西就比较简单了 先上个小demo

public static HttpParams sign(Map<String, String> paramValues, List<String> ignoreParamNames) {
        try {
            paramValues.put("timestamp", String.valueOf(System.currentTimeMillis()));
            StringBuilder sb = new StringBuilder();
            List<String> paramNames = new ArrayList<>(paramValues.size());
            paramNames.addAll(paramValues.keySet());
            if (ignoreParamNames != null && ignoreParamNames.size() > 0) {
                paramNames.removeAll(ignoreParamNames);
            }
            Collections.sort(paramNames);
            for (String paramName : paramNames) {
               sb.append(paramName).append(paramValues.get(paramName));
            }
            //重点 这个是一个ndk开放的一个签名+加盐工具类
            String sign = Utils.getSign(sb.toString());
            HttpParams httpParams = new HttpParams();
            for (String key : paramValues.keySet()) {
                httpParams.put(key, paramValues.get(key));
            }
            httpParams.put("sign", sign);
            return httpParams;
        } catch (Exception e) {
            throw new RuntimeException("加密签名计算错误", e);
        }

    }

如上就完成了参数的签名 并且为参数里面增加了一个时间戳字段timestamp和一个签名值字段sign 其中sign不参与签名 参数要做排序

然后是ndk那边

#include <jni.h>
#include <string>
#include <string.h>
#include <malloc.h>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <iterator>
#include <cctype>

jstring str2jstring(JNIEnv *env, const char *pat) {
    //定义java String类 strClass
    jclass strClass = (env)->FindClass("Ljava/lang/String;");
    //获取String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
    jmethodID ctorID = (env)->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
    //建立byte数组
    jbyteArray bytes = (env)->NewByteArray(strlen(pat));
    //将char* 转换为byte数组
    (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte *) pat);
    // 设置String, 保存语言类型,用于byte数组转换至String时的参数
    jstring encoding = (env)->NewStringUTF("UTF-8");
    //将byte数组转换为java String,并输出
    return (jstring) (env)->NewObject(strClass, ctorID, bytes, encoding);
}


std::string jstring2str(JNIEnv *env, jstring jstr) {
    char *rtn = NULL;
    jclass clsstring = env->FindClass("java/lang/String");
    jstring strencode = env->NewStringUTF("UTF-8");
    jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode);
    jsize alen = env->GetArrayLength(barr);
    jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
    if (alen > 0) {
        rtn = (char *) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    env->ReleaseByteArrayElements(barr, ba, 0);
    std::string stemp(rtn);
    free(rtn);
    return stemp;
}


//xxx是你的包名 点好换成下划线
extern "C"
JNIEXPORT jstring JNICALL Java_com_xxx_xxx_Utils_getSign(
        JNIEnv *env,
        jclass type, jstring arg0) {
    std::string content = jstring2str(env, arg0);
    content="我是盐值"+content+"我是味精值";
    std::string val = A1 + "com/xxx/xxx/Utils"

//这里是ndk调用java方法 因为懒 直接调用java的MD5方法了 注意路径和参数
    jclass clazz = env->FindClass(val.c_str());
    jmethodID mid = env->GetStaticMethodID(clazz, "getMD5",
                                           "(Ljava/lang/String;)Ljava/lang/Object;");
    jstring byte = (jstring) env->CallStaticObjectMethod(clazz, mid,
                                                         env->NewStringUTF(content.c_str()));

    content = jstring2str(env, byte);

    std::transform(content.begin(), content.end(), content.begin(), toupper);

    return env->NewStringUTF(content.c_str());
}

对应的java类

public class Utils {
    static {
    //ndk编译后的名字 
        System.loadLibrary("sign");
    }
    //Keep注解是为了防止混淆的时候混淆掉名字
    @Keep 
    //对应ndk调用的方法名字
    public native static String getSign(String arg0);

    @Keep
    //这个就是ndk调用的java方法 注意名字 参数返回值 
    public static Object getMD5(String data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return byte2hex(md.digest(data.getBytes("UTF-8")));
        } catch (Exception gse) {
            gse.printStackTrace();
        }
        return "";
    }

    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (byte aByte : bytes) {
            String hex = Integer.toHexString(aByte & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }
}

上面就完成了一套安卓端的签名了 签名办法也放在了ndk 大大提高了安全性

然后是服务端的签名校验

    public static Boolean sign(Map<String, String> paramValues) throws CommonException {
        String sign = paramValues.get("sign");
        //先提前并移除sign字段 因为他不参与签名计算
        paramValues.remove("sign");
        long currentTimeMillis = System.currentTimeMillis();
        Long timestamp = Long.valueOf(paramValues.get("timestamp"));

        long difference = getDifference(timestamp, currentTimeMillis, 0);
        if (difference > 60) {
        //抛出自定义异常 没做异常return也行 无所谓 不是重点 这里的if是个时间戳校验 免得api重放工具(别人抓包你的借口模拟请求 重复请求)
            throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败 请重试或检查手机时间是否准确");
        }
        StringBuilder sb = new StringBuilder();

        List<String> paramNames = new ArrayList<>(paramValues.size());
        paramNames.addAll(paramValues.keySet());

//记得排序 不然结构和安卓签名时的顺序不一致 结果也会不同
        Collections.sort(paramNames);

        sb.append("我是盐值");
        for (String paramName : paramNames) {
            sb.append(paramName).append(paramValues.get(paramName));
        }
        sb.append("我是味精值");

        String md5 = DigestUtils.md5Hex(sb.toString()).toUpperCase();
        //然后判断是否与安卓端传过来的签名一致 不一致就返回错误信息就行 
        return md5.equalsIgnoreCase(sign);
    }

注意 时间戳校验 如果用户手机时间不正确 或者时区和服务器不一致 要做处理 提示用户校准时间 并且获取时间时要获取和服务器一致的时区时间 否则校验不通过

使用时可以自定义个注解+拦截器 给要进行校验的接口添加校验注解就OK 十分方便

下面给出拦截器示例代码

@Component
public class SafetyCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        SafetyCheck annotation = method.getAnnotation(SafetyCheck.class);
        if (annotation == null) {
            return true;
        }

        String requestMethod = request.getMethod();
        if (requestMethod.equalsIgnoreCase("GET")) {
            Map<String, String> map = new LinkedHashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String nextElement = parameterNames.nextElement();
                map.put(nextElement, request.getParameter(nextElement));
            }
            //这里判断是否存在sign参数 代码走到这里了 肯定是接口加了校验 但是安卓端没有提供这个sign字段 那估计是有点问题了 怎么处理看你自己咯
            if (map.containsKey("sign")) {
            //最终比对 如果匹配就玩下执行 如果不匹配 就返回异常
                if (VerifyUtils.sign(map)) {
                    return true;
                }
            }
        }

//这里是校验post的请求 因为post 的body不能被重复读取 所以只能勉强校验一下parameter 如果你的请求没用body传数据 那就下面的代码就可以的 如果body传参 那就需要单独在Controller校验了

//        if (requestMethod.equalsIgnoreCase("POST")) {
//            Map<String, String> map = new LinkedHashMap<>();
//            Enumeration<String> parameterNames = request.getParameterNames();
//            while (parameterNames.hasMoreElements()) {
//                String nextElement = parameterNames.nextElement();
//                map.put(nextElement, request.getParameter(nextElement));
//            }
//            if (map.isEmpty()) {
//                return true;
//            }
//            if (map.containsKey("sign")) {
//                if (VerifyUtils.sign(map)) {
//                    return true;
//                }
//            }
//        }
        System.out.println("-----------------------------------------");
        System.out.println("数据校验失败 请重试并检查手机时间是否准确");
        System.out.println(request.getRequestURL().toString());
        System.out.println("Version:" + request.getHeader("version"));
        System.out.println("-----------------------------------------");
        //校验不通过的 抛出异常提现客户端
        throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败 请重试并检查手机时间是否准确");
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

下面是 单独处理body

    @Authentication
    @RequestMapping(value = "/setCachePool", method = RequestMethod.POST)
    public ServerResponse setCachePool(@RequestAttribute @ApiIgnore String from, @RequestParam String sign, @RequestBody String cachePool) {
    //这里单独校验body的内容签名
        VerifyUtils.sign(cachePool, sign);
        likePoolService.setCachePool(from, GsonUtils.create().fromJson(cachePool, CachePool.class));
        return new ServerResponse();
    }

校验工具类

    public static void sign(String json, String sign) {
        String sb = "我是盐值" + json + "我是味精值";
        String md5 = DigestUtils.md5Hex(sb).toUpperCase();
        if (!md5.equalsIgnoreCase(sign)) {
        //不通过就甩异常
            throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败");
        }
    }

客户端单独处理body用Utils.getSign(body); 然后参数拼接 xxx.com/getinfo?sign="+ sign

这样就行

最后附上客户端普通处理的用法

 Map<String, String> paramMap = new TreeMap<>();
        paramMap.put("size", String.valueOf(size));
        HTTP.<String>get(HOST.concat("/api/v3/pool/getPool"))
                .params(ParamUtils.sign(paramMap))
                .execute();

ParamUtils返回的是一个参数类 你们改成返回map什么的都行 自己安排就好

相关标签: 安全