7

未经安全保护的API非常的危险

未经安全保护的API非常的危险,其和裸奔无异。即使API文档没有被人为泄露,通过简单的抓包也可以非常容易的获取到API的URL以及对应的请求参数。下面举几个未经保护的API可能将会造成的安全事故:

  1. 通过抓包,找到发送短信验证码的API。然后利用该API恶意的发送短信验证码。而发送短信验证码是需要收费的,这样的恶意攻击,将会导致无故的损失发送短信的费用。还会让很多不明真相的吃瓜群众收到奇怪的短信验证码,进而对产品产生不好的印象。

  2. 通过抓包,找到获取用户信息的API。该API的暴露,将会让该平台上所有用户的信息被毫无保留的暴露在互联网上。如果用户信息中还涉及到一些重要的个人信息。比如身份证号,手机号等。将会让用户受到人生和财产的损失。

  3. 通过抓包,找到和财产相关的API。比如说修改订单状态的API。一旦该API暴露,黑客可以恶意的修改用户的订单状态。比如说将订单的状态修改为取消。这可能将会直接对用户造成财产损失。

上面这些例子仅仅只是笔者临时想到的一些利用裸露API进行恶意攻击的方式,实际中还存在着更多由于未对API进行加密,而造成损失的情况。因此,未经安全保护的API非常的危险,对API进行安全保护异常重要。

什么是JWT

JWT是json web token的缩写。关于其如何确保数据传输的安全性的文章你可以在搜索引擎上找到很多,在这里我将仅仅简单介绍我对此的理解。

  1. JWT可以理解为一串通过特定算法生成的字符串,在API的请求中,将这段字符串放入请求参数中。API Server通过判断这段字符串是合法的还是伪造的,来确定这次API请求是否有效。通过该安全措施,将确保即使API被暴露,没有生成JWT字符串的算法,也没有办法成功调用API。

  2. JWT字符串分为两个部分(官方的说法是分为3个部分),分别是‘未加密部分’和‘加密部分’。而‘加密部分’的内容实际上是‘未加密部分’加密得到的。API Server检查JWT字符串是否有效的第一步是将‘加密部分’解密然后与‘未加密部分’进行比较,查看是否内容一致。如果内容不一致,则说明该JWT字符串是伪造的。

  3. JWT字符串中包括一个‘过期时间’的字段,当API Server获取到JWT字符串后,可以通过检查该字段与当前时间相比,是否已经处于过期的状态。如果‘过期时间’字段早于当前时间,则说明这次API请求是无效的。

  4. 你也可以在JWT字段种加入自定义的字段。然后在API Server获取到JWT字段后,通过这些自定义的字段判断是不是符合具体的业务逻辑,进而判断这次请求是不是有效。

利用jjwt实现JWT对API的保护

jjwtjava对JWT的封装,下面的方法将会演示。在java中如何利用jjwt实现API的保护

gradle依赖

compile 'io.jsonwebtoken:jjwt:0.7.0'

生成JWT字符串

public String buildJwt(Date exp) {
    String jwt = Jwts.builder()
            .signWith(SignatureAlgorithm.HS256,SECRET_KEY)//SECRET_KEY是加密算法对应的密钥,这里使用额是HS256加密算法
            .setExpiration(exp)//expTime是过期时间
            .claim("key","vaule")//该方法是在JWT中加入值为vaule的key字段
            .compact();
    return jwt;
}

判断JWT是否有效

public boolean isJwtValid(String jwt) {
    try {
        //解析JWT字符串中的数据,并进行最基础的验证
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)//SECRET_KEY是加密算法对应的密钥,jjwt可以自动判断机密算法
                .parseClaimsJws(jwt)//jwt是JWT字符串
                .getBody();
        String vaule = claims.get("key", String.class);//获取自定义字段key
        //判断自定义字段是否正确
        if ("vaule".equals(vaule)) {
            return true;
        } else {
            return false;
        }
    }
    //在解析JWT字符串时,如果密钥不正确,将会解析失败,抛出SignatureException异常,说明该JWT字符串是伪造的
    //在解析JWT字符串时,如果‘过期时间字段’已经早于当前时间,将会抛出ExpiredJwtException异常,说明本次请求已经失效
    catch (SignatureException|ExpiredJwtException e) {
        return false;
    }
}

