SpringBoot整合JWT取代session

前言

关于JWT(Json Web Token)的含义就不多做介绍了,最常见的是和shiro、springSercurity等一起使用来进行鉴权操作,这次主要记录下jwt在SpringBoot中的用法,有时间再研究下和SpringSercurity一起使用。

开始

  • IntelliJ IDEA 2018.1 x64
  • jdk8
  • SpringBoot 2.0.3

大致流程

1.首先在用户登录的时候生成token,返回给客户端。
2.客户端每次请求带上token,服务端在接收请求的时候取出token进行验证并且从token中获取当前用户信息
3.根据用户信息(用户相应的权限)来作出判断是否允许该请求通过。

redis作用

由于JWT生成的token在设置过期时间后我们是无法手动控制的,所以在进行登出操作后还携带token的话也是可以访问请他请求的,所以为避免这种情况,我们可以:
1.在登陆的时候存到redis缓存里面,把username作key,并设置缓存过期时间。
2.通过token获取用户信息,用username去查询redis中的token是否过期,过期则提示用户重新登陆。

添加依赖

本次项目用到所有依赖

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>

配置文件application.yml

1
2
3
4
5
6
7
8
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/XXX?characterEncoding=utf8
username: XXX
password: XXX
server:
port: 80

实体类UserBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author :mathias
* Description:
* Date: 2018/6/1
*/
@Entity
@Data
@Table(name = "user")
public class UserBean {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@Transient
private String token;
}

这里 @Transient表示不作数据关系映射

Dao层UserJpa

1
2
3
4
5
6
7
8
public interface UserJpa extends JpaRepository<UserBean,Long> {
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
UserBean findByUsername(String username);
}

Service层

UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface UserService {
/**
* 登录
* @param user
* @return
*/
UserBean auth(UserBean user);

/**
* 根据token获取用户信息
* @param token
* @return
*/
UserBean getUserByToken(String token);

/**
* 退出登录
* @param token
*/
void invalidate(String token);
}

UserServiceImpl

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
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserJpa userJpa;

@Autowired
private StringRedisTemplate redisTemplate;

@Override
public UserBean auth(UserBean user) {
if (StringUtils.isBlank(user.getUsername())){
throw new UserException(400,"用户名不能为空");
}
if (StringUtils.isBlank(user.getPassword())){
throw new UserException(400,"密码不能为空");
}

UserBean userBean = userJpa.findByUsername(user.getUsername());
if (userBean == null){
throw new UserException(400,"没有找到用户名为【"+user.getUsername()+"】的用户");
}
if (! user.getPassword().equals(userBean.getPassword())){
throw new UserException(400,"用户名或者密码错误");
}
// 登录生成token
onLogin(userBean);
return userBean;
}

@Override
public UserBean getUserByToken(String token) {
Map<String,String> map;
try {
map = JwtUtil.verifyToken(token);
}catch (Exception e){
throw new UserException(400,"用户未登录");
}
String username = map.get("username");
Long expire = redisTemplate.getExpire(username);
// 存入缓存的token已经过期
if (expire < 0L){
throw new UserException(400,"用户未登录");
}
// 刷新token
refreshToken(token,username);
UserBean userBean = userJpa.findByUsername(username);
if (userBean == null){
throw new UserException(400,"没有找到用户名为【"+username+"】的用户");
}
userBean.setToken(token);
return userBean;
}

@Override
public void invalidate(String token) {
Map<String, String> map = JwtUtil.verifyToken(token);
// 删除当前用户在redis中缓存的token
redisTemplate.delete(map.get("username"));
}

/**
* 登录并且生成token
* @param userBean
*/
private void onLogin(UserBean userBean) {
String token = JwtUtil.createToken(ImmutableMap.of("username", userBean.getUsername(),
"id", userBean.getId().toString()));
// 刷新token有效时间
refreshToken(token,userBean.getUsername());
userBean.setToken(token);
}

private void refreshToken(String token, String username) {
// 存入redis
redisTemplate.opsForValue().set(username,token);
// 设置过期时间
redisTemplate.expire(username,30,TimeUnit.MINUTES);
}

}

这里做了全局异常处理,所以直接抛出异常即可(不知道怎么设置全局异常处理?可以观看我之前写的SpringBoot全局异常处理)
注意:在登陆生成token的时候,不能放入敏感信息,如用户密码等,因为这个是可以通过base64破解的,jwt主要验证的是签名,只有签名部分是不可破解的。

工具类JwtUtil

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
public class JwtUtil {

private static final String SECRET = "session_secret";
private static final String ISSUER = "mathias";


/**
* 创建token
* @param claims
* @return
*/
public static String createToken(Map<String,String> claims){

Algorithm algorithm = Algorithm.HMAC256(SECRET);

JWTCreator.Builder builder = JWT.create().withIssuer(ISSUER)
.withExpiresAt(DateUtils.addDays(new Date(), 1));
claims.forEach(builder::withClaim) ;

return builder.sign(algorithm);
}

/**
* 验证token
* @param token
* @return
*/
public static Map<String,String> verifyToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
Map<String,String> map = Maps.newHashMap();
claims.forEach((k,v) -> map.put(k,v.asString()));
return map;
}
}

验证

方便验证,使用了postman
1.输入http://localhost/jwt/getUserByToken,在未登录的时候访问根据token获取用户信息的接口:

1
2
3
4
{
"code": 400,
"msg": "用户未登录"
}

2.进行登录操作,生成token,访问http://localhost/jwt/auth参数传入用户名和密码

1
2
3
4
5
6
{
"id": 8,
"username": "mathias",
"password": "XXXX",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYXRoaWFzIiwiaWQiOiI4IiwiZXhwIjoxNTI5NjU4Mjk3LCJ1c2VybmFtZSI6Im1hdGhpYXMifQ.UNJ9pyTT7QKl1a2cXdFT4igioJ2UhhLxwFsxXdlXSAg"
}

3.再次执行第一步,加上参数token,值就是第二步返回的token值

1
2
3
4
5
6
{
"id": 8,
"username": "mathias",
"password": "XXXX",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYXRoaWFzIiwiaWQiOiI4IiwiZXhwIjoxNTI5NjU4Mjk3LCJ1c2VybmFtZSI6Im1hdGhpYXMifQ.UNJ9pyTT7QKl1a2cXdFT4igioJ2UhhLxwFsxXdlXSAg"
}

返回了用户信息,说明成功
4.测试下登出操作,输入http://localhost/jwt/logout参数也是token,在执行第三步会发现信息已经是未登录状态了。

写在最后

这个只是一个简单的demo,在实际开发过程中我们的token一般会放在请求头Header里面或者是cookie中,毕竟我们不可能每请求一次就设置一下token参数。
以上纯属个人拙见,如果有纰漏、不足之处还请各位大佬指正,不胜感激…

-------------本文结束感谢您的阅读-------------