令牌访问: Token & Jwt &Ticket &的应用

1,567 阅读7分钟

1) Token

定义

token 即使是在计算机领域中也有不同的定义,这里我们说的token,是指访问资源的凭据

  • 注册你的应用程序,注册完毕  会拿到认证信息(credentials)包括 ID 和 secret。 
    
  • 请求 access token, 通过代码取得 access token 参数直接放到header里

  • token 获取到之后,就能够带上 token 访问 API 对应资源

  • 服务端需要对token,签名,时间戳ts 进行验证(防止恶意无限制访问),只有token有效,时间戳未超时,签名有效才能被放行

  • 注意: access token 过期, 可以通过 refresh token 再次请求 access token。

token示例

{
	"success": true,
	"data": {
		"access_token": "93cdfdc3260996cf01508fb571e826",
		"expires_in": 1200000,
		"refresh_token": "93cdfdc3260996cf01508b571e826",
		"security_key": "K7ODHcfFAfzKhihGNLNljOq9aunGoE\u003d",
		"ts": 1650786844444
	},
	"code": "200"
}

image.png

应用场景

Open API即开放API,也称[开放平台]

JWT 与 Token 的区别

相同: 都是访问资源的令牌 都可以记录用户的信息 都是使服务端无状态变化 都是验证成功后,客户端才能访问服务端上受保护的资源 区别

Token: 服务端验证客户端发送过来的Token 时,还需要查询数据库获取用户信息,然后验证Token 是否有效 JWT: 将token 和 Payload 加密后存储于客户端,服务端只需要使用秘钥进行校验即可,不需要查询或者减少查询数据库,因为JWT自包含了用户信息和加密的数据

2)Ticket

定义

Ticket 类似前面说的token,也是是指访问资源的凭据。 类似入 门票, 这种认证机制不关系持票人,重点关系的是门票是否合法!

用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。 把它放置在ticket内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。

应用场景

登录认证

Sign签名 (防止参数恶意篡改)

把所有的参数拼接一起,再加入系统密钥,进行MD5计算生成一个sign签名,防止参数被人恶意篡改,后台按同样的方法生成签名,进行签名对比

/**
     * @param request
     * @return
     */
    public static Boolean checkSign(HttpServletRequest request,String sign){
        Boolean flag= false;
        //检查sigin是否过期
        Enumeration<?> pNames =  request.getParameterNames();
        Map<String, String> params = new HashMap<String, String>();
        while (pNames.hasMoreElements()) {
            String pName = (String) pNames.nextElement();
            if("sign".equals(pName)) continue;
            String pValue = (String)request.getParameter(pName);
            params.put(pName, pValue);
        }
        System.out.println("现在的sign-->>" + sign);
        System.out.println("验证的sign-->>" + getSign(params,secretKeyOfWxh));
        if(sign.equals(getSign(params, secretKeyOfWxh))){
            flag = true;
        }
        return flag;
    }

重复访问 (防止恶意请求)

引入一个时间戳参数Ts,保证Ticket仅在2分钟内有效,需要和客户端时间保持一致。

需要跟当前服务器时间 new Date 进行对比,如果超过2分钟,就拒绝本次请求,节省服务器查询数据的消耗

拦截器 (过滤非法请求)

每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。