Client端

Client端需要做的就是,根据API的需求将JWT字符串放入http请求中。我的做法是对于所有的API,在Client端生成JWT字段,然后将其添加到http请求的header中,确保所有的API都获得保护。对于一些比较敏感的信息,再用加一层JWT验证。比如说用户信息,在调用登录API后,API Server将会返回一个特定的JWT字符串,该JWT字段总将会包含该用户的userId。如果要获取用户信息,除了要将Client端生成的JWT字段放入请求,还需要将该JWT字符串放入请求。接下来展示一下利用OKHttp在http请求的header中加入JWT字段的代码:

//该方法将会在所有请求的header中加入jwt
public Response call(Request request) throws IOException {
    OkHttpClient client = new OkHttpClient();
    Request.Builder requestBuilder = request.newBuilder()
            .addHeader("commonJwt", jwtService.makeJwt());//加入Client本地生成的JWT字符串
    //加入登录成功后获取到的JWT字符串
    String userJwt = jwtService.getUserJwt();
    if (!StringUtils.isSpace(userJwt))
        requestBuilder.addHeader("userJwt", userJwt);
    request = requestBuilder.build();
    return client.newCall(request).execute();
}

API Server

API Server端需要做的就是,在收到API请求时,首先检查client端生成的JWT字段是否有效,然后如果该API涉及敏感信息,则检查检测特定的JWT字段是否有效。接下来展示一下在spring中利用aop进行JWT字段的验证:


@Pointcut("@annotation(org.springframework.web.bind.annotation.ResponseBody)")
public void onCommonAuth(){}

//所有的API都需要验证client生成的JWT字段是否有效
@Order(1)
@Around("onCommonAuth()")
public Object onCommonAuth(ProceedingJoinPoint joinPoint) throws Throwable {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String commonJwt = request.getHeader("commonJwt");
    if (jwtService.isCommonJwtValid(commonJwt)) {
        return joinPoint.proceed();
    } else {
        return "没有访问该API的权限";
    }
}

@Pointcut("execution(* com.demo.controller.UserController.getUserInfo(..))")
public void onGetByUserInfo() {}

//对获取用户信息API,坚持userJwt是否有效
@Order(2)
@Around("onGetByUserInfo()&&args(userId,..)")
public Object onGetByUserInfo(ProceedingJoinPoint joinPoint, Long userId) throws Throwable {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String userJwt = request.getHeader("userJwt");
    if (jwtService.isUserJwtValid(userJwt, userId)) {
        return joinPoint.proceed();
    } else {
        return "没有访问该API的权限";
    }
}

补充

  • JWT在一定程度上,保护了API的安全。但是其本身还是存在一定的缺陷的。比如说,一定JWT的加密密钥一旦被泄露,那么黑客就可以生成JWT字符串了,因此保护好JWT加密密钥非常重要。

  • 在上面的例子当中,介绍了获取用户信息API需要加入userJwt的例子。userJwt其实就是在JWT字符串中加入了userId字段,继而保证一个userJwt只能访问一个用户的信息。对于其他的API,比如说PUT和POST操作,需要新增和修改数据的API。可以将请求参数一并放入jwt中,以此来确保数据的安全性。否则黑客还可以在JWT字符串还没有过期的时间段内,修改请求中的参数,达到攻击的目的。

  • 另外还要防止重复式攻击,黑客还可以在JWT字符串还没有过期的时间段内,重复提交请求,达到攻击的目的。比如说新增订单的API,如果被黑客采用重复式攻击的方式,就会生成多个订单。

参考

RESTful Api 身份认证中的安全性设计探讨
JSON Web Token - 在Web应用间安全地传递信息
八幅漫画理解使用JSON Web Token设计单点登录系统
How to Create and verify JWTs in Java
jwt官方介绍
jwt官网
jjwt在github上的地址


kohoh_
99 声望2 粉丝

野生androd狗一只,目前全职远程开发。欢迎勾搭我。