Jwt+Gateway+nacos+redis实现免登录

Spring Cloud Gateway作为网关在请求得到路由前进行过滤并分配请求到他指定的路由,Spring Cloud Alibana Nacos作为服务注册使得网关能够服务发现,分配路由,jwt作为token令牌解决传统的单点登录设置cookie和session登录方式,也解决了分布式情况下登录问题

1. 整体实现

虽说token无状态,减少交互数据库,但token无法主动失效,用户注销情况下,仍可以拿着token请求服务器直接登录,故集合redis,加入一个黑名单,当用户注销时,token就加入黑名单,使用户无法直接操作,只能发送重新登录请求

1.1. 响应设计

token 响应状态
无token 50000 - 无访问权限
有token,token格式不正确 50008 - 非法token
有token,用户持有的token已被加入到黑名单,即用户被注销 50010 - 用户已登出
有token,用户正在使用的token和服务端保存的用户正在使用的不一致 50010 - 用户已登出
有token,用户在线时间超时 50014 - token失效

1.2. redis设计

  • JWT_USERNAME::id

    字符串类型 存入 用户名

    用于保证单端登录

    有效时间为用户免登录时间

  • JWT_TOKEN

    Hash类型 存入 id-token

    用于记录当前用户【正在使用】的token

  • JWT_BLACKLIST::group

    字符串类型 存入 token

    group来自token的载荷中UUID生成的group,用于唯一的对应该token

    用于注销,重新登录,刷新操作导致用户被注销,token未失效时,使token自动失效

    判断token时,当黑名单中拥有该token,则返回用户被注销

1.3. 登录操作中redis操作

1.3.1. 登录-login

验证账号密码后,验证 【get JWT_USERNAME::id】是否有值,有值则不能登录

无值,用户名存入【set JWT_USERNAME::id username EX time】,防止其他端登录

生成token,存入【hset JWT_TOKEN id token】,保存正在使用的token

返回token给客户端

1.3.2. 登出-logout

删除用户名【del JWT_USERNAME::id】

删除用户当前使用的token 【hdel JWT_TOKEN id】

将token加入黑名单【set JWT_BLACKLIST::group token EX time】

1.3.3. 刷新-refresh

当用户注销,token过期情况下,token可以进行刷新,重新获得免登录权限

将token加入黑名单 【set JWT_BLACKLIST::group token EX time】

重新生成token

更新用户当前使用的token 【hset JWT_TOKEN id token】

更新用户名的有效时间【expire JWT_USERNAME::id time】

返回token

1.3.4. 重新登录-relogin

当用户篡改token,导致token和redis中用户使用的token不一致,但用户名依旧有效情况下,用户无法登录,无法注销,进行重新登录

验证账号密码

删除用户名【del JWT_USERNAME::id】

删除用户当前使用的token 【hdel JWT_TOKEN id】

将token加入黑名单【set JWT_BLACKLIST::group token EX time】

用户名存入【set JWT_USERNAME::id username EX time】,防止其他端登录

生成token,存入【hset JWT_TOKEN id token】,保存正在使用的token

1.3.5. 获取用户信息-getInfo

获取token中的用户信息,不交互redis

1.4. 网关过滤登录路由

网关过滤 路由
直接放行 /jwt-client/login
无token
有token,token格式不正确
else /jwt-client/relogin,/jwt-client/refresh
有token,用户持有的token已被加入到黑名单,即用户被注销
有token,用户正在使用的token和服务端保存的用户正在使用的不一致
有token,用户在线时间超时
else /jwt-client/logout
else /jwt-client/getInfo

2. 项目

2.1. jwt-api 公共接口类

2.1.1. Maven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 简化实体类 get/set方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>

<!--swagger 用于生成、描述、调用和可视化 RESTful 风格的 Web 服务-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

2.1.2. 定义实体类,统一返回类型

2.1.2.1. Admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;

@Data
public class Admin {

private String id;

private String username;

private String password;

public boolean equal(String name,String pwd){
if (username.equals(name)&&password.equals(pwd)){
return true;
}
return false;
}
}

2.1.2.2. R

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
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

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

