API接口之JWT设置token过期时间(二)

目录

1.什么是Jwt

2.token是什么

3.为什么要使用token

4.如何实现token

5.JWT的简单案例

6.API接口token案例

6.1 token的创建

6.2 用户验证流程

7.测试流程

7.1 项目地址

7.2 表结构


1.什么是Jwt

Jwt是JSON Web Tokens的简称,从单词可以看出它也是一种token,其实可以理解为一种生成token的框架或规范。既然也是token那我们可以换一种问法,token是什么?为什么要使用token?

2.token是什么

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

3.为什么要使用token

说到这大家可能会想到,用服务器的session_id存储到cookies中也能做到,为什么非要用token呢?网上有许多文章来比较token和session的优缺点,其实笔者觉得,开发web应用的话用哪种都行。但如果是开发api接口,前后端分离,最好使用token,为什么这么说呢,因为session+cookies是基于web的。但是针对 api接口,可能会考虑到移动端,app是没有cookies和session的。

4.如何实现token

其实token只是一个概念模型,就是为了方便用户不用每次访问页面都输入用户名密码来做验证。开发者可以针对自己开发的应用定义自己的token。只要能做到不让黑客或者无聊的人钻系统漏洞即可。设想一下,我们自定义的token只是通过简单的MD5加密,一个黑客很容易找到token的加密规则,那么他就可以伪造其他用户登陆的token,可以随意的获取数据了。所以个人自定义的token,没有太多的加密经验,还是很不安全的。再者,自己定义token加密解密规则无形增加了工作量,有现成的为什么不用呢?

通过对token的模型总结,jwt作者总结了一套比较完善的token生成方案,它的加密方式非常安全,可以方便用户生成自己token。那么我们就看下token的流程和jwt的组成。

API接口之JWT设置token过期时间(二)_第1张图片

非常常见的一个架构,首先用户需要 通过登录等手段向服务端发送一个认证请求,服务端会返回给用户一个JWT(这个JWT的具体内容格式是啥后面会说,先理解成一个简单的字符串好了),此后用户向服务端发送的所有请求都要捎带上这个JWT,然后服务端会验证这个JWT的合法性,验证通过则说明用户请求时来自合法守信的客户端。
下面简单说一下这个JWT的格式,十分简单,就是一个三部分组成的字符串:

API接口之JWT设置token过期时间(二)_第2张图片

下面一部分一部分来讲:

header, 一个例子是:

API接口之JWT设置token过期时间(二)_第3张图片

非常简单,typ顾名思义就是type的意思,例如上面这里就指明是JWT的类型。alg顾名思义是algorithm的意思,指代一个加密算法,例如上面指代HS256(HMAC-SHA256),这个算法会在生成第三部分signature的时候用到。

payload,一个例子是:

API接口之JWT设置token过期时间(二)_第4张图片

这部分的本质是用户数据,怎么理解呢,就是JWT的目的是认证身份来源,那么你是不是得自报家门我是谁呢?所以总得往里塞点跟用户相关的信息吧,例如这里就是userId

payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims,
它的一个“属性值对”其实就是一个claim(要求),每一个claim的都代表特定的含义和作用。

注1:英文“claim”就是要求的意思

注2:如上面结构中的sub代表这个token的所有人,存储的是所有人的ID;name表示这个所有人的名字;admin表示所有人是否管理员的角色。当后面对JWT进行验证的时候,这些claim都能发挥特定的作用

注3:根据JWT的标准,这些claims可以分为以下三种类型:

A. Reserved claims(保留)
它的含义就像是编程语言的保留字一样,属于JWT标准里面规定的一些claim。JWT标准里面定义好的claim有:

iss(Issuser):代表这个JWT的签发主体;
sub(Subject):代表这个JWT的主体,即它的所有人;
aud(Audience):代表这个JWT的接收对象;
exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
jti(JWT ID):是JWT的唯一标识。

B. Public claims,略(不重要)

C. Private claims(私有)

这个指的就是自定义的claim,比如前面那个示例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证;而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行按照JWT标准的说明:保留的claims都是可选的,在生成payload不强制用上面的那些claim,你可以完全按照自己的想法来定义payload的结构,不过这样搞根本没必要:

