1. 摘要
在实际业务开展过程中,我们经常遇到需要与第三方互联网公司进行技术对接的需求,比如支付宝支付对接、微信支付对接、高德地图查询对接等服务。 如果你是一家创业型互联网公司,大部分可能会接入其他公司的API接口。
当你的公司规模变大的时候,有的公司可能会开始找你进行技术对接,你会提供API接口。 这个时候,我们应该如何设计和保证API接口的安全呢?
2. 方案介绍
最常用的解决方案主要有两种类型:
2.1. 代币解决方案
其中,token方案是网络上应用最广泛的接口认证方案。 记得之前写过一篇文章《一步一步教你,使用JWT实现单点登录》,介绍的比较详细。 有兴趣熟悉的朋友可以看一下。 如果你不明白也没关系。 这里我们简单介绍一下token的解决方案。
如何保证API接口安全?
从上图我们可以清楚地看到,token解决方案的实现主要包括以下步骤:
实际使用中,当用户登录成功时像素游戏素材,生成的token存储在redis中是有时间限制的。 一般设置为2小时。 2小时后,它将自动失效。 这时候我们需要重新登录。 然后再次获取有效token。
Token方案是目前商业型项目中应用最广泛的方案,而且非常实用,可以有效防止黑客抓包、爬取数据。
但代币解决方案也有一些缺点! 最明显的一个是与第三方公司建立联系时。 当你的接口请求量很大的时候,token突然失效,大量的接口请求就会失败。
我对此有深刻的认识。 我记得很早的时候,我在和一家大中型互联网公司进行联调的时候3D植物,他们给我提供的接口对接方案就是token方案。 当时,在我们公司的流量高峰期,我要求他们的接口报告大量错误。 原因是令牌已过期。 当token过期时,我们会调用他们刷新token接口。 刷新完成后,在token过期到token重新刷新的时间间隔内,会出现大量请求失败日志。 ,所以在实际的API对接过程中,我不建议大家使用token的方案。
2.2. 接口签名
接口签名,顾名思义,就是通过一些签名规则对参数进行签名,然后将签名后的信息放入请求头中。 服务器收到客户端请求后,只需根据既定的规则产生相应的签名字符串即可。 比对客户端的签名信息。 如果一致,则进入业务处理流程; 否则,会提示签名验证失败。
如何保证API接口安全?
接口签名方案中,主要有四个核心参数:
签名生成规则分为两步:
//步骤一
String 参数1 = 请求方式 + 请求URL相对地址 + 请求Body字符串;
String 参数1加密结果= md5(参数1)
复制
//步骤二
String 参数2 = appsecret + timestamp + nonce + 参数1加密结果;
String 参数2加密结果= md5(参数2)
复制
参数2的加密结果就是我们想要的最终签名串。
接口签名方案,尤其是接口请求量较大时,保持稳定。
换句话说腾讯空间游戏接口对接程序开发,您可以将接口签名视为令牌方案的补充。
但如果要将接口签名方案扩展到前后端对接,答案是:不适合。
因为签名计算非常复杂,其次很容易泄露appsecret!
说了这么多,我们来用程序来实践一下吧!
2. 程序实践2.1、代币方案
如上所述,token解决方案的关键点在于,用户成功登录后,我们只需要生成对应的token,然后返回给前端即可。 下次请求业务接口时,需要带上token。
具体做法也可以分为两种:
下面,我们介绍第二种实现方法。
首先,编写一个jwt工具。
public class JwtTokenUtil {
//定义token返回头部
public static final String AUTH_HEADER_KEY = "Authorization";
//token前缀
public static final String TOKEN_PREFIX = "Bearer ";
//签名密钥
public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";
//有效期默认为 2hour
public static final Long EXPIRATION_TIME = 1000L*60*60*2;
/**
* 创建TOKEN
* @param content
* @return
*/
public static String createToken(String content){
return TOKEN_PREFIX + JWT.create()
.withSubject(content)
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(Algorithm.HMAC512(KEY));
}
/**
* 验证token
* @param token
*/
public static String verifyToken(String token) throws Exception {
try {
return JWT.require(Algorithm.HMAC512(KEY))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
} catch (TokenExpiredException e){
throw new Exception("token已失效,请重新登录",e);
} catch (JWTVerificationException e) {
throw new Exception("token验证失败!",e);
}
}
}
复制
然后,当我们登录时,我们生成一个令牌并将其返回给客户端。
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){
//...参数合法性验证
//从数据库获取用户信息
User dbUser = userService.selectByUserNo(userDto.getUserNo);
//....用户、密码验证
//创建token,并将token放在响应头
UserToken userToken = new UserToken();
BeanUtils.copyProperties(dbUser,userToken);
String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));
response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);
//定义返回结果
UserVo result = new UserVo();
BeanUtils.copyProperties(dbUser,result);
return result;
}
复制
最后编写一个统一的拦截器来验证客户端传入的token是否有效。
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从http请求头中取出token
final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
//如果不是映射到方法,直接通过
if(!(handler instanceof HandlerMethod)){
return true;
}
//如果是方法探测,直接通过
if (HttpMethod.OPTIONS.equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
//如果方法有JwtIgnore注解,直接通过
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method=handlerMethod.getMethod();
if (method.isAnnotationPresent(JwtIgnore.class)) {
JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
if(jwtIgnore.value()){
return true;
}
}
LocalAssert.isStringEmpty(token, "token为空,鉴权失败!");
//验证,并获取token内部信息
String userToken = JwtTokenUtil.verifyToken(token);
//将token放入本地缓存
WebContextUtil.setUserToken(userToken);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//方法结束后,移除缓存的token
WebContextUtil.removeUserToken();
}
}
复制
在生成token的时候,我们可以将一些用户的基本信息,比如用户ID、用户名等,存储到token中。 这样,token认证通过后,我们只需要解析里面的信息就可以获取对应的用户了。 ID,可以省去在数据库中查询一些基本信息的操作。
同时,在使用过程中尽量不要存储敏感信息,因为很容易被黑客解析!
2.2. 接口签名
同样的思路,从服务器端验证的角度来看腾讯空间游戏接口对接程序开发,我们可以先写一个签名拦截器来验证客户端传入的参数是否合法。 只要其中一个参数非法,就会提示错误。
具体代码实践如下:
public class SignInterceptor implements HandlerInterceptor {
@Autowired
private AppSecretService appSecretService;
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//appId验证
final String appId = request.getHeader("appid");
if(StringUtils.isEmpty(appId)){
throw new CommonException("appid不能为空");
}
String appSecret = appSecretService.getAppSecretByAppId(appId);
if(StringUtils.isEmpty(appSecret)){
throw new CommonException("appid不合法");
}
//时间戳验证
final String timestamp = request.getHeader("timestamp");
if(StringUtils.isEmpty(timestamp)){
throw new CommonException("timestamp不能为空");
}
//大于5分钟,非法请求
long diff = System.currentTimeMillis() - Long.parseLong(timestamp);
if(Math.abs(diff) > 1000 * 60 * 5){
throw new CommonException("timestamp已过期");
}
//临时流水号,防止重复提交
final String nonce = request.getHeader("nonce");
if(StringUtils.isEmpty(nonce)){
throw new CommonException("nonce不能为空");
}
//验证签名
final String signature = request.getHeader("signature");
if(StringUtils.isEmpty(nonce)){
throw new CommonException("signature不能为空");
}
final String method = request.getMethod();
final String url = request.getRequestURI();
final String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
String signResult = SignUtil.getSignature(method, url, body, timestamp, nonce, appSecret);
if(!signature.equals(signResult)){
throw new CommonException("签名验证失败");
}
//检查是否重复请求
String key = appId + "_" + timestamp + "_" + nonce;
if(redisUtil.exist(key)){
throw new CommonException("当前请求正在处理,请不要重复提交");
}
//设置5分钟
redisUtil.save(key, signResult, 5*60);
request.setAttribute("reidsKey",key);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//请求处理完毕之后,移除缓存
String value = request.getAttribute("reidsKey");
if(!StringUtils.isEmpty(value)){
redisUtil.remove(value);
}
}
}
复制
签名工具类SignUtil:
public class SignUtil {
/**
* 签名计算
* @param method
* @param url
* @param body
* @param timestamp
* @param nonce
* @param appSecret
* @return
*/
public static String getSignature(String method, String url, String body, String timestamp, String nonce, String appSecret){
//第一层签名
String requestStr1 = method + url + body + appSecret;
String signResult1 = DigestUtils.md5Hex(requestStr1);
//第二层签名
String requestStr2 = appSecret + timestamp + nonce + signResult1;
String signResult2 = DigestUtils.md5Hex(requestStr2);
return signResult2;
}
}
复制
对于签名计算,可以改用hamc方法进行计算,思路大致相同。
三、总结
上面介绍的token和接口签名方案可以对外保护所提供的接口,防止他人篡改请求或模拟请求。
但数据本身缺乏安全保护,即请求的参数和返回的数据可能被别人截获并获取,而这些数据都是明文的,所以只要被截获,相应的业务就会被截获。可以获得数据。
这种情况下,建议您对请求参数和返回参数进行加密,例如RSA、AES等加密工具。
同时,在生产环境中,使用https进行传输可以起到很好的安全防护作用。