如何设计并保证API接口安全呢?-

如何设计并保证API接口安全呢?-

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进行传输可以起到很好的安全防护作用。

文章来源:https://cloud.tencent.com/developer/article/1851564