JWT登陆认证

2019/4/21 jwt

基于客户端的用户登录认证( 轻量安全,服务端不用记录用户状态信息(无状态) )

# 使用场景

  • 分布式的登录认证
  • Token 可以是无状态的,可以在多个服务间共享

# 初识JWT

# jwt的组成

jwt的组成方式: header.payload.signature

第一部分我们称它为头部(header), 用于存放签名的生成算法

{
  "alg": "HS256",
  "typ": "JWT"
}
1
2
3
4

第二部分我们称其为载荷(payload),用于存放内容

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
1
2
3
4
5

第三部分是签证(String signature), 一旦header和payload被篡改,验证将失败

//secret为加密算法的密钥,密钥只能由服务端和客户端知悉
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),your-256-bit-secret
) 
1
2
3
4
5

# jwt实例

  • 最终的JWT串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1

# JWT登陆认证

# 认证原理

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。
  • JWT登录授权过滤器,拦截请求,从每个请求中获取token,从token中获取负载,从负载中获取用户名放入SpringSecurity中,之后认证授权由SpringSecurity框架管理。
  • 第一次登陆还没有token,用户的登陆信息由SpringSecurity管理认证通过之后,生成jwtToken返回给客户端保存,客户端之后发送请求头携带我们需要的token即可

# 引入pom依赖

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
1
2
3
4
5
6

# JwtTokenUtil工具类

  • 主要包括生成token,设置过期时间,验证token是否有效,获取token负载等
  • 工具类可具体参考mall项目
package com.macro.mall.tiny.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

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

/**
 * JwtToken生成的工具类
 * Created by macro on 2018/4/26.
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}",token);
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

# 客户端添加请求头

$.ajax({
	url: "./test.php",
	type: "POST",
    headers: {
   		"Accept" : "text/plain; charset=utf-8",
   		"Content-Type": "text/plain; charset=utf-8"
    },
    /*
    beforeSend: function(jqXHR, settings) {
    	jqXHR.setRequestHeader('Accept', 'text/plain; charset=utf-8');
    	jqXHR.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');
    },
    */
    data: {"user" : "min", "pass" : "he"},
    error: function(jqXHR, textStatus, errorThrown) {
    	//....
    },
    success: function(data, textStatus, jqXHR) {
    	//....
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# jwt+redis使用流程

  1. 登录:用户第一次登录,校验通过,生成token并存到redis里,返回客户端(token设置过期时间1天,redis设置过期时间0.5小时)。

  2. 鉴权:之后客户端每次请求都携带token,服务端校验token通过并且redis过期时间没过,则redis续期然后返回token;服务端校验token通过但如果redis中token已过期,则失效。

  3. 登出:服务端校验token通过,将redis中的token删除。

# 安全问题

  1. 如果其他用户获得我们电脑上的token,那么他们就能模仿真实用户进行操作(黑客监控电脑,获得网站发送的token,进行操作)?

    对于敏感的api接口,需使用https 。https是在http超文本传输协议加入SSL层,它在网络间通信是加密的,所以需要加密证书。

  2. 我们是否可以伪造用户token进行访问?

    不可以,因为你无法使用服务器的签名,就是你自己的密钥签名的信息服务器识别不了。

  3. 我们是否可以修改token中body的信息?

    不能,修改了之后签名信息就不正确,然后就无法验证签名,说明数据被修改了。

  4. app登录后,服务端返回一个token,app存在客户端,下次再打开app时,直接读token,传token到服务端做验证,免去重新输入用户密码的麻烦,这个token存储在header里,目前看大多数app都是这样做,但如果黑客抓包获取到token,伪造http 请求,对服务器做操作,那岂不是很不安全。。。

    这方法确实不好啊,不能只依赖于http的header里的东西来认证,太容易模仿。最简单的方法可能就是走https,客户端只要接受服务器端的签名即可。

    理解:签名就是验证信息的唯一性,如果中间人进行获得token,那么无法进行公钥签名,服务器获得token之后还要进行解密的,因此这个方法可以进行避免攻击。

# 总结

  • 优点:在非跨域环境下使用JWT机制是一个非常不错的选择,实现方式简单,操作方便,能够快速实现。由于服务端不存储用户状态信息,因此大用户量,对后台服务也不会造成压力。

  • 缺点:跨域实现相对比较麻烦,安全性也有待探讨。因为JWT令牌返回到页面中,可以使用js获取到,如果遇到XSS攻击令牌可能会被盗取,在JWT还没超时的情况下,就会被获取到敏感数据信息。

  • 对于安全问题:

    对于敏感的api接口,需使用https 。https是在http超文本传输协议加入SSL层,它在网络间通信是加密的,所以需要加密证书。采用https 或者 代码层面也可以做安全检测,比如ip地址发生变化,MAC地址发生变化等等,可以要求重新登录

# 其他

此生不换
青鸟飞鱼