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什么的都行 自己安排就好