@Data
@ApiModel(value = "全局统一返回结果")
public class R {

@ApiModelProperty(value = "是否成功")
private Boolean success;

@ApiModelProperty(value = "返回码")
private Integer code;

@ApiModelProperty(value = "返回消息")
private String message;

@ApiModelProperty(value = "返回数据")
private Map<String,Object> data = new HashMap<String,Object>();

public static R ok(){
R r = new R();
r.setSuccess(true);
r.setCode(20000);
r.setMessage("操作成功");
return r;
}
public static R error(){
R r = new R();
r.setSuccess(false);
r.setCode(20001);
r.setMessage("操作失败");
return r;
}

//链式编程
public R success(Boolean success){
this.setSuccess(success);
return this;
}

public R message(String message){
this.setMessage(message);
return this;
}

public R code(Integer code){
this.setCode(code);
return this;
}

public R data(Map<String,Object> map){
this.setData(map);
return this;
}

public R data(String key,Object value){
this.data.put(key,value);
return this;
}

}

2.1.2.3. JwtClient

1
2
3
4
5
6
7
8
9
10
11
12
public class JwtConstant {

public static final String tokenHeader = "Authorization";

public static final String CLAIM_KEY_USERID = "id";
public static final String CLAIM_KEY_USERNAME = "username";
public static final String CLAIM_KEY_CREATED = "created";
public static final String CLAIM_KEY_HOLDTIME = "holdtime";
//用于区分token,充当存入redis中的key
public static final String CLAIM_KEY_GROUP = "group";
}

2.2. jwt-Gateway web项目

2.2.1. Maven依赖

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
<dependency>
<groupId>com.runaccepted.jwt</groupId>
<artifactId>jwt-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>


<!-- 文件配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2.2.2. 跨域设置-定义CorsWebFilter的Bean

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class GatewayCorsConfiguration {

//跨域
@Bean
public CorsWebFilter corsWebFilter(){

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

CorsConfiguration configuration = new CorsConfiguration();

configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.addAllowedOrigin("*");
configuration.setAllowCredentials(true);

source.registerCorsConfiguration("/**",configuration);

CorsWebFilter filter = new CorsWebFilter(source);

return filter;
}
}

2.2.3. nacos服务注册 -application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 9500


spring:
application:
name: jwt-gateway

cloud:
nacos:
discovery:
server-addr: 192.168.0.100:8848

gateway:
routes:
- id: jwt-route
uri: lb://jwt-client
predicates:
- Path=/jwt-client/**

2.2.4. jwt配置-application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#过滤路由
auth.skip.uris=/jwt-client/login
#判断token请求格式的路由
auth.skip.checktoken=/jwt-client/token/refresh,/jwt-client/relogin

#jwt设置
jwt.secret.key=online-runaccepted
jwt.subject.name=edu-admin
#jwt有效期 2分钟
jwt.expire.time=120000
#免登录截止时间 天/小时/分钟/秒/微妙
#Calendar.DATE=5 HOUR=10 MINUTE=12 SECOND=13 MILLISECOND=14
jwt.hold.type=12
jwt.hold.time=10
#令牌黑名单,用于用户注销/登出/修改账号密码时
jwt.blacklist.format=JWT_BLACKLIST::%s
#令牌名单,当前活跃的jwt令牌
jwt.token.format=JWT_TOKEN

#redis
spring.redis.host=192.168.0.100
spring.redis.port=6379

2.2.5. 🌟🌟JwtUtils工具类

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package com.runaccepted.jwt.gateway.utils;

import com.runaccepted.jwt.api.constant.JwtConstant;
import com.runaccepted.jwt.api.entity.Admin;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.*;

/**
* JwtToken生成的工具类
*
* JWT token的格式:header.payload.signature
*
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"id":1,"sub":"wang","created":1489079981393,"exp":1489684781}
*/
@Slf4j
@Component
public class JwtUtils {

@Value("${jwt.subject.name}")
private String SUBJECT;

//秘钥
@Value("${jwt.secret.key}")
private String APPSECRET;

//过期时间,毫秒,30分钟
@Value("${jwt.expire.time}")
private long EXPIRE;

@Value("${jwt.hold.time}")
private int holdTime;

@Value("${jwt.hold.type}")
private int holdType;

/**
* 根据用户信息生成token
*/
public String generateToken(Admin admin) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put(JwtConstant.CLAIM_KEY_USERID, admin.getId());
claims.put(JwtConstant.CLAIM_KEY_USERNAME, admin.getUsername());
claims.put(JwtConstant.CLAIM_KEY_CREATED, new Date());
claims.put(JwtConstant.CLAIM_KEY_HOLDTIME,generateLoginDate());
claims.put(JwtConstant.CLAIM_KEY_GROUP,generateGroup());
return generateToken(claims);
}

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