public class LoginInterceptor implements HandlerInterceptor {

@Autowired
private RedisTemplate redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws IOException {
    JSONObject jsonObject = new JSONObject();
    String ticket = request.getParameter("ticket");
    String sign = request.getParameter("sign");
    String ts = request.getParameter("ts");
    
    
    if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){
        jsonObject.put("success",false);
        jsonObject.put("message","args is isEmpty");
        jsonObject.put("code","1001");
        PrintWriter printWriter = response.getWriter();
        printWriter.write(jsonObject.toJSONString());
        return false;
    }
    
    
    //如果redis存在ticket就认为是合法的请求
    if (redisTemplate.hasKey(ticket)){
         
        String values = (String) redisTemplate.opsForValue().get(ticket);
        
        
        //判断ticket是否即将过期,进行续命操作
        if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) < 20){
            redisTemplate.opsForValue().set(ticket,values,10L, TimeUnit.MINUTES);
        }
        
        
        
        //判断是否重复访问,存在重放攻击的时间窗口期
        if (SignUtils.getTimestamp() - Long.valueOf(ts) > 1200){
            jsonObject.put("success",false);
            jsonObject.put("message","Overtime to connect to server");
            jsonObject.put("code","1002");
            PrintWriter printWriter = response.getWriter();
            printWriter.write(jsonObject.toJSONString());
            return false;
        }
        
        
        //验证签名
        if (!SignUtils.checkSign(request,sign)){
            jsonObject.put("success",false);
            jsonObject.put("message","sign is invalid");
            jsonObject.put("code","1003");
            PrintWriter printWriter = response.getWriter();
            printWriter.write(jsonObject.toJSONString());
            return false;
        }
        return true;
    }else {
        jsonObject.put("success",false);
        jsonObject.put("message","ticket is invalid,Relogin.");
        jsonObject.put("code","1004");
        PrintWriter printWriter = response.getWriter();
        printWriter.write(jsonObject.toJSONString());
    }
    return false;
}

}

image.png

示例


/** 
     * 
     * Description:验证登录,ticket成功后放置缓存中,
     * @param
     * @author huangweicheng
     * @date 2020/12/31   
    */ 
    public JSONObject login(String username,String password){
        JSONObject result = new JSONObject();
        PersonEntity personEntity = personDao.findByLoginName(username);
        if (personEntity == null || (personEntity != null && !personEntity.getPassword().equals(password))){
            result.put("success",false);
            result.put("ticket","");
            result.put("code","999");
            result.put("message","用户名和密码不匹配");
            return result;
        }
        if (personEntity.getLoginName().equals(username) && personEntity.getPassword().equals(password)){
            String ticket = UUID.randomUUID().toString();
            ticket = ticket.replace("-","");
            redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(),10L, TimeUnit.MINUTES);
            result.put("success",true);
            result.put("ticket",ticket);
            result.put("code",200);
            result.put("message","登录成功");
            return result;
        }
        result.put("success",false);
        result.put("ticket","");
        result.put("code","1000");
        result.put("message","未知异常,请重试");
        return result;
    }

3) JWT

定义

JWT Java library

维基百科的定义JSON WEB TokenJWT,读作 [/dʒɒt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。

头信息指定了该JWT使用的签名算法:

header = '{"alg":"HS256","typ":"JWT"}'

HS256 表示使用了 HMAC-SHA256 来生成签名。

消息体包含了JWT的意图:

payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的时间

未签名的令牌由base64url编码的头信息和消息体拼接而成(使用"."分隔),签名则通过私有的key计算而成:

key = 'secretkey'  
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
signature = HMAC-SHA256(key, unsignedToken)

最后在未签名的令牌尾部拼接上base64url编码的签名(同样使用"."分隔)就是JWT了:

token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) 

# token看起来像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

JWT常常被用作保护服务端的资源(resource),客户端通常将JWT通过HTTP的Authorization header发送给服务端,服务端使用自己保存的key计算、验证签名以判断该JWT是否可信:

应用场景

防止:CSRF 攻击 跨站请求伪造Cross-site request forgery(简称CSRF, 读作 [sea-surf])是一种典型的利用cookie-session漏洞的攻击

“开票系统”

“签名系统”

JWT 规范确定了 12 种标准签名算法——3 种密钥算法和 9 种非对称密钥算法——由以下名称标识:

  • HS256: 使用 SHA-256 的 HMAC
  • HS384: 使用 SHA-384 的 HMAC
  • HS512: 使用 SHA-512 的 HMAC
  • ES256: ECDSA 使用 P-256 和 SHA-256
  • ES384: ECDSA 使用 P-384 和 SHA-384
  • ES512: ECDSA 使用 P-521 和 SHA-512
  • RS256:使用 SHA-256 的 RSASSA-PKCS-v1_5
  • RS384: RSASSA-PKCS-v1_5 使用 SHA-384
  • RS512: RSASSA-PKCS-v1_5 使用 SHA-512
  • PS256: RSASSA-PSS 使用 SHA-256 和 MGF1 和 SHA-256
  • PS384: 使用 SHA-384 的 RSASSA-PSS 和使用 SHA-384 的 MGF1
  • PS512: RSASSA-PSS 使用 SHA-512 和 MGF1 和 SHA-512

