OpenResty使用Lua大全(十一)实战: nginx实现接口签名安全认证

@[TOC]

系列文章索引

OpenResty使用Lua大全(一)Lua语法入门实战 OpenResty使用Lua大全(二)在OpenResty中使用Lua OpenResty使用Lua大全(三)OpenResty使用Json模块解析json OpenResty使用Lua大全(四)OpenResty中使用Redis OpenResty使用Lua大全(五)OpenResty中使用MySQL OpenResty使用Lua大全(六)OpenResty发送http请求 OpenResty使用Lua大全(七)OpenResty使用全局缓存 OpenResty使用Lua大全(八)OpenResty执行流程与阶段详解 OpenResty使用Lua大全(九)实战:nginx-lua-redis实现访问频率控制 OpenResty使用Lua大全(十)实战: Lua + Redis 实现动态封禁 IP OpenResty使用Lua大全(十一)实战: nginx实现接口签名安全认证 OpenResty使用Lua大全(十二)实战: 动手实现一个网关框架

一、需求背景

现在app客户端请求后台服务是非常常用的请求方式,在我们写开放api接口时如何保证数据的安全, 我们先看看有哪些安全性的问题

请求来源(身份)是否合法? 请求参数被篡改? 请求的唯一性(不可复制)

二、实现目标

1、案例

为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。

我们通过给某 [移动端(app)] 写 [后台接口(api)] 的案例进行分析:

客户端: 以下简称app 后台接口:以下简称api

我们通过app查询产品列表这个操作来进行分析: app中点击查询按钮==》调用api进行查询==》返回查询结果==>显示在app中

2、不进行验证的方式

api查询接口:/getproducts?参数 app调用:api.test.com/getproducts.......... 如上,这种方式简单粗暴,通过调用getproducts方法即可获取产品列表信息了,但是 这样的方式会存在很严重的安全性问题, 没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露。 那么,如何验证调用者身份呢?如何防止参数被篡改呢?

3、MD5参数签名的方式

我们对api查询产品接口进行优化: 1.给app客户端分配对应的key=1、secret秘钥

2.Sign签名,调用API 时需要对请求参数进行签名验证,签名方式如下: a. 按照请求参数名称将所有请求参数按照字母先后顺序排序得到:keyvaluekeyvalue...keyvalue

字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2 然后将参数名和参数值进行拼接 得到参数字符串:arong1crong3mrong2。 b. 将secret加在参数字符串的头部后进行MD5加密 ,加密后的字符串需大写。即得到签名Sign

新api接口代码: app调用:api.test.com/getproducts.......... 注:secret 仅作加密使用, 为了保证数据安全请不要在请求参数中使用。

如上,优化后的请求多了key和sign参数,这样请求的时候就需要合法的key和正确签名sign才可以获取产品数据。

这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。 再也无法伪造合法的请求。

api.test.com/getproducts... 加密后: api.test.com/getproducts...

客户端的算法 要和 我们服务器端的算法是一致的

"a=1&b=hello&c=world&key=1" 和秘钥进行拼接 secret=123456

"a=1&b=hello&c=world&123456" =》md5 加密 ===》字符串sign= BCC7C71CF93F9CDBDB88671B701D8A35


api.test.com/getproducts...

key去判断 是否客户端身份是合法 参数是否被篡改 服务器这边 也去生成一个sign签名,算法和客户端一致 a=2&c=world&b=hello ==》"a=2&b=hello&c=world" ==》secret=123456==》 "a=2&b=hello&c=world&123456" ==》md5 ===》服务器生成的sign ===》如果和客户端传过来的sign一致,就代表合法===》验证参数是否被篡改

4、不可复制

第二种方案就够了吗?我们会发现,如果我获取了你完整的链接,一直使用你的key和sign和一样的参数不就可以正常获取数据了!!是的,仅仅是如上的优化是不够的

请求的唯一性: 为了防止别人重复使用请求参数问题,我们需要保证请求的唯一性,就是对应请求只能使用一次, 这样就算别人拿走了请求的完整链接也是无效的。

唯一性的实现:在如上的请求参数中,我们加入时间戳 timestamp(yyyyMMddHHmmss),同样,时间戳作为请求参数之一, 也加入sign算法中进行加密。

新的api接口: app调用: api.test.com/getproducts..........

api.test.com/getproducts...

api.test.com/getproducts...

time是客户端发起请求的那一时刻,传过来的

客户端的算法 要和 我们服务器端的算法是一致的

"a=1&b=hello&c=world&time=201801232" 和秘钥进行拼接 secret=123456

"a=1&b=hello&c=world&time=201801232&123456" =》md5 加密 ===》字符串sign= BCC7C71CF93F9CDBDB88671B701D8A35


key=1 是否身份验证合法 time=客户端在调用这个接口那一刻传的时间 服务器去处理这个接口请求的当前时间 相减,如果这个大于10s;;;这个链接应该是被人家截取 如果小于10s,表示正常请求

如上,我们通过timestamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。

Sign签名安全性分析: 通过上面的案例,我们可以看出,安全的关键在于参与签名的secret,整个过程中secret是不参与通信的, 所以只要保证secret不泄露,请求就不会被伪造。

总结 上述的Sign签名的方式能够在一定程度上防止信息被篡改和伪造,保障通信的安全,这里使用的是MD5进行加密, 当然实际使用中大家可以根据实际需求进行自定义签名算法,比如:RSA,SHA等。

三、实现

1、编辑nginx.conf

lua 复制代码
location /sign {
	access_by_lua_file /usr/local/lua/access_by_sign.lua;
	echo "sign验证成功";
}

2、编辑access_by_sign.lua