/**
* 从token中获取JWT中的负载
*/
public Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(APPSECRET)
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException e) {
String id = (String) e.getClaims().get(JwtConstant.CLAIM_KEY_USERID);
String username = (String) e.getClaims().get(JwtConstant.CLAIM_KEY_USERNAME);
log.error("JWT载荷中 用户ID:{} 用户名:{}", id, username);

claims=e.getClaims();
} catch (MalformedJwtException e){
log.error("Json格式错误 {}",e.getLocalizedMessage());
} catch (SignatureException e){
log.error("Json格式错误 {}",e.getLocalizedMessage());
} catch(IllegalArgumentException e){
log.error("错误 {}",e.getLocalizedMessage());
}
return claims;
}

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

/**
* 生成token的免登录时间
*/
public Date generateLoginDate() {

//有效期内可刷新token
Calendar calendar = new GregorianCalendar();
//当天+2
calendar.add(holdType,holdTime);

return calendar.getTime();
}

/**
* 生成token的group
*/
public String generateGroup() {

String group = UUID.randomUUID().toString();
group = group.replace(".","");

return group;
}

/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {

Claims claims = getClaimsFromToken(token);
String username = (String) claims.get(JwtConstant.CLAIM_KEY_USERNAME);

return username;
}

/**
* 从token中获取过期时间
*/
public Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
Date expiredDate = claims.getExpiration();
log.error("token中过期时间 {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredDate));
return expiredDate;
}

/**
* 从token中获取group
*/
public String getGroupFromToken(String token) {
Claims claims = getClaimsFromToken(token);
String group = (String)claims.get(JwtConstant.CLAIM_KEY_GROUP);
log.error("token中的用户组 {}", group);
return group;
}

/**
* 从token中获取登录用户名id
*/
public String getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
String id = (String) claims.get(JwtConstant.CLAIM_KEY_USERID);
return id;
}

/**
* 从token中获取登录截止时间
*/
public Date getHoldTime(String token){
Claims claims = getClaimsFromToken(token);
long dateTime = (long)claims.get(JwtConstant.CLAIM_KEY_HOLDTIME);
Date date = new Date(dateTime);
log.info("原数据值:{} 该token免登录时间截止至 {}",dateTime,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
return date;
}

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

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

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

/**
* 免登录截止时间判断
*/
public boolean isHoldTime(String token){
Date date = getHoldTime(token);
return new Date().before(date);
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}


/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(JwtConstant.CLAIM_KEY_CREATED, new Date());
claims.put(JwtConstant.CLAIM_KEY_GROUP,generateGroup());
//网关仅更新token有效期,不更新免登录时间
//claims.put(JwtConstant.CLAIM_KEY_HOLDTIME,generateLoginDate());
return generateToken(claims);
}
}

2.2.6. 🌟AuthFilter implements GlobalFilter, Ordered

主要是 Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)

其中可以解析出路由过来的请求头信息,并按条件过滤请求,还可以自定义返回结果