JWT 标准注册声明 方法

为 JWT 规范中定义的JwtBuilder标准注册声明名称提供了方便的 setter 方法。他们是:

JWT 自定义声明 方法

claim根据需要简单地调用一次或多次:

String jws = Jwts.builder()

    .claim("hello", "world")

创建 JWS 签名

您可以按如下方式创建 JWS:

  1. 使用该Jwts.builder()方法创建一个JwtBuilder实例。
  2. 调用JwtBuilder方法以根据需要添加标头参数和声明。
  3. 指定要用于签署 JWT的SecretKey或非对称。PrivateKey
  4. 最后,调用该compact()方法进行压缩和签名,生成最终的 jws。

例如:

String jws = Jwts.builder() // (1)
    .setSubject("Bob")      // (2) 
    .signWith(key)          // (3)
    .compact();             // (4)
    
    
    
    // Create a legitimate RSA public and private key pair:
    KeyPair kp = RsaProvider.generateKeyPair();
    PublicKey publicKey = kp.getPublic();
    PrivateKey privateKey = kp.getPrivate();
    String jwt = Jwts.builder()
    .setSubject("Joe")
    .signWith(SignatureAlgorithm.RS256, privateKey)
    .compact();

注意:不能使用PublicKeys 签署 JWT,因为这总是不安全的。 JJWT 将拒绝任何指定 PublicKey用于使用InvalidKeyException.

验证签名: 解析签名

  1. 使用该Jwts.parserBuilder()方法创建一个JwtParserBuilder实例。
  2. 指定要用于验证 JWS 签名的SecretKey或非对称的。PublicKey
  3. 调用 上的build()方法JwtParserBuilder以返回线程安全的JwtParser
  4. 最后,parseClaimsJws(String)使用您的 jws 调用该方法String,生成原始 JWS。
  5. 整个调用被包装在一个 try/catch 块中,以防解析或签名验证失败。稍后我们将介绍异常和失败的原因。
  6. 允许服务器时差 setAllowedClockSkewSeconds
Jws<Claims> jws;


try {
    jws = Jwts.parserBuilder()  // (1)
    .setSigningKey(key)         // (2)
    .build()                    // (3)
    .parseClaimsJws(jwsString); // (4)
    
    // we can safely trust the JWT
     
    catch (JwtException ex) {       // (5)
    
    // we *cannot* use the JWT as intended by its creator
}


SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();

Jwts.parserBuilder()

    .setSigningKeyResolver(signingKeyResolver) // <----
     .setAllowedClockSkewSeconds(seconds) // <----

    .build()
    .parseClaimsJws(jwsString);
    
    
 
    

使用RS256和Jwts.builder()生成令牌.signWith()生成签名 Jwts.parser()解析签名

使用并使用RS256算法产生一个令牌

@Component
@ConfigurationProperties("jwt.config")
@Data
public class JWTUtils {
   private  String key;//签名私钥
   private Long ttl;//签名失效时间
  /* private  String key="zcc-ynhrm";
   private int ttl=3600000;*/

   /**
    * 设置认证token
    *         参数:
    *          id:登陆用户id
    *          subject(主题):登陆用户名
    */
   public String createJWT(String id, String subject, Map<String,Object> map){
       //1.设置失效时间
       long now=System.currentTimeMillis();//当前毫秒数
       long exp=now+ttl;
       //2.创建jwtBuilder
       JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(subject)
               .setIssuedAt(new Date())
               .signWith(SignatureAlgorithm.HS256, key);
       //3.通过map设置claims,指定失效时间
       for (Map.Entry<String,Object> entry:map.entrySet()){
           jwtBuilder.claim(entry.getKey(),entry.getValue());
       }
       //jwtBuilder.setClaims(map);
       jwtBuilder.setExpiration(new Date(exp));
       //4.创建token
       String token = jwtBuilder.compact();
       return token;
   }

   /**
    * 解析token字符串获取clamis
    */

   public Claims parseJWT(String token){
       Claims claims = 
       Jwts.parser().setSigningKey(key)
       .parseClaimsJws(token)
       .getBody();
       return claims;
   }
}