第一是,如果把JWT用于认证, 那么JWT标准内规定的几个claim就足够用了,甚至只需要其中一两个就可以了,假如想往JWT里多存一些用户业务信息,比如角色和用户名等,这倒是用自定义的claim来添加;

第二是,JWT标准里面针对它自己规定的claim都提供了有详细的验证规则描述,每个实现库都会参照这个描述来提供JWT的验证实现,所以如果是自定义的claim名称,那么你用到的实现库就不会主动去验证这些claim
 

signature,一个例子是:

API接口之JWT设置token过期时间(二)_第5张图片

signature顾名思义就是签名,签名一般就是用一些算法生成一个能够认证身份的字符串,具体算法就是上面表示的,也比较简单,不赘述,唯一说明的一点是上面hash方法用到了一个secret,这个东西需要application server和authentication server双方都知道,相当于约好了同一把验证的钥匙,最终才好做认证。

至此,三个部分,都解释完了,那么按照header.payload.signature这个格式串起来就行了,串之前注意,header和payload也要做一个base64url encoded的转换。那么最终拼出来的一个例子是:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

再次强调一点,别看上面做了那么多hash,其实目的不在加密保护数据,而是为了认证来源,认证来源,认证来源。JWT不保证数据不泄露,因为JWT的设计目的就不是数据加密和保护。

最后再解释一下application server如何认证用户发来的JWT是否合法,首先服务端和客户端必须要有个约定,例如双方同时知道加密用的secret(这里假设用的就是简单的对称加密算法),那么在服务端 收到这个JWT是,就可以利用JWT前两段(别忘了JWT是个三段的拼成的字符串哦)数据作为输入,用同一套hash算法和同一个secret自己计算一个签名值,然后把计算出来的签名值和收到的JWT第三段比较,如果相同则认证通过,如果不相同,则认证不通过。就这么简单,当然,上面是假设了这个hash算法是对称加密算法,其实如果用非对称加密算法也是可以的,比方说我就用非对称的算法,那么对应的key就是一对,而非一个,那么一对公钥+私钥可以这样分配:私钥由客户端保存,公钥由服务端保存,服务端验证的时候,用公钥解密收到的signature,这样就得到了header和payload的拼接值,用这个拼接值跟前两段比较,相同就验证通过。总之,方法略不同,但大方向完全一样。
 

5.JWT的简单案例

public class Demo03 {

    private String secret = "a1g2y47dg3dj59fjhhsd7cnewy73j";
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    @Test
    public void test1() {// 生成JWT
        Map claims = new HashMap();
        claims.put("username", "zss");
        claims.put("age", 18);

        //生成token
        String token = Jwts.builder()
                .setClaims(claims)
                .setId("666")  //登录用户的id
                .setSubject("小马")  //登录用户的名称
                .setExpiration(new Date(System.currentTimeMillis() + 30*1000))//过期时间
                .setIssuedAt(new Date(System.currentTimeMillis()))//当前时间
                .signWith(SignatureAlgorithm.HS512, this.secret)//头部信息 第一个参数为加密方式为哈希512  第二个参数为加的盐为secret字符串
                .compact();

        System.out.println("token令牌是:"+token);

        Claims claims1 = Jwts.parser()
                .setSigningKey(this.secret)
                .parseClaimsJws(token)
                .getBody();

        Date d1 = claims1.getIssuedAt();
        Date d2 = claims1.getExpiration();
        System.out.println("username参数值:" + claims1.get("username"));
        System.out.println("登录用户的id:" + claims1.getId());
        System.out.println("登录用户的名称:" + claims1.getSubject());
        System.out.println("令牌签发时间:" + sdf.format(d1));
        System.out.println("令牌过期时间:" + sdf.format(d2));
    }

}

输出:

API接口之JWT设置token过期时间(二)_第6张图片

根据上面的简单案例,我们先体验一下JWT的操作方法。

6.API接口token案例

6.1 token的创建

   /**
     * 初始化生成token的参数
     * @param userId
     * @return String
     */
    public String generateToken(String userId) {
        Map claims = new HashMap<>(1);
        claims.put("sub", userId);
        return generateToken(claims);
    }

    /**
     * 生成token
     * @param claims
     * @return String
     */
    private String generateToken(Map claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(this.generateExpirationDate())
                .setIssuedAt(this.generateCurrentDate())
                .signWith(SignatureAlgorithm.HS512, this.secret)
                .compact();
    }