为了在免登录期间请求资源问题:token是在2分钟后就失效的,在线失效时间是10分钟,在判断token仅为token过期情况下,向路由传token时,就传刷新token有效时间后的token,不刷新免登录时间,从而保证载荷内容的一致性,也不用2分钟就更新传输过来的token值,当该token的登录时间到期才算真正的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
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.runaccepted.jwt.api.constant.JwtConstant;
import com.runaccepted.jwt.api.to.R;
import com.runaccepted.jwt.gateway.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Component
@ConfigurationProperties(prefix = "auth.skip")
@Data
public class AuthFilter implements GlobalFilter, Ordered {

private List<String> uris;

private List<String> checktoken;

@Value("${jwt.blacklist.format}")
private String jwtBlacklist;

@Value("${jwt.token.format}")
private String jwtToken;


@Autowired
JwtUtils jwtUtils;

@Autowired
StringRedisTemplate redisTemplate;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {


ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type","application/json; charset=utf-8");

String path = request.getURI().getPath();

//如果访问路径在定义过滤路径之中,直接放行
boolean containUri=this.uris.contains(path);
if (containUri){
return chain.filter(exchange);
}
log.error("放行路径{},当前路径 {},是否放行 {}",Arrays.asList(uris),path,containUri);

String token = "";
//得到请求头中Authorization的token值
List<String> tokenHead = request.getHeaders().get(JwtConstant.tokenHeader);
if (tokenHead!=null){
token=tokenHead.get(0);
}

//验证token
//没有token,没有权限
if (StringUtils.isEmpty(token)){

//50000: no token
DataBuffer dataBuffer = createResponseBody(50000,"无访问权限",response);

return response.writeWith(Flux.just(dataBuffer));
}

//有token,token不合法
Claims claim = jwtUtils.getClaimsFromToken(token);
if(claim==null){
//50008: Illegal token
DataBuffer dataBuffer = createResponseBody(50008,"非法token",response);
return response.writeWith(Flux.just(dataBuffer));
}

String username = jwtUtils.getUserNameFromToken(token);
String id = jwtUtils.getUserIdFromToken(token);
String group = jwtUtils.getGroupFromToken(token);
//没有有效载荷,token定义为非法
if (StringUtils.isEmpty(username)
||StringUtils.isEmpty(id)
||StringUtils.isEmpty(group)){
DataBuffer dataBuffer = createResponseBody(50008,"非法token",response);
return response.writeWith(Flux.just(dataBuffer));
}

//token可用性判断后 才可以刷新和重新登录
boolean checkUri = this.checktoken.contains(path);
if (checkUri){
return chain.filter(exchange);
}
log.error("验证token后放行路径{},当前路径 {},是否放行 {}",Arrays.asList(checktoken),path,checkUri);


//有token,但已被加入黑名单,只能选择再登录
String key = String.format(jwtBlacklist,group);
String blackToken=redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(blackToken)){

//50010: Token out;
DataBuffer dataBuffer = createResponseBody(50010,username+" 已登出",response);

return response.writeWith(Flux.just(dataBuffer));
}

// redis中id对应的token不存在
// 或者请求中的token和redis中活跃的token不匹配,只能选择再登录
String redisToken = (String)redisTemplate.opsForHash().get(jwtToken,id);
if (StringUtils.isEmpty(redisToken)||!redisToken.equals(token)){
//50010: Token out;
DataBuffer dataBuffer = createResponseBody(50010,username+" 已登出",response);
return response.writeWith(Flux.just(dataBuffer));
}

//有身份,过免登录时间
if(!jwtUtils.isHoldTime(token)){

//50014: Token expired;
DataBuffer dataBuffer = createResponseBody(50014,"token过期",response);
return response.writeWith(Flux.just(dataBuffer));
}

//token有效期内,可以进行登出
boolean expiredTimeUri = path.equals("/jwt-client/logout");
if (expiredTimeUri){
return chain.filter(exchange);
}
log.error("当前路径 {},是否放行 {}",path,expiredTimeUri);

//token 失效
if(jwtUtils.canRefresh(token)){

String refreshToken = jwtUtils.refreshToken(token);
//更新请求头
ServerHttpRequest httpRequest = request.mutate().header(JwtConstant.tokenHeader, refreshToken).build();
ServerWebExchange webExchange = exchange.mutate().request(httpRequest).build();
return chain.filter(webExchange);
}
return chain.filter(exchange);

}

private DataBuffer createResponseBody(int code,String message,ServerHttpResponse response){

R result = R.error().code(code).message(message);
ObjectMapper objectMapper = new ObjectMapper();
String str="";
try {
str=objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
log.error("json转换错误 {}",e.getLocalizedMessage());
}
DataBuffer dataBuffer = response.bufferFactory().wrap(str.getBytes());
return dataBuffer;
}
@Override
public int getOrder() {
return 0;
}
}

