如今大多数应用采用前后端分离的形式,后端服务提供api接口,前端(APP、WEB等)调用接口,这样应用架构会比较灵活,同时也有一个问题就是api接口安全的问题。
一、api主要的要全问题
- 授权认证:确认访问者的身份,控制应用接口访问,控制用户数据访问。
- 网络攻击:防止Dos攻击,防止网络重放攻击
- 数据安全:防止数据泄露、防止数据被截取、篡改、伪造(包括请求的数据和返回的结果数据)。
二、解决方案
- 授权认证:可使用appid+appsecret签名认证、用户登录验证及JWT Token验证
- 网络攻击:每个请求带上时间参数,进行过期判断,防止Dos攻击;带上一个随机数,可以防止网络重放攻击
- 数据安全:使用https协议,或者使用其他加密方案
三、具体技术实现
1、接口appid、appsecret签名认证
所有接口请求都必须通过appid、appsecret签名验证。
pc端、web端、移动端、小程序、以及各第三方应用都当作是一个独立的应用,每个应用都会分配一个appid和appsecret,每个appid对应分配不同的接口,以实现接口的授权控制。
appid:应用id,用于标识是哪个应用,
appsecret:接口密钥,用于签名。密钥须保证安全,一般保存于服务端,不参与传输。由于web端无法保证appsecret的安全,所以web端必须通过JWT Token登录验证。
为了识别请求应用,以及防止请求参数被篡改伪造,所有请求参数必须通过签名认证,具体规则如下:
所有接口必须包含以下公共参数,为了防止公共参数和其他参数有冲突,可以加一个前缀,比如:lh
参数名 | 参数说明 |
---|---|
lhappid | 即:appid。用于标识引用 |
lhtime | 当前时间戳(13位毫秒数)。用于期限限制等 |
lhnonce | 请求随机数。用于防止网络重放攻击,每次请求必须重新赋值(一次性使用),建议使用uuid/guid |
lhsign | 签名。签名生成规则: |
(1)所有请求参数(排除lhsign),根据参数key排序(升序)拼接成键值连接的字符串; | |
(2)最后把appsecret加在前面,得到待加密字符串; | |
(3)再进行md5加密转大写,得到签名。MD5(appsecret+请求参数字符串)转大写 |
签名说明
(1)假设:appid=111111,appsecret=eeeeee
(2)请求参数为:k=1&b=2&a=3&lhappid=111111&lhtime=1583069221120
(3)排序后为:a=3,b=2,k=1,lhappid=111111,lhtime=1583069221120
(4)然后将参数键值拼接得到参数字符串:a3b2k1lhappid111111lhtime1583069221120
(5)把appsecret加在头部,得到加密字符串:eeeeeea3b2k1lhappid111111lhtime1583069221120
(6)MD5加密并转大写,得到签名:464CD7E4075B8484FBE348689173CEF3
签名代码参考:
:::code-tabs#shell
@tab java
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* 签名生成工具类
* 请求公共参数,为防止参数冲突以lh为前缀
* lhappid (公共参数)应用id,用于定位是哪个应用
* lhtime (公共参数)当前时间毫秒数。用于防止网络重放攻击,期限限制等
* lhsign (公共参数)签名
*/
public class SignUtil {
/**
* 签名生成规则
* 1、所有请求参数(排除lhsign),根据参数key排序(升序)拼接成键值连接的字符串
* 2、最后把appsecret加在前面,得到待加密字符串
* 3、再进行md5加密转大写,得到签名
*
* @param params 请求参数map,带有lhappid,lhtime
* @param appsecret app密钥
* @return
*/
public static String sign(Map<String, String> params, String appsecret) {
//如果有,去掉签名字段
if (params.containsKey("lhsign"))
params.remove("lhsign");
//根据参数Key排序(升序),TreeMap有排序功能
TreeMap<String, String> treeMap = new TreeMap<>();
treeMap.putAll(params);
//构造待签名的请求串
StringBuilder sb = new StringBuilder();
//把密钥加载最前面
sb.append(appsecret);
for (Map.Entry<String, String> entry : treeMap.entrySet()) {
sb.append(entry.getKey()).append(entry.getValue());
}
//生成md5值,转大写
return md5(sb.toString()).toUpperCase();
}
/**
* 验证签名
*
* @param params 参数
* @param appsecret app密钥
* @return
*/
public static boolean verify(Map<String, String> params, String appsecret) {
String sign = params.get("lhsign");
String tmpsign = sign(params, appsecret);
return sign.equals(tmpsign);
}
/**
* 获取sha1值
*
* @param str
* @return
*/
public static String sha1(String str) {
return encode("sha1", str);
}
/**
* 计算并获取md5值
*
* @param str
* @return
*/
public static String md5(String str) {
return encode("md5", str);
}
/**
* 编码加密
*
* @param algorithm 加密算法
* @param value 加密字符串
* @return
*/
private static String encode(String algorithm, String value) {
if (value == null) {
return null;
}
try {
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
messageDigest.update(value.getBytes());
return getFormattedText(messageDigest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 转成16进制
*
* @param bytes
* @return
*/
private static String getFormattedText(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
for (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* 测试示例
*
* @param args
*/
public static void main(String[] args) {
String url = "http://localhost/webmvc/v2/patrol/findGpsrealList";
String appid = "5e58bc4452a36d76ed455bc8";
String appsecret = "56d6b50c7c49927ab4d92bdbea0f6b0ffa1ad70f";
Map<String, String> params = new HashMap<>();
params.put("projectid", "slfhtest");
params.put("adcd", "530000000000000");
params.put("gpstype", "0");
params.put("next", "0");
params.put("lhappid", appid);
params.put("lhtime", String.valueOf(System.currentTimeMillis()));
params.put("lhnonce", UUID.randomUUID().toString());
//生成签名
String sign = sign(params, appsecret);
params.put("lhsign", sign);
Map<String, Object> qarams = new HashMap<>(params);
//发出http请求
//String res= HttpUtil.post(url,qarams);
}
}
@tab javascript
//MD5库:https://github.com/blueimp/JavaScript-MD5
/**生成签名
* @param params 请求参数对象
* @param appsecret 密钥
* @returns {string}
*/
function sign(params,appsecret){
//如果存在lhsign,去掉此属性
if(params.hasOwnProperty("lhsign")){
delete params.lhsign;
}
//key排序
var keys=Object.keys(params).sort();
var str=appsecret;
//封装请求字符串
for(var i=0;i<keys.length;i++){
str=str+keys[i]+params[keys[i]];
}
//MD5加密并转大写
return md5(str).toUpperCase();
}
/**
*验证签名
* @param params 请求参数对象
* @param appsecret 密钥
* @returns {boolean}
*/
function verify(params,appsecret){
var sign=params["lhsign"];
var tmpsign=sign(params,appsecret);
return sign==tmpsign;
}
/**
* 生成uuid
* @returns {string}
*/
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
//测试
var url="http://task.lonhcloud.cn/webmvc/v2/elementquery/findAdcdChildren"
var appid="5e58bc4452a36d76ed455bc8";
var appsecret="56d6b50c7c49927ab4d92bdbea0f6b0ffa1ad70f";
var params={
lhappid:appid,
lhtime:Date.now(),
lhnonce:uuid(),
adcd:'530000000000000',
}
var sign=sign(params,appsecret);
params["lhsign"]=sign;
//get请求
$.get(url,params,function(data,status,request){
console.log(data);
});
@tab csharp
class SignUtil
{
/// <summary>
/// 生成签名
/// </summary>
/// <param name="dict">请求参数</param>
/// <param name="appsecret">密钥</param>
/// <returns></returns>
public static String sign(Dictionary<String,String> dict,String appsecret)
{
if (dict.ContainsKey("lhsign"))
dict.Remove("lhsign");
SortedDictionary<String, String> sortDict = new SortedDictionary<string, string>(dict);
StringBuilder sb = new StringBuilder();
sb.Append(appsecret);
foreach (var item in sortDict)
{
sb.Append(item.Key).Append(item.Value);
}
return md5(sb.ToString()).ToUpper();
}
/// <summary>
/// 验证签名
/// </summary>
/// <param name="dict">请求参数</param>
/// <param name="appsecret">密钥</param>
/// <returns></returns>
public static bool verify(Dictionary<String, String> dict, String appsecret)
{
String lhsign = dict["lhsign"];
String tmpsign = sign(dict, appsecret);
return lhsign.Equals(tmpsign);
}
/// <summary>
/// md5加密
/// </summary>
/// <param name="str">加密字符串</param>
/// <returns></returns>
public static String md5(String str)
{
MD5 md5 = MD5.Create();
Byte[] bytes= md5.ComputeHash(Encoding.UTF8.GetBytes(str));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
sb.Append(bytes[i].ToString("x2"));
}
return sb.ToString();
}
/// <summary>
/// 获取时间戳,13位,毫秒
/// </summary>
/// <returns></returns>
public static string GetTimeStamps()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalMilliseconds).ToString();
}
/// <summary>
/// 测试示例
/// </summary>
public static void test()
{
String url = "http://task.lonhcloud.cn/webmvc/v2/elementquery/findAdcdChildren";
String appid = "5e58bc4452a36d76ed455bc8";
String appsecret = "56d6b50c7c49927ab4d92bdbea0f6b0ffa1ad70f";
Dictionary<String,String> dict =new Dictionary<string, string>();
dict["lhappid"]=appid;
dict["lhtime"] = GetTimeStamps();
dict["lhnonce"] = Guid.NewGuid().ToString();
dict["adcd"] = "530000000000000";
//生成签名
String lhsign = sign(dict, appsecret);
dict["lhsign"]= lhsign;
//http请求
//String res=HttpUtil.HttpPost(url, dict);
}
}
:::
2、用户登录验证及JWT Token验证
对于web项目或者更加严格的安全验证时,可以加上一层身份登录验证,一般使用用户登录+JWT Token验证。规则如下:
(1)客户端调用登录接口,登录成功后,服务端会返回JWT Token,位于返回对象的header中的lhtoken字段, 即: token=response.header("lhtoken")
(2)之后客户端每次请求都要在请求对象的header中带上上一次请求得到的lhtoken,即:request.header("lhtoken",token)
(3)每个token都有一定的期限,一般为15分钟,请求没有token或者token过期,都将导致请求失败
(3)可将lhtoken在本地存成公共变量,客户端每次请求带上lhtoken,返回成功后更新本地lhtoken
JWT Token 参考:https://www.cnblogs.com/aaron911/p/11300062.html
3、数据安全加密
为了防止数据被拦截抓取,导致泄露,所有请求参数和返回结果都要进行加密。
经过考虑,数据加密使用RSA(非对称加密)和AES(对称加密)相结合的方案。
AES 对称加密:
(1)加密方和解密方适用同一个秘钥
(2)加密解密的速度快,适合数据比较长时使用
(3)秘钥传输过程不安全,而且秘钥管理也麻烦
RSA 非对称加密
(1)有一对密钥,公钥和私钥。公钥加密只能私钥解密,私钥加密只能公钥解密
(2)算法强度复杂,其安全性依赖于算法与秘钥
(3)加密解密的速度慢,不适用于数据量较大的情况
具体实现
(1)服务端生成RSA密钥对,保存私钥,公开公钥
(2)客户端获取服务端RSA公钥。每次请求,成一个AES的密钥,请求的数据拼成json字符串后使用AES加密,放到请求body里,AES的密钥使用RSA公钥加密,放到请求头的lhkey里,并使用POST发出请求。
(3)服务端接收数据后,先取出lhkey,使用RSA私钥解密出AES的密钥,再使用AES解密出请求数据的json字符串,此时解析json字符串就可以得到请求内容。
(4)服务端返回结果时,使用AES进行加密返回,客户端再进行AES解密,就可以得到明文数据