https://gitee.com/helloworld1006/jwt-login
虽说token使得服务器不保存任何会话数据,即服务器变为无状态,并减少交互数据库,但token无法主动失效,用户注销情况下,仍可以携带原有token请求服务器直接通过验证,继续操作,故结合redis,加入一个黑名单,当用户注销时,token就加入黑名单,使用户无法用原有token操作,只能发送重新登录请求
token | 响应状态 |
---|---|
无token | 50000 - 无访问权限 |
有token,token格式不正确 | 50008 - 非法token |
有token,用户持有的token已被加入到黑名单,即用户被注销 | 50010 - 用户已登出 |
有token,用户正在使用的token和服务端保存的用户正在使用的不一致 | 50012 - 登录信息不一致,无法继续操作 |
有token,用户在线时间超时 | 50014 - token失效 |
JWT_USERNAME::id
字符串类型 存入 用户名
用于保证单端登录
有效时间为用户免登录时间
JWT_TOKEN
Hash类型 存入 id-token
用于记录当前用户【正在使用】的token
JWT_BLACKLIST::group
字符串类型 存入 token
group来自token的载荷中UUID生成的group,用于唯一的对应该token
用于注销,重新登录,刷新操作导致用户被注销,token未失效时,使token自动失效
判断token时,当黑名单中拥有该token,则返回用户被注销
验证账号密码后,验证 【get JWT_USERNAME::id】是否有值,有值则不能登录
无值,用户名存入【set JWT_USERNAME::id username EX time】,防止其他端登录
生成token,存入【hset JWT_TOKEN id token】,保存正在使用的token
返回token给客户端
删除用户名【del JWT_USERNAME::id】
删除用户当前使用的token 【hdel JWT_TOKEN id】
将token加入黑名单【set JWT_BLACKLIST::group token EX time】
当用户注销,token过期情况下,token可以进行刷新,重新获得免登录权限
将token加入黑名单 【set JWT_BLACKLIST::group token EX time】
重新生成token
更新用户当前使用的token 【hset JWT_TOKEN id token】
更新用户名的有效时间【expire JWT_USERNAME::id time】
返回token
当用户篡改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
获取token中的用户信息,不交互redis
网关过滤 | 路由 |
---|---|
直接放行 | /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 |
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency>
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;
}
}
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;
}
}
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";
}
<dependency>
<groupId>com.runaccepted.jwtgroupId>
<artifactId>jwt-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
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;
}
}
server:
port: 9500
spring:
application:
name: jwt-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: jwt-route
uri: lb://jwt-client
predicates:
- Path=/jwt-client/**
#过滤路由
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
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);
}
}
主要是 Mono
其中可以解析出路由过来的请求头信息,并按条件过滤请求,还可以自定义返回结果
为了在免登录期间请求资源问题:token是在2分钟后就失效的,在线失效时间是10分钟,在判断token仅为token过期情况下,向路由传token时,就传刷新token有效时间后的token,不刷新免登录时间,从而保证载荷内容的一致性,也不用2分钟就更新传输过来的token值,当该token的登录时间到期才算真正的token过期
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);
log.error("放行路径{},当前路径 {},是否放行 {}",Arrays.asList(uris),path,containUri);
if (containUri){
return chain.filter(exchange);
}
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(50012,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");
log.error("当前路径 {},验证 {}",path,expiredTimeUri);
if (expiredTimeUri){
return chain.filter(exchange);
}
//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;
}
}
<dependency>
<groupId>com.runaccepted.jwtgroupId>
<artifactId>jwt-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
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
和网关中的唯一不同就是刷新时同时刷新在线时间
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);
}
}
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);
String username = jwtUtils.getUserNameFromToken(refreshToken);
if(expired>0){
redisTemplate.opsForValue.set(userkey,username,expired,TimeUnit.MILLISECONDS);
}
Date date = jwtUtils.getHoldTime(refreshToken);
//将新的token交给前端
return R.ok().data("token",refreshToken).data("date",date);
}
}
服务端记录
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
服务端记录
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
服务端记录
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
刷新可以用于在线超时,重新登录
用于清除当前服务器中的用户名和token,重新登录进行业务
redis中存在黑名单
127.0.0.1:6379> keys *
1) "JWT_TOKEN"
2) "JWT_USERNAME::1249426830067269633"
3) "JWT_BLACKLIST::08b9371df5594eb09da8291cdd773f41"
id对应的token为当前token
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