手機端API接口驗證及參數簽名驗證


問題背景:

后端服務對手機APP端開放API,沒有基本的校驗就是裸奔,別人抓取接口后容易惡意請求,不要求嚴格的做的安全,但是簡單的基礎安全屏障是要建立的,再配合HTTPS使用,這樣使后端服務盡可能的安全。

對接口安全問題,采用JWT對接口進行token驗證,判斷請求的有效性,目前對JWT解釋的博客文章很多,對JWT不了解的可以查找相關資料,JWT官網

JWT是JSON Web Token的簡寫,一些是JWT官網的解釋:


 

什么是JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

看不懂的可以用Google翻譯:

JSON Web Token(JWT)是一個開放標准(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON對象安全地傳輸信息。 此信息可以通過數字簽名進行驗證和信任。 JWT可以使用密鑰(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰進行簽名。

JWT的結構是怎樣的?

JWT主要由三部分構成,

  • Header  頭部,說明使用JWT的類型,和使用的算法
  • Payload  中間體,定義的一些有效數據,比如簽發者,簽發時間,過期時間等等,具體可查看RFC7519,除了一些公共的屬性外,可以定義一些私有屬性,用於自己的業務邏輯。
  • Signature  簽名,創建簽名,base64UrlEncode對header和Payload進行處理后,再根據密鑰和頭部中定義的算法進行簽名。如下格式:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
//生成的Token如下樣式
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJFU0JQIiwibmFtZSI6IuWImOWFhuS8nyIsImV4cCI6MTUzMTQ0OTExNSwiaWF0IjoxNTMxNDQ5MDg1LCJqdGkiOjEsImFjY291bnQiOiIxNTAwMTEwMTUzNiJ9.4IEi95xcOQ4SfXvjz34bBC8ECej56jiMuq7Df4Vd9YQ

 


 

具體實現:

1. maven構建,可以查看Github

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2. 創建Token

 1 import com.alibaba.fastjson.JSONObject;
 2 import com.woasis.wos.api.UserClaim;
 3 import io.jsonwebtoken.Claims;
 4 import io.jsonwebtoken.JwtBuilder;
 5 import io.jsonwebtoken.Jwts;
 6 import io.jsonwebtoken.SignatureAlgorithm;
 7 
 8 import javax.crypto.spec.SecretKeySpec;
 9 import javax.xml.bind.DatatypeConverter;
10 import java.security.Key;
11 
12 public class JwtHandler {
13 
14     //簽發者
15     private static final String ISSUER = "iss";
16     //簽發時間
17     private static final String ISSUED_AT = "iat";
18     //過期時間
19     private static final String EXPIRATION_TIME = "exp";
20     private static final Long EXPIRATION_TIME_VALUE = 1000*30L;
21     //JWT ID
22     private static final String JWT_ID = "jti";
23     //密鑰
24     private static final String SECRET = "AAAABBBCCC";
25 
26     /**
27      * 構造Token
28      * @param userId 用戶ID
29      * @param userName  用戶名稱
30      * @param phone  手機號
31      * @return
32      */
33     public static String createToken(Integer userId, String userName, String phone) {
34 
35         //采用HS256簽名算法對token進行簽名
36         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
37 
38         //當前系統時間
39         long nowMillis = System.currentTimeMillis();
40 
41         //采用密鑰對JWT加密簽名
42         byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
43         Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
44 
45         //構造payload
46         JSONObject payload = new JSONObject();
47         payload.put(ISSUER, "ESBP");
48         payload.put(ISSUED_AT, nowMillis/1000);
49         payload.put(JWT_ID, userId);
50         payload.put("account", phone);
51         payload.put("name",userName);
52         //設置過期時間
53         long expMillis = nowMillis + EXPIRATION_TIME_VALUE;
54         payload.put(EXPIRATION_TIME, expMillis/1000);
55 
56         //設置JWT參數
57         JwtBuilder builder = Jwts.builder()
58                 .setPayload(payload.toJSONString())
59                 .signWith(signatureAlgorithm, signingKey);
60         //構造token字符串
61         return builder.compact();
62     }
63 }

3. 解析JWT

    private static Logger logger = LoggerFactory.getLogger(JwtHandler.class);

    /**
     * JWT解析
     * @param jwt
     * @return
     */
    public static UserClaim parseJWT(String jwt) {
        Claims claims = Jwts.parser()
                .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
                .setAllowedClockSkewSeconds(100) //設置允許過期時間,在構造token的時候有設置過期時間,此處是指到了過期時間之后還允許多少秒有效,且此token可以解析
                .parseClaimsJws(jwt).getBody();

        UserClaim userClaim = new UserClaim();
        userClaim.setAccount((String) claims.get("account"));
        userClaim.setName((String) claims.get("name"));
        userClaim.setJti(claims.getId());
        userClaim.setIss(claims.getIssuer());
        userClaim.setIat(claims.getIssuedAt());
        userClaim.setExp(claims.getExpiration());
        logger.debug("parseJWT UserClaim:"+JSONObject.toJSONString(userClaim));
        return userClaim;
    }

特別說明:

在jjwt源碼文件JwtMap.java中有這么個方法toDate(),在解析數據的時候這個地方按秒對時間處理的,所以在設置簽發時間或過期時間的時候要設置秒。

 protected static Date toDate(Object v, String name) {
        if (v == null) {
            return null;
        } else if (v instanceof Date) {
            return (Date) v;
        } else if (v instanceof Number) {
            // https://github.com/jwtk/jjwt/issues/122:
            // The JWT RFC *mandates* NumericDate values are represented as seconds.
            // Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
            long seconds = ((Number) v).longValue();
            long millis = seconds * 1000;
            return new Date(millis);
        } else if (v instanceof String) {
            // https://github.com/jwtk/jjwt/issues/122
            // The JWT RFC *mandates* NumericDate values are represented as seconds.
            // Because Because java.util.Date requires milliseconds, we need to multiply by 1000:
            long seconds = Long.parseLong((String) v);
            long millis = seconds * 1000;
            return new Date(millis);
        } else {
            throw new IllegalStateException("Cannot convert '" + name + "' value [" + v + "] to Date instance.");
        }
    }

4. 攔截器使用

要想對api進行控制,就要使用攔截器,或是過濾器,提問:攔截器和過濾器的區別是什么?此處采用攔截器進行控制。

攔截器具體實現代碼:

import com.woasis.wos.api.UserClaim;
import com.woasis.wos.common.exception.ExceptionEnum;
import com.woasis.wos.common.exception.WosException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Token驗證攔截器
 */
public class TokenInterceptor implements HandlerInterceptor {

    private static Logger logger = LoggerFactory.getLogger(TokenInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {

        logger.debug("path:"+httpServletRequest.getRequestURI());
        String token = httpServletRequest.getParameter("token");
        String userId = httpServletRequest.getParameter("id");

        if (!StringUtils.isBlank(token)){
            UserClaim claim = null;
            try {
                claim = JwtHandler.parseJWT(token);
            }catch (ExpiredJwtException e){//token過期
                throw new WosException(ExceptionEnum.EXPIRATION_TIME);
            }catch (SignatureException e){//簽名被篡改
                throw new WosException(ExceptionEnum.SIGNATUREEXCEPTION);
            }
            if (claim != null && userId != null){
                if (userId.equals(claim.getJti())){

                    return true;
                }else {//token用戶非請求用戶,非法請求
                    throw new WosException(ExceptionEnum.ILLEGAL_REQUEST);
                }
            }else {
                throw new WosException(ExceptionEnum.ILLEGAL_REQUEST);
            }
        }else {//token為空,非法請求
            throw new WosException(ExceptionEnum.ILLEGAL_REQUEST);
        }
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

在Spring Boot中攔截器的使用:

import com.woasis.wos.api.util.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WosAppConfigurer extends WebMvcConfigurerAdapter {

    //排除攔截的請求路徑
    private static String[] excludePatterns = new String[]{"/oauth/login"};

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**").excludePathPatterns(excludePatterns);

        super.addInterceptors(registry);

    }
}

5. 效果測試

模擬獲取token

模擬token過期

模擬token中簽名被篡改


參數簽名://TODO

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM