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

这个东西就比较简单了 先上个小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什么的都行 自己安排就好

猜你喜欢

转载自blog.csdn.net/weixin_44515491/article/details/86561419