基于JWT的Token认证

1、什么是JWT

JSON Web Tokens,是一种开发的行业标准规范RFC 7519。广泛的用在系统的认证和数据交换方面。

2、JWT结构

JWT 由三个部分依次组成

  • Header(头部)
  • Payload(载荷)
  • Signature(签名)

2.1、Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,包含算法和token类型。
需要对json进行base64url加密

{
 "alg": "HS256",
 "typ": "JWT"
}

2.2、Payload

用来存放实际需要传递的数据。
需要json进行base64url加密

里面的前五个字段都是由JWT的标准所定义的,并且也支持自定义字段

iss: 该JWT的签发者 
sub: 该JWT所面向的用户 
aud: 接收该JWT的一方 
exp(expires): 什么时候过期,这里是一个Unix时间戳 
iat(issued at): 在什么时候签发的

#自定义字段
name:hello

2.3、Signature

把前两段的base密文通过·拼接起来,使用HS256加密

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

例如

eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MTQ2Puk_fv6cyfk0B1j7vbIqw_Q

3、JWT认证流程

  • 客户端发送(用户名、密码)身份信息至服务器端;
  • 服务器端对客户端发送身份信息进行校验,校验通过后,生成token字符串;
  • 服务器端将生成的token字符串发送给客户端(服务器端不保存token);
  • 客户端接收到token后,将token保存到cookie或者localstorage;
  • 客户端每次向服务器端发送请求,携带token(通过header、cookie等方式);
  • 服务器端收到请求,首先验证token是否合法过期(可通过拦截器方式),返回对应信息;

4、JWT认证和传统session认证区别

4.1、基于session的认证

http协议是一种无状态的协议,本身是无法对访问的客户端进行识别。
采用session机制,客户端在服务端登陆成功之后,服务端会生成一个sessionID,返回给客户端,客户端将sessionID保存到cookie中,再次发起请求的时候,携带cookie中的sessionID到服务端,服务端会缓存该session(会话),当客户端请求到来的时候,服务端就知道是哪个用户的请求,并将处理的结果返回给客户端。

存在问题:

  • session保存在服务端,当客户访问量增加时,服务端就需要存储大量的session会话,对内存压力增大
  • 分布式集群环境存在session共享问题
  • CSRF攻击: 基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

4.2、基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,服务端不需要保存认证信息或者会话信息

5、JWT特点

跨域访问:基于Token的访问策略可以克服cookies的跨域问题
无状态token:token无状态,session有状态的
使用前后端分离、移动端:Cookie手机端不支持
跨平台:语言无关性,标准规范
避免了CSRF 攻击

JWT撤销问题:无法在服务器端撤销,只能辅助逻辑判断处理

6、JWT续约

jwt受自身机制原因,payload具有过期时间,参与签名过程,过期时间改动,签名发生改变,因此jwt本身是不支持续签的。但是结合一些方案来辅助实现。

  • 每次请求刷新token。每次请求都返回一个新的 jwt 给客户端,但产生性能上问题;
  • 要过期token预刷新。服务器端判断token是否将要过期(如10min内过期),则更新token,返回最新token客户端,但触发刷新时间无法判断,可能后10min内没有请求发送,则token无法更新;
  • 第三方组件redis 等保存过期时间。结合redis保存token,改变了token的无状态性;
  • refreshToken机制。1个 acessToken 设置过期时间 ,如半个小时,另一个是 refreshToken 过期时间如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。但存在问题是:1、客户端操作更复杂;2、用户注销的时候需要同时保证两个 token 都无效;3、重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)

7、项目实战

package com.spring.util;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 测试
 *
 * @author yilei
 * @className DemoController
 * @date 2021/3/1 20:33
 **/
@Controller
public class DemoController {

    /**
     * 获取access_token
     *
     * @param clientId     客户端ID
     * @param clientSecret 客户端secret
     * @return java.lang.String
     * @author yilei
     * @date 2021-03-01 21:32
     */
    @RequestMapping(value = "tokens")
    @ResponseBody
    public String tokens(String clientId, String clientSecret) {
        Map map = new HashMap<>(16);
        // 验证客户端ID,密钥是否匹配
        if (StringUtils.equals("ddddd", clientId) && StringUtils.equals("sssss", clientSecret)) {
            // 生成access_token
            String token = JwtTokenUtil.generateToken(clientId);
            map.put("code", 1);
            map.put("access_token", token);
            Date expiration = JwtTokenUtil.getClaimsFromToken(token).getExpiration();
            map.put("expires_in", DateUtil.between(expiration, new Date(), DateUnit.MS));
        } else {
            map.put("code", -1);
            map.put("msg", "授权信息不正确!");
        }
        return JSON.toJSONString(map);
    }

