JWT確保API的安全

轉載:https://segmentfault.com/a/1190000007119872

未經安全保護的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,如果被黑客採用重複式攻擊的方式,就會生成多個訂單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章