2.3. jwt-Client -web项目

2.3.1. Maven依赖

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
<dependency>
<groupId>com.runaccepted.jwt</groupId>
<artifactId>jwt-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2.3.2. jwt配置

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
server.port=9000
spring.application.name=jwt-client

#nacos
spring.cloud.nacos.discovery.server-addr=192.168.0.100:8848

#jwt设置
jwt.secret.key=online-runaccepted
jwt.subject.name=edu-admin
#jwt有效期 2分钟
jwt.expire.time=120000
#免登录截止时间 天/小时/分钟/秒/微妙
#Calendar.DATE=5 HOUR=10 MINUTE=12 SECOND=13 MILLISECOND=14
jwt.hold.type=12
jwt.hold.time=10
#存入redis中的key
#单端登录限制
jwt.username.format=JWT_USERNAME::%s
#令牌黑名单,用于用户注销/登出/修改账号密码时
jwt.blacklist.format=JWT_BLACKLIST::%s
#令牌名单,当前活跃的jwt令牌
jwt.token.format=JWT_TOKEN

#id,用户名 密码 - 从数据库中取得
login.id=1249426830067269633
login.username=admin
login.password=123456

#redis
spring.redis.host=192.168.0.100
spring.redis.port=6379

2.3.3. 🌟🌟JwtUtils工具类

和网关中的唯一不同就是刷新时同时刷新在线时间

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package com.runaccepted.jwt.client.utils;

import com.runaccepted.jwt.api.constant.JwtConstant;
import com.runaccepted.jwt.api.entity.Admin;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.*;

/**
* JwtToken生成的工具类
*
* JWT token的格式:header.payload.signature
*
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"id":1,"sub":"wang","created":1489079981393,"exp":1489684781}
*/
@Slf4j
@Component
public class JwtUtils {

@Value("${jwt.subject.name}")
private String SUBJECT;

//秘钥
@Value("${jwt.secret.key}")
private String APPSECRET;

//过期时间,毫秒,30分钟
@Value("${jwt.expire.time}")
private long EXPIRE;

@Value("${jwt.hold.time}")
private int holdTime;

@Value("${jwt.hold.type}")
private int holdType;

/**
* 根据用户信息生成token
*/
public String generateToken(Admin admin) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put(JwtConstant.CLAIM_KEY_USERID, admin.getId());
claims.put(JwtConstant.CLAIM_KEY_USERNAME, admin.getUsername());
claims.put(JwtConstant.CLAIM_KEY_CREATED, new Date());
claims.put(JwtConstant.CLAIM_KEY_HOLDTIME,generateLoginDate());
claims.put(JwtConstant.CLAIM_KEY_GROUP,generateGroup());
return generateToken(claims);
}

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

/**
* 从token中获取JWT中的负载
*/
public Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(APPSECRET)
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException e) {
String id = (String) e.getClaims().get(JwtConstant.CLAIM_KEY_USERID);
String username = (String) e.getClaims().get(JwtConstant.CLAIM_KEY_USERNAME);
String group = (String)e.getClaims().get(JwtConstant.CLAIM_KEY_GROUP);
log.error("JWT载荷中 用户ID:{} 用户名:{} 所处组:{}", id, username,group);

claims=e.getClaims();
} catch (MalformedJwtException e){
log.error("Json格式错误 {}",e.getLocalizedMessage());
} catch (SignatureException e){
log.error("Json格式错误 {}",e.getLocalizedMessage());
} catch(IllegalArgumentException e){
log.error("错误 {}",e.getLocalizedMessage());
}
return claims;
}

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

/**
* 生成token的免登录时间
*/
public Date generateLoginDate() {

//有效期内可刷新token
Calendar calendar = new GregorianCalendar();
//当天+2
calendar.add(holdType,holdTime);

return calendar.getTime();
}

/**
* 生成token的group
*/
public String generateGroup() {

String group = UUID.randomUUID().toString();
group = group.replace("-","");

return group;
}