    /**
     * 校验token
     *
     * @param token
     * @param clientId 客户端ID
     * @return java.lang.String
     * @author yilei
     * @date 2021-03-01 21:33
     */
    @RequestMapping(value = "tokens/check")
    @ResponseBody
    public String tokensCheck(@RequestHeader String token, @RequestParam String clientId) {
        Map map = new HashMap<>(16);
        boolean flag = JwtTokenUtil.validateToken(token, clientId);
        if (flag) {
            map.put("code", 1);
            map.put("client_id", clientId);
            Date expiration = JwtTokenUtil.getClaimsFromToken(token).getExpiration();
            map.put("expires_in", DateUtil.between(expiration, new Date(), DateUnit.MS));
        } else {
            map.put("code", -1);
            map.put("msg", "非法token或token已过期!");
        }
        return JSON.toJSONString(map);
    }
}

package com.spring.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 *
 * @author yilei
 * @className JwtTokenUtil
 * @date 2021/3/1 20:33
 **/
@Component
@PropertySource("classpath:conf/jwt.properties")
public class JwtTokenUtil implements InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);

    private static final String CLAIM_KEY_CLIENT_ID = "client";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expire}")
    private Long expire;

    private static String JWT_SECRET;
    private static Long JWT_EXPIRE;

    @Override
    public void afterPropertiesSet() {
        JWT_SECRET = secret;
        JWT_EXPIRE = expire;
    }

    /**
     * 生成token
     *
     * @param clientId
     * @return java.lang.String
     * @author yilei
     * @date 2021-03-01 20:04
     */
    public static String generateToken(String clientId) {
        Map claims = new HashMap<>(3);
        claims.put(CLAIM_KEY_CLIENT_ID, clientId);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 根据claims生成token
     *
     * @param claims
     * @return java.lang.String
     * @author yilei
     * @date 2021-03-01 20:04
     */
    public static String generateToken(Map claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRE * 1000))
                .signWith(SignatureAlgorithm.HS512, JWT_SECRET)
                .compact();
    }

    /**
     * 根据token获取claims
     *
     * @param token
     * @return io.jsonwebtoken.Claims
     * @author yilei
     * @date 2021-03-01 20:04
     */
    public static Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(JWT_SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

    /**
     * 验证token是否合法
     * 1、client_id是否匹配
     * 2、expire是否过期
     *
     * @param token
     * @param clientId
     * @return boolean
     * @author yilei
     * @date 2021-03-01 20:04
     */
    public static boolean validateToken(String token, String clientId) {
        Claims claims = getClaimsFromToken(token);
        if (null == claims) {
            return false;
        }
        Object cId = claims.get(CLAIM_KEY_CLIENT_ID);
        if (null == cId) {
            return false;
        }
        return StringUtils.equals(clientId, cId.toString()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     *
     * @param token
     * @return boolean
     * @author yilei
     * @date 2021-03-01 20:04
     */
    public static boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiredDate = claims.getExpiration();
        return expiredDate.before(new Date());
    }

    public static void main(String[] args) {
        String token = JwtTokenUtil.generateToken("123456");
        System.out.println(token);
        System.out.println("============");
        String aa = "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MTQ2MDg3NjgsImNsaWVudCI6IjEyMzQ1NiIsImNyZWF0ZWQiOjE2MTQ2MDg3MDg2MDV9.DyVpgKZ_2_8fP1gQdNYzdH5pI5JM7diw1ivXTHGtl1ayH6KQn3K3pRxGn1xrQRVzJyz6flLooY2_611XE8RNZA";
        Claims claimsFromToken = JwtTokenUtil.getClaimsFromToken(aa);
        System.out.println(claimsFromToken);
    }
}

获取access_token

var settings = {
  "url": "http://localhost:8080/spring_jwt/tokens.do?clientId=ddddd&clientSecret=sssss",
  "method": "GET",
  "timeout": 0,
};

$.ajax(settings).done(function (response) {
  console.log(response);
});

{“access_token”:“eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MTQ2NDk3OTUsImNsaWVudCI6ImRkZGRkIiwiY3JlYXRlZCI6MTYxNDY0OTczNTI5OH0.EsKmthI44vBXdrl3ZkOb2tQGdds2T90LJ1CKXsjj4dge2JCW4Rw1c5MbMwwzwaGoDbaGE09Fwav6KTnFGqI-qg”,“code”:1,“expires_in”:59193}

校验token

var settings = {
  "url": "http://localhost:8080/spring_jwt/tokens/check.do?clientId=ddddd",
  "method": "GET",
  "timeout": 0,
  "headers": {
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MTQ2NDk3OTUsImNsaWVudCI6ImRkZGRkIiwiY3JlYXRlZCI6MTYxNDY0OTczNTI5OH0.EsKmthI44vBXdrl3ZkOb2tQGdds2T90LJ1CKXsjj4dge2JCW4Rw1c5MbMwwzwaGoDbaGE09Fwav6KTnFGqI-qg"
  },
};

$.ajax(settings).done(function (response) {
  console.log(response);
});

{“code”:1,“expires_in”:5206,“client_id”:“ddddd”}

源码spring-jwt

你可能感兴趣的:(License,jwt,session,token)