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

代码下载

https://gitee.com/helloworld1006/jwt-login

整体实现

虽说token使得服务器不保存任何会话数据,即服务器变为无状态,并减少交互数据库,但token无法主动失效,用户注销情况下,仍可以携带原有token请求服务器直接通过验证,继续操作,故结合redis,加入一个黑名单,当用户注销时,token就加入黑名单,使用户无法用原有token操作,只能发送重新登录请求

响应设计

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

redis设计

  • JWT_USERNAME::id

    字符串类型 存入 用户名

    用于保证单端登录

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

  • JWT_TOKEN

    Hash类型 存入 id-token

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

  • JWT_BLACKLIST::group

    字符串类型 存入 token

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

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

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

登录操作中redis操作

登录-login

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

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

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

返回token给客户端

登出-logout

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

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

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

刷新-refresh

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

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

重新生成token

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

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

返回token

重新登录-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

获取用户信息-getInfo

获取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

项目

jwt-api 公共接口类

Maven

  <!-- 简化实体类 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>

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

Admin
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;
    }
}
R
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;
    }

}
JwtClient
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";
}

jwt-Gateway web项目

Maven依赖

<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>

跨域设置-定义CorsWebFilter的Bean

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;
    }
}

nacos服务注册 -application.yml

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/**

jwt配置-application.properties

#过滤路由
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

🌟🌟JwtUtils工具类

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);
	}
}

🌟AuthFilter implements GlobalFilter, Ordered

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

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

为了在免登录期间请求资源问题: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;
    }
}

jwt-Client -web项目

Maven依赖

<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>

jwt配置

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

🌟🌟JwtUtils工具类

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

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);
	}
}

🌟ClientController

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);
    }
}

测试 Postman

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JKWz0eKH-1586929207104)(/login.png)]

服务端记录

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGSKCqxv-1586929207114)(/getInfo.png)]

服务端记录

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GOv2BQDw-1586929207329)(/logout.png)]

服务端记录

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Ph8N1TS-1586929207338)(/refresh.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZCXNBpBz-1586929207341)(/relogin.png)]

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

在这里插入图片描述

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值