判断token是否可以刷新

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(this.secret)
                    .parseClaimsJws(token)
                    .getBody();
            final Date iat = claims.getIssuedAt();
            final Date exp = claims.getExpiration();
            if (iat.before(lastPasswordReset) || exp.before(generateCurrentDate())) {
                return false;
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }

刷新token

public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = Jwts.parser()
                    .setSigningKey(this.secret)
                    .parseClaimsJws(token)
                    .getBody();
            refreshedToken = this.generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

校验token

    public String verifyToken(String token) {
        String result = "";
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(this.secret)
                    .parseClaimsJws(token)
                    .getBody();
            result = TokenStatus.TOKEN_VALID;
        } catch (Exception e) {
            result = TokenStatus.TOKEN_INVALID;
        }
        return result;
    }

6.2 用户验证流程

用户登录

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public JSONResponse login(@RequestBody Map map) {
        String loginName = map.get("loginName");
        String password = map.get("password");
        User user1 = new User();
        user1.setName(loginName);
        user1.setPassword(password);
        //身份验证是否成功
        boolean isSuccess = userService.checkUser(user1);
        if (isSuccess) {
            User user = userService.getUserByLoginName(loginName);
            if (user != null) {
                //生成token,返回给客户端
                String token = jwtUtil.generateToken(user.getId());
                if (token != null) {
                    return JSONResponse.ok(token);
                }
            }
        }
        //返回登陆失败消息
        return JSONResponse.info("登陆失败");
    }

拦截器验证

 @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String token = request.getHeader("access_token");
        //token是否存在
        if (null != token) {
            //验证token是否正确
            String result = jwtUtil.verifyToken(token);
            if(result.equals(TokenStatus.TOKEN_INVALID)){//无效
                outputStream(servletResponse,"token令牌无效...");
            }else{//有效令牌,需要重新刷新token,再将token传回客户端,客户端会拿着新的token进行访问
                String refreshedToken = jwtUtil.refreshToken(token);
                System.out.println("refreshedToken:"+refreshedToken);
                // Access-Control-Allow-Origin就是我们需要设置的域名
                // Access-Control-Allow-Headers跨域允许包含的头。
                // Access-Control-Allow-Methods是允许的请求方式
//                response.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名
//                response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
                // response.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type, Accept");
                // 允许请求头Token
                // httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With, Content-Type, Accept, Token");
                // 允许客户端,发一个新的请求头jwt
//                response.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, jwt");
                // 允许客户端,处理一个新的响应头jwt
//                response.setHeader("Access-Control-Expose-Headers", "jwt");
                response.setHeader("access_token", refreshedToken);
            }
            filterChain.doFilter(request, response);
            return;
        }
        outputStream(servletResponse,"无token令牌...");
    }

向客户端返回响应信息

    /**
     * @description: 向客户端返回响应信息(json格式)
     * @author wangdong
     * @date 2019/10/8 16:46
     */
    private void outputStream(ServletResponse servletResponse,String message){
        try{
            String string = JSON.toJSONString(JSONResponse.info(message));
            servletResponse.setContentType("application/json;charset=UTF-8");
            servletResponse.getOutputStream().write(string.getBytes("UTF-8"));
            servletResponse.getOutputStream().close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

业务请求方法

    @RequestMapping(value = "/list",method = RequestMethod.POST)
    @ResponseBody
    public JSONResponse list(@RequestBody User user){
        System.out.println("进入了业务方法..."+user.getName());
        List list = userService.list(user.getName());
        return JSONResponse.ok(list);
    }

7.测试流程

登录接口 http://localhost:8080/login

API接口之JWT设置token过期时间(二)_第7张图片

请求业务接口 http://localhost:8080/user/list ,并且在header带上token参数

API接口之JWT设置token过期时间(二)_第8张图片

并且我们可以看到业务接口返回的header里也有刷新的token

API接口之JWT设置token过期时间(二)_第9张图片

下次再去请求业务接口,我们需要带上新的token,否则token到期会提示token无效

API接口之JWT设置token过期时间(二)_第10张图片

说明测试成功。

7.1 项目地址

https://github.com/loafer7423/signature

7.2 表结构

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `age` int(11) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Java)