zoukankan      html  css  js  c++  java
  • 手机端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

  • 相关阅读:
    【Spring Framework】10、代理模式
    【Spring Framework】8、使用注解开发
    Codeforces 516E
    AtCoder Grand Contest 055 题解
    Codeforces 1606F
    贪心/构造/DP 杂题选做
    整数拆分最大乘积
    CSP-S2021 被碾压记
    洛谷 P2791
    LCT 小记
  • 原文地址:https://www.cnblogs.com/soinve/p/9304087.html
Copyright © 2011-2022 走看看