/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {

Claims claims = getClaimsFromToken(token);
String username = (String) claims.get(JwtConstant.CLAIM_KEY_USERNAME);

return username;
}

/**
* 从token中获取过期时间
*/
public Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
Date expiredDate = claims.getExpiration();
log.error("token中过期时间 {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredDate));
return expiredDate;
}

/**
* 从token中获取group
*/
public String getGroupFromToken(String token) {
Claims claims = getClaimsFromToken(token);
String group = (String)claims.get(JwtConstant.CLAIM_KEY_GROUP);
log.error("token中的用户组 {}", group);
return group;
}

/**
* 从token中获取登录用户名id
*/
public String getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
String id = (String) claims.get(JwtConstant.CLAIM_KEY_USERID);
return id;
}

/**
* 从token中获取登录截止时间
*/
public Date getHoldTime(String token){
Claims claims = getClaimsFromToken(token);
long dateTime = (long)claims.get(JwtConstant.CLAIM_KEY_HOLDTIME);
Date date = new Date(dateTime);
log.info("原数据值:{} 该token免登录时间截止至 {}",dateTime,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
return date;
}

/**
* 从token中获取登录截止时间
*/
public long getLoginDate(String token) {
Date date=getHoldTime(token);
return date.getTime();
}

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

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

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

/**
* 免登录截止时间判断
*/
public boolean isHoldTime(String token){
Date date = getHoldTime(token);
return new Date().before(date);
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}

/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(JwtConstant.CLAIM_KEY_CREATED, new Date());
claims.put(JwtConstant.CLAIM_KEY_HOLDTIME,generateLoginDate());
//新的group key 区分黑名单中的key
claims.put(JwtConstant.CLAIM_KEY_GROUP,generateGroup());
return generateToken(claims);
}
}

2.3.4. 🌟ClientController

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package com.runaccepted.jwt.client.controller;

import com.runaccepted.jwt.api.constant.JwtConstant;
import com.runaccepted.jwt.api.entity.Admin;
import com.runaccepted.jwt.api.to.R;
import com.runaccepted.jwt.client.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/jwt-client")
@Slf4j
public class ClientController {

@Autowired
JwtUtils jwtUtils;

@Value("${login.id}")
private String id;

@Value("${login.username}")
private String username;

@Value("${login.password}")
private String password;

@Value("${jwt.username.format}")
private String jwtUsername;

@Value("${jwt.blacklist.format}")
private String jwtBlacklist;

@Value("${jwt.token.format}")
private String jwtToken;

@Autowired
StringRedisTemplate redisTemplate;

@ApiOperation(value = "登录")
@PostMapping("/login")
public R login(@RequestBody Admin admin){

if (!admin.equal(username,password)) {

return R.error().message("账号或密码错误");

}else{

admin.setId(id);
String key = String.format(jwtUsername,admin.getId());
log.error("redis key: {}",key);
//判断redis中是否存在该用户名
String name = (String) redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(name)){
return R.error().message(name+" 已经登录!");
}
//成功生成token
String token= jwtUtils.generateToken(admin);
//用户名有效时间 - 用户免登录时间
//得到jwt中的截止时间
long time=jwtUtils.generateLoginDate().getTime();

long expired = time-new Date().getTime();

log.error("原始数据: {} redis {} 截止时间: {}",time,key,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time)));
//信息放入redis - set key value EX 10
redisTemplate.opsForValue().set(key,admin.getUsername(),expired,TimeUnit.MILLISECONDS);
//存当前id对应正在使用的token
//hset key field value
redisTemplate.opsForHash().put(jwtToken,admin.getId(),token);
log.error("redis hashKey: {} field: {} token:{}",jwtToken,admin.getId(),token);
return R.ok().data("token",token);

}
}

