手機 App 介面公開於公網,面臨參數竄改、重放攻擊、中間人攻擊等風險。本文實戰一套雙 Token + MD5 簽名防篡改體系:idToken 識別用戶、encryptToken 參與簽名但不傳輸、請求參數排序拼接 → MD5 → 與客戶端簽名比對,附完整 Servlet 代碼與攻擊場景分析。
| 威脅 | 對策 |
|---|---|
| 參數竄改 (金額、用戶 ID) | 簽名覆蓋所有業務參數,篡改即驗證失敗 |
| 重放攻擊 | 引入 timestamp/nonce + 服務端快取去重 (本文簡化未示範) |
| 中間人竊取密鑰 | encryptToken 僅存服務端、登入時下發、不隨請求傳輸 |
| 簽名算法逆向 | MD5 不可逆,但建議升級 HMAC-SHA256 + 密鑰輪換 |
idToken (用戶唯一標識) + encryptToken (簽名密鑰),雙雙存資料庫,idToken 回傳客戶端,encryptToken 僅回傳一次 (登入響應體)。idToken → 拼接 encryptToken → MD5(UTF-8) → md5Str → 發送 {parameter: {...}, md5Str: "..."}。idToken 查 DB 取 encryptToken → 同樣拼接業務 JSON + encryptToken → MD5 → 比對 md5Str。public class Md5Util {
public static String encrypt(String src) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(src.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
public class SignVerifier {
public static int verify(String idToken, JSONObject bizParams, String clientSign) {
// 1. 查詢 encryptToken
String sql = "SELECT encrypt_token FROM user_tokens WHERE id_token=?";
String encryptToken = JdbcTemplate.queryForObject(sql, String.class, idToken);
if (encryptToken == null) return 1; // idToken 無效
// 2. 重構簽名字串:業務參數 JSON (按 key 排序) + encryptToken
// 這裡簡化:直接用 JSONObject.toString() (無序),生產必須按 key 排序
String signSrc = bizParams.toString() + encryptToken;
String serverSign = Md5Util.encrypt(signSrc);
// 3. 比對
return serverSign.equals(clientSign) ? 0 : 2; // 0=成功, 2=簽名不符
}
}
@WebServlet("/api/*")
public class ApiGatewayServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
// 解決中文亂碼 (Tomcat 8+ 需 URIEncoding="UTF-8")
String paramJson = new String(req.getParameter("parameter").getBytes("ISO-8859-1"), "UTF-8");
JSONObject wrapper = JSONObject.parseObject(paramJson);
JSONObject bizParams = JSONObject.parseObject(wrapper.getString("parameter"));
String clientSign = wrapper.getString("md5Str");
String idToken = bizParams.getString("idToken");
int code = SignVerifier.verify(idToken, bizParams, clientSign);
if (code != 0) {
resp.getWriter().write("{\"code\":" + code + ",\"msg\":\"簽名驗證失敗\"}");
return;
}
// 簽名通過,分發具體業務 Handler
String action = req.getPathInfo(); // /login, /order/create ...
dispatch(action, bizParams, resp);
}
}
public String generateSign(JSONObject bizParams, String encryptToken) {
// 1. 參數按 key 字典序排序拼接 key=value&key=value...
// 2. 拼接 encryptToken
// 3. MD5
String sorted = bizParams.keySet().stream()
.sorted()
.map(k -> k + "=" + bizParams.getString(k))
.collect(Collectors.joining("&"));
return Md5Util.encrypt(sorted + encryptToken);
}
JSONObject.toString() 無序,生產必須按 key 排序拼接 (如 key1=val1&key2=val2),否則同參數不同序導致簽名不一致。idToken,服務端查 DB 取 encryptToken 參與驗證,網路層永不傳輸。HmacSHA256 + SecretKeySpec,密鑰定期輪換。timestamp (允許 ±5 分鐘) + nonce (Redis Set 去重,TTL 10 分鐘)。idToken="LOGIN" 固定值 + encryptToken 從註冊時返回,防止登入介面被暴力破解。雙 Token + MD5 簽名以「密鑰不出服務端、參數全覆蓋、不可逆摘要」三原則,構建輕量級介面防篡改體系。掌握參數排序一致性、密鑰生命週期管理、重放防護、算法升級路徑四大工程細節,即可從「能跑通」進化到「經得起攻擊」。