lua 复制代码
--判断table是否为空
local function isTableEmpty(t)
    return t == nil or next(t) == nil
end

--两个table合并
local function union(table1,table2)
	for k, v in pairs(table2) do
		table1[k] = v
    end
    return table1
end

--检验请求的sign签名是否正确
--params:传入的参数值组成的table
--secret:项目secret,根据key找到secret
local function signcheck(params,secret)
	--判断参数是否为空,为空报异常
	if isTableEmpty(params) then
		local mess="参数为空"
        ngx.log(ngx.ERR, mess)
        return false,mess
	end
	
	if secret == nil then
		local mess="私钥为空"
        ngx.log(ngx.ERR, mess)
		return false,mess
	end
	
	local key = params["key"]; --平台分配给某客户端类型的keyID
	if key == nil then
		local mess="key值为空"
        ngx.log(ngx.ERR, mess)
		return false,mess
	end
	
	--判断是否有签名参数
	local sign = params["sign"]
	if sign == nil then
		local mess="签名参数为空"
        ngx.log(ngx.ERR, mess)
        return false,mess
	end
	
	--是否存在时间戳的参数
	local timestamp = params["time"]
	if timestamp == nil then
		local mess="时间戳参数为空"
        ngx.log(ngx.ERR, mess)
        return false,mess
	end
	
	--时间戳有没有过期,10秒过期
	local now_mill = ngx.now() * 1000 --毫秒级
	if now_mill - timestamp > 10000 then
		local mess="链接过期"
        ngx.log(ngx.ERR, mess)
        return false,mess
	end
	
	local keys, tmp = {}, {}

    --提出所有的键名并按字符顺序排序
    for k, _ in pairs(params) do 
		if k ~= "sign" then --去除掉
			keys[#keys+1]= k
		end
    end
	table.sort(keys)
	--根据排序好的键名依次读取值并拼接字符串成key=value&key=value
    for _, k in pairs(keys) do
        if type(params[k]) == "string" or type(params[k]) == "number" then 
            tmp[#tmp+1] = k .. "=" .. tostring(params[k])
        end
    end
	
	--将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比,
    local signchar = table.concat(tmp, "&") .."&"..secret
    local rightsign = ngx.md5(signchar);
	if sign ~= rightsign then
        --如果签名错误返回错误信息并记录日志,
        local mess="sign error: sign,"..sign .. " right sign:" ..rightsign.. " sign_char:" .. signchar
        ngx.log(ngx.ERR, mess)
        return false,mess
    end
    return true
end

local params = {}

local get_args = ngx.req.get_uri_args();
ngx.req.read_body()
local post_args = ngx.req.get_post_args();

union(params,get_args)

union(params,post_args)

local secret = "123456"  --根据keyID到后台服务获取secret

local checkResult,mess = signcheck(params,secret)

if not checkResult then
	ngx.say(mess);
	return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end 

3、java代码

模仿客户端

java 复制代码
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SignApplication {

	public static void main(String[] args) throws IOException {
		SpringApplication.run(SignApplication.class, args);
		
		HashMap<String,String> params = new HashMap<String,String>();
		
		params.put("key", "1");
		params.put("a", "1");
		params.put("c", "w");
		params.put("b", "2");
		
		long time = new Date().getTime();
		
		params.put("time", "" + time);
		
		System.out.println(time);
		
		String sign = getSignature(params,"123456");
		
		System.out.println(sign);
		
		params.put("sign", sign);
		
		String resp = HttpUtil.doGet("http://192.168.56.10/sign",params);
		
		System.out.println(resp);
	}
	
	/**
	 * 签名生成算法
	 * @param HashMap<String,String> params 请求参数集,所有参数必须已转换为字符串类型
	 * @param String secret 签名密钥
	 * @return 签名
	 * @throws IOException
	 */
	public static String getSignature(HashMap<String,String> params, String secret) throws IOException
	{
	    // 先将参数以其参数名的字典序升序进行排序
	    Map<String, String> sortedParams = new TreeMap<String, String>(params);
	    Set<Entry<String, String>> entrys = sortedParams.entrySet();
	 
	    // 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
	    StringBuilder basestring = new StringBuilder();
	    for (Entry<String, String> param : entrys) {
	    	if(basestring.length() != 0){
	    		basestring.append("&");
	    	}
	        basestring.append(param.getKey()).append("=").append(param.getValue());
	    }
	    basestring.append("&");
	    basestring.append(secret);
	    
	    System.out.println("basestring="+basestring);
	 
	    // 使用MD5对待签名串求签
	    byte[] bytes = null;
	    try {
	        MessageDigest md5 = MessageDigest.getInstance("MD5");
	        bytes = md5.digest(basestring.toString().getBytes("UTF-8"));
	    } catch (GeneralSecurityException ex) {
	        throw new IOException(ex);
	    }
	    
	    String strSign = new String(bytes);
	    System.out.println("strSign="+strSign);
	    // 将MD5输出的二进制结果转换为小写的十六进制
	    StringBuilder sign = new StringBuilder();
	    for (int i = 0; i < bytes.length; i++) {
	        String hex = Integer.toHexString(bytes[i] & 0xFF);
	        if (hex.length() == 1) {
	            sign.append("0");
	        }
	        sign.append(hex);
	    }
	    return sign.toString();
	}
}
相关推荐
郑祎亦27 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
本当迷ya40 分钟前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
捂月3 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn3 小时前
依赖注入注解
java·后端·spring
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪4 小时前
Django:从入门到精通
后端·python·django
一个小坑货4 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet274 小时前
【Rust练习】22.HashMap
开发语言·后端·rust