@ApiOperation(value = "登录")
@PostMapping("/relogin")
public R relogin(@RequestBody Admin admin,HttpServletRequest request){

if (!admin.equal(username,password)) {

return R.error().message("账号或密码错误");

}else{
admin.setId(id);
String token = request.getHeader(JwtConstant.tokenHeader);
//删除用户名
String userKey = String.format(jwtUsername,admin.getId());
redisTemplate.delete(userKey);
//删除用户token
redisTemplate.opsForHash().delete(jwtToken,id);
//token放入黑名单
String group = jwtUtils.getGroupFromToken(token);
long time= jwtUtils.generateLoginDate().getTime();
long expired = time - new Date().getTime();
log.error("黑名单 - 原始数据: {} redis {} 截止时间: {}",time,userKey,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time)));

String blackKey = String.format(jwtBlacklist,group);
//可能token已过期
if(expired>0) {
redisTemplate.opsForValue().set(blackKey, token, expired, TimeUnit.MILLISECONDS);
}

//重新生成用户名有效时间 - 用户免登录时间
admin.setId(id);
String newToken = jwtUtils.generateToken(admin);
//得到jwt中的截止时间
time=jwtUtils.generateLoginDate().getTime();
expired = time-new Date().getTime();

log.error("重新登录 原始数据-: {} redis {} 截止时间: {}",time,userKey,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time)));
//信息放入redis - set key value EX 10
redisTemplate.opsForValue().set(userKey,admin.getUsername(),expired,TimeUnit.MILLISECONDS);
//存当前id对应正在使用的token
//hset key field value
redisTemplate.opsForHash().put(jwtToken,admin.getId(),newToken);
log.error("redis hashKey: {} field: {} token:{}",jwtToken,admin.getId(),token);
return R.ok().data("token",newToken);
}
}

@ApiOperation(value = "根据jwt得到信息")
@GetMapping("/getInfo")
public R getInfo(HttpServletRequest request){

String token = request.getHeader(JwtConstant.tokenHeader);

log.info("请求头 {}",token);

String username = jwtUtils.getUserNameFromToken(token);

return R.ok().data("username",username);
}

@ApiOperation(value = "清除token,登入")
@GetMapping("/logout")
public R logout(HttpServletRequest request){

String token = request.getHeader(JwtConstant.tokenHeader);

log.info("logout 请求头 {}",token);

String id = jwtUtils.getUserIdFromToken(token);
//删除登录的用户名
String userKey = String.format(jwtUsername,id);
redisTemplate.delete(userKey);

//删除id当前使用的token
redisTemplate.opsForHash().delete(jwtToken,id);
//token放入黑名单
String group = jwtUtils.getGroupFromToken(token);
long time= jwtUtils.getLoginDate(token);
long expired = time - new Date().getTime();
log.error("logout 原始数据: {} redis {} 截止时间: {}",time,userKey,
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time)));
String blackKey = String.format(jwtBlacklist,group);
if (expired>0) {
redisTemplate.opsForValue().set(blackKey, token, expired, TimeUnit.MILLISECONDS);
}

return R.ok().message("注销成功");
}

@ApiOperation(value = "刷新token")
@GetMapping(value = "/token/refresh")
public Object refreshToken(HttpServletRequest request) {
//1、获取请求头中的Authorization完整值
String oldToken = request.getHeader(JwtConstant.tokenHeader);
String refreshToken = "";

//2、是否可以进行刷新(未过有效时间/是否在免登录范围)
// if(!jwtUtils.canRefresh(oldToken)|| jwtUtils.isHoldTime(oldToken)){
// return R.error().message("jwt还未失效,无需刷新").code(20001);
// }

//再次获得免登录机会
long time = jwtUtils.generateLoginDate().getTime();
long expired = time - new Date().getTime();

refreshToken = jwtUtils.refreshToken(oldToken);

String id = jwtUtils.getUserIdFromToken(refreshToken);
//原token放入黑名单
String group = jwtUtils.getGroupFromToken(oldToken);
String key = String.format(jwtBlacklist,group);
if (expired>0) {
redisTemplate.opsForValue().set(key, oldToken, expired, TimeUnit.MILLISECONDS);
}
//当前使用的token进行修改
redisTemplate.opsForHash().put(jwtToken,id,refreshToken);
//更新用户有效时间
String userkey = String.format(jwtUsername,id);

redisTemplate.expire(userkey,expired,TimeUnit.MILLISECONDS);

Date date = jwtUtils.getHoldTime(refreshToken);

//将新的token交给前端
return R.ok().data("token",refreshToken).data("date",date);
}
}

3. 测试 Postman

3.1. http://localhost:9500/jwt-client/login

服务端记录

1
2
3
ERROR 47138 --- [nio-9000-exec-4] c.r.j.c.controller.ClientController      : redis key: JWT_USERNAME::1249426830067269633           
ERROR 47138 --- [nio-9000-exec-4] c.r.j.c.controller.ClientController : 原始数据: 1586891867067 redis JWT_USERNAME::1249426830067269633 截止时间: 2020-04-15 03:17:47
ERROR 47138 --- [nio-9000-exec-4] c.r.j.c.controller.ClientController : redis hashKey: JWT_TOKEN field: 1249426830067269633 token:eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVkIjoxNTg2ODkxMjY1OTkyLCJpZCI6IjEyNDk0MjY4MzAwNjcyNjk2MzMiLCJob2xkdGltZSI6MTU4Njg5MTg2NTk5MiwiZXhwIjoxNTg2ODkxMzg2LCJ1c2VybmFtZSI6ImFkbWluIiwiZ3JvdXAiOiIwOGI5MzcxZGY1NTk0ZWIwOWRhODI5MWNkZDc3M2Y0MSJ9.FOv1VOQAhe9vWL4ZlsRZ1wkfrc47i-QU0_Rcn3baokg5AZn4eje-5J2x2wJjp0g7_1s2wsns_FQL1u1EEwAkcw

3.2. http://localhost:9500/jwt-client/getInfo

服务端记录

1
2
3
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.filter.AuthFilter        : 当前路径 /jwt-client/getInfo,是否放行 false
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.utils.JwtUtils : JWT载荷中 用户ID:1249426830067269633 用户名:admin
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.utils.JwtUtils : token中过期时间 2020-04-15 03:09:46

3.3. http://localhost:9500/jwt-client/logout

服务端记录

1
2
3
4
5
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.filter.AuthFilter        : 放行路径[[/jwt-client/login]],当前路径 /jwt-client/logout,是否放行 false
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.utils.JwtUtils : JWT载荷中 用户ID:1249426830067269633 用户名:admin
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.utils.JwtUtils : token中的用户组 08b9371df5594eb09da8291cdd773f41
ERROR 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.filter.AuthFilter : 验证token后放行路径[[/jwt-client/token/refresh, /jwt-client/relogin]],当前路径 /jwt-client/logout,是否放行 false
INFO 42712 --- [ctor-http-nio-3] c.r.jwt.gateway.utils.JwtUtils : 原数据值:1586891865992 该token免登录时间截止至 2020-04-15 03:17:45

3.4. http://localhost:9500/jwt-client/refresh

刷新可以用于在线超时,重新登录

3.5. http://localhost:9500/jwt-client/relogin

用于清除当前服务器中的用户名和token,重新登录进行业务

redis中存在黑名单

1
2
3
4
127.0.0.1:6379> keys *
1) "JWT_TOKEN"
2) "JWT_USERNAME::1249426830067269633"
3) "JWT_BLACKLIST::08b9371df5594eb09da8291cdd773f41"

id对应的token为当前token

1
2
3
127.0.0.1:6379> hget "JWT_TOKEN" 1249426830067269633
"eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVkIjoxNTg2ODkyMTY2ODU5LCJpZCI6IjEyNDk0MjY4MzAwNjcyNjk2MzMiLCJob2xkdGltZSI6MTU4Njg5Mjc2Njg1OSwiZXhwIjoxNTg2ODkyMjg2LCJ1c2VybmFtZSI6ImFkbWluIiwiZ3JvdXAiOiIzMmQ3OGQ0OGI5ZjA0ZWMwOWE3MzcyNTYxNDMxNWY3YiJ9.7GeEeqM53v8FTbtQsdPJ1AQMMILdHg2BRviJ7lKOeaAsd43e9BYaeDH25i7G93WlSAH7aBt8j7cfgNPM6n-GJA"
127.0.0.1:6379>

此时请求 /jwt-login/login

请求 /jwt-login/getInfo 用原来的token

本文结束  感谢您的阅读