SpringBoot + SpringSecurity + Mybatis-Plus + JWT + Redis 实现分布式系统认证和授权(刷新Token和Token黑名单)
1. 前提
本文在基于SpringBoot整合SpringSecurity实现JWT的前提中添加刷新Token以及添加Token黑名单。在浏览之前,请查看博客:
SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权
2. 添加Redis依赖及配置
- Redis安装
Docker 安装并部署Tomcat、Mysql8、Redis - 修改pom.xml,添加Redis依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.c3stones</groupId>
<artifactId>spring-security-jwt-redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-jwt-redis-demo</name>
<description>Spring Boot + Srping Security + Mybatis-Plus + JWT + Redis Demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<jjwt.version>0.9.0</jjwt.version>
<druid.version>1.1.6</druid.version>
<jwt.version>1.0.9.RELEASE</jwt.version>
<fastjson.version>1.2.45</fastjson.version>
<mybatis-plus.version>3.3.1</mybatis-plus.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Mybatis-Plus 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- StringUtils 工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- Redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 修改application.yml,添加Redis配置及刷新时间配置
server:
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
redis:
host: 127.0.0.1
port: 6379
password: 123456
# JWT配置
jwt:
# 密匙Key
secret: JWTSecret,C3Stones
# HeaderKey
tokenHeader: Authorization
# Token前缀
tokenPrefix: Bearer
# 过期时间,单位秒
expiration: 300
# 刷新时间,单位天
refreshTime: 7
# 配置白名单(不需要认证)
antMatchers: /login/**,/register/**,/static/**
# Mybatis-plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
global-config:
db-config:
id-type: AUTO
configuration:
# 打印sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 添加Redis工具类
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* Redis工具类
*
* @author CL
*
*/
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate redisTemplate;
private static RedisUtils redisUtils;
/**
* 初始化
*/
@PostConstruct
public void init() {
redisUtils = this;
redisUtils.redisTemplate = this.redisTemplate;
}
/**
* 查询key,支持模糊查询
*
* @param key
*/
public static Set<String> keys(String key) {
return redisUtils.redisTemplate.keys(key);
}
/**
* 获取值
*
* @param key
*/
public static Object get(String key) {
return redisUtils.redisTemplate.opsForValue().get(key);
}
/**
* 设置值
*
* @param key
* @param value
*/
public static void set(String key, String value) {
redisUtils.redisTemplate.opsForValue().set(key, value);
}
/**
* 设置值,并设置过期时间
*
* @param key
* @param value
* @param expire 过期时间,单位秒
*/
public static void set(String key, String value, Integer expire) {
redisUtils.redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
/**
* 删出key
*
* @param key
*/
public static void delete(String key) {
redisUtils.redisTemplate.opsForValue().getOperations().delete(key);
}
/**
* 设置对象
*
* @param key key
* @param hashKey hashKey
* @param object 对象
*/
public static void hset(String key, String hashKey, Object object) {
redisUtils.redisTemplate.opsForHash().put(key, hashKey, object);
}
/**
* 设置对象
*
* @param key key
* @param hashKey hashKey
* @param object 对象
* @param expire 过期时间,单位秒
*/
public static void hset(String key, String hashKey, Object object, Integer expire) {
redisUtils.redisTemplate.opsForHash().put(key, hashKey, object);
redisUtils.redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
/**
* 设置HashMap
*
* @param key key
* @param map map值
*/
public static void hset(String key, HashMap<String, Object> map) {
redisUtils.redisTemplate.opsForHash().putAll(key, map);
}
/**
* key不存在时设置值
*
* @param key
* @param hashKey
* @param object
*/
public static void hsetAbsent(String key, String hashKey, Object object) {
redisUtils.redisTemplate.opsForHash().putIfAbsent(key, hashKey, object);
}
/**
* 获取Hash值
*
* @param key
* @param hashKey
* @return
*/
public static Object hget(String key, String hashKey) {
return redisUtils.redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 获取key的所有值
*
* @param key
* @return
*/
public static Object hget(String key) {
return redisUtils.redisTemplate.opsForHash().entries(key);
}
/**
* 删除key的所有值
*
* @param key
*/
public static void deleteKey(String key) {
redisUtils.redisTemplate.opsForHash().getOperations().delete(key);
}
/**
* 判断key下是否有值
*
* @param key
*/
public static Boolean hasKey(String key) {
return redisUtils.redisTemplate.opsForHash().getOperations().hasKey(key);
}
/**
* 判断key和hasKey下是否有值
*
* @param key
* @param hasKey
*/
public static Boolean hasKey(String key, String hasKey) {
return redisUtils.redisTemplate.opsForHash().hasKey(key, hasKey);
}
}
- 添加获取请求IP地址工具类
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 获取请求IP地址工具类
*
* @author CL
*
*/
@Component
public class AccessAddressUtils {
/**
* 获取用户真实IP地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
3. 核心类修改
- 修改JWT配置类,并添加刷新时间属性
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* JWT配置基础类
*
* @author CL
*
*/
@Component
@ConfigurationProperties(prefix = "jwt")
@SuppressWarnings("static-access")
public class JWTConfig {
/**
* 密匙Key
*/
public static String secret;
/**
* HeaderKey
*/
public static String tokenHeader;
/**
* Token前缀
*/
public static String tokenPrefix;
/**
* 过期时间
*/
public static Integer expiration;
/**
* 有效时间
*/
public static Integer refreshTime;
/**
* 配置白名单
*/
public static String antMatchers;
/**
* 将过期时间单位换算成毫秒
*
* @param expiration 过期时间,单位秒
*/
public void setExpiration(Integer expiration) {
this.expiration = expiration * 1000;
}
/**
* 将有效时间单位换算成毫秒
*
* @param validTime 有效时间,单位秒
*/
public void setRefreshTime(Integer refreshTime) {
this.refreshTime = refreshTime * 24 * 60 * 60 * 1000;
}
public void setSecret(String secret) {
this.secret = secret;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public void setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix + " ";
}
public void setAntMatchers(String antMatchers) {
this.antMatchers = antMatchers;
}
}
- 修改JWTToken工具类
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.c3stones.security.config.JWTConfig;
import com.c3stones.security.entity.SysUserDetails;
import com.c3stones.security.service.SysUserDetailsService;
import com.c3stones.utils.RedisUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
/**
* JWT生产Token工具类
*
* @author CL
*
*/
@Slf4j
@Component
public class JWTTokenUtils {
/**
* 时间格式化
*/
private static final DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private SysUserDetailsService sysUserDetailsService;
private static JWTTokenUtils jwtTokenUtils;
@PostConstruct
public void init() {
jwtTokenUtils = this;
jwtTokenUtils.sysUserDetailsService = this.sysUserDetailsService;
}
/**
* 创建Token
*
* @param sysUserDetails 用户信息
* @return
*/
public static String createAccessToken(SysUserDetails sysUserDetails) {
String token = Jwts.builder()// 设置JWT
.setId(sysUserDetails.getId().toString()) // 用户Id
.setSubject(sysUserDetails.getUsername()) // 主题
.setIssuedAt(new Date()) // 签发时间
.setIssuer("C3Stones") // 签发者
.setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 过期时间
.signWith(SignatureAlgorithm.HS512, JWTConfig.secret) // 签名算法、密钥
.claim("authorities", JSON.toJSONString(sysUserDetails.getAuthorities()))// 自定义其他属性,如用户组织机构ID,用户所拥有的角色,用户权限信息等
.claim("ip", sysUserDetails.getIp()).compact();
return JWTConfig.tokenPrefix + token;
}
/**
* 刷新Token
*
* @param oldToken 过期但未超过刷新时间的Token
* @return
*/
public static String refreshAccessToken(String oldToken) {
String username = JWTTokenUtils.getUserNameByToken(oldToken);
SysUserDetails sysUserDetails = (SysUserDetails) jwtTokenUtils.sysUserDetailsService
.loadUserByUsername(username);
sysUserDetails.setIp(JWTTokenUtils.getIpByToken(oldToken));
return createAccessToken(sysUserDetails);
}
/**
* 解析Token
*
* @param token Token信息
* @return
*/
public static SysUserDetails parseAccessToken(String token) {
SysUserDetails sysUserDetails = null;
if (StringUtils.isNotEmpty(token)) {
try {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
// 解析Token
Claims claims = Jwts.parser().setSigningKey(JWTConfig.secret).parseClaimsJws(token).getBody();
// 获取用户信息
sysUserDetails = new SysUserDetails();
sysUserDetails.setId(Long.parseLong(claims.getId()));
sysUserDetails.setUsername(claims.getSubject());
// 获取角色
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
String authority = claims.get("authorities").toString();
if (StringUtils.isNotEmpty(authority)) {
List<Map<String, String>> authorityList = JSON.parseObject(authority,
new TypeReference<List<Map<String, String>>>() {
});
for (Map<String, String> role : authorityList) {
if (!role.isEmpty()) {
authorities.add(new SimpleGrantedAuthority(role.get("authority")));
}
}
}
sysUserDetails.setAuthorities(authorities);
// 获取IP
String ip = claims.get("ip").toString();
sysUserDetails.setIp(ip);
} catch (Exception e) {
log.error("解析Token异常:" + e);
}
}
return sysUserDetails;
}
/**
* 保存Token信息到Redis中
*
* @param token Token信息
* @param username 用户名
* @param ip IP
*/
public static void setTokenInfo(String token, String username, String ip) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
Integer refreshTime = JWTConfig.refreshTime;
LocalDateTime localDateTime = LocalDateTime.now();
RedisUtils.hset(token, "username", username, refreshTime);
RedisUtils.hset(token, "ip", ip, refreshTime);
RedisUtils.hset(token, "refreshTime",
df.format(localDateTime.plus(JWTConfig.refreshTime, ChronoUnit.MILLIS)), refreshTime);
RedisUtils.hset(token, "expiration", df.format(localDateTime.plus(JWTConfig.expiration, ChronoUnit.MILLIS)),
refreshTime);
}
}
/**
* 将Token放到黑名单中
*
* @param token Token信息
*/
public static void addBlackList(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
RedisUtils.hset("blackList", token, df.format(LocalDateTime.now()));
}
}
/**
* Redis中删除Token
*
* @param token Token信息
*/
public static void deleteRedisToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
RedisUtils.deleteKey(token);
}
}
/**
* 判断当前Token是否在黑名单中
*
* @param token Token信息
*/
public static boolean isBlackList(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hasKey("blackList", token);
}
return false;
}
/**
* 是否过期
*
* @param expiration 过期时间,字符串
* @return 过期返回True,未过期返回false
*/
public static boolean isExpiration(String expiration) {
LocalDateTime expirationTime = LocalDateTime.parse(expiration, df);
LocalDateTime localDateTime = LocalDateTime.now();
if (localDateTime.compareTo(expirationTime) > 0) {
return true;
}
return false;
}
/**
* 是否有效
*
* @param refreshTime 刷新时间,字符串
* @return 有效返回True,无效返回false
*/
public static boolean isValid(String refreshTime) {
LocalDateTime validTime = LocalDateTime.parse(refreshTime, df);
LocalDateTime localDateTime = LocalDateTime.now();
if (localDateTime.compareTo(validTime) > 0) {
return false;
}
return true;
}
/**
* 检查Redis中是否存在Token
*
* @param token Token信息
* @return
*/
public static boolean hasToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hasKey(token);
}
return false;
}
/**
* 从Redis中获取过期时间
*
* @param token Token信息
* @return 过期时间,字符串
*/
public static String getExpirationByToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hget(token, "expiration").toString();
}
return null;
}
/**
* 从Redis中获取刷新时间
*
* @param token Token信息
* @return 刷新时间,字符串
*/
public static String getRefreshTimeByToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hget(token, "refreshTime").toString();
}
return null;
}
/**
* 从Redis中获取用户名
*
* @param token Token信息
* @return
*/
public static String getUserNameByToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hget(token, "username").toString();
}
return null;
}
/**
* 从Redis中获取IP
*
* @param token Token信息
* @return
*/
public static String getIpByToken(String token) {
if (StringUtils.isNotEmpty(token)) {
// 去除JWT前缀
token = token.substring(JWTConfig.tokenPrefix.length());
return RedisUtils.hget(token, "ip").toString();
}
return null;
}
}
- 修改登录成功处理类,将Token添加到Redis中
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.c3stones.security.entity.SysUserDetails;
import com.c3stones.security.utils.JWTTokenUtils;
import com.c3stones.utils.AccessAddressUtils;
import com.c3stones.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 登录成功处理类
*
* @author CL
*
*/
@Slf4j
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
SysUserDetails sysUserDetails = (SysUserDetails) authentication.getPrincipal();
// 获得请求IP
String ip = AccessAddressUtils.getIpAddress(request);
sysUserDetails.setIp(ip);
String token = JWTTokenUtils.createAccessToken(sysUserDetails);
// 保存Token信息到Redis中
JWTTokenUtils.setTokenInfo(token, sysUserDetails.getUsername(), ip);
log.info("用户{}登录成功,Token信息已保存到Redis", sysUserDetails.getUsername());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
ResponseUtils.responseJson(response, ResponseUtils.response(200, "登录成功", tokenMap));
}
}
- 修改登出成功处理类,登出后将Token添加至黑名单
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import com.c3stones.security.config.JWTConfig;
import com.c3stones.security.utils.JWTTokenUtils;
import com.c3stones.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 登出成功处理类
*
* @author CL
*
*/
@Slf4j
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
// 添加到黑名单
String token = request.getHeader(JWTConfig.tokenHeader);
JWTTokenUtils.addBlackList(token);
log.info("用户{}登出成功,Token信息已保存到Redis的黑名单中", JWTTokenUtils.getUserNameByToken(token));
SecurityContextHolder.clearContext();
ResponseUtils.responseJson(response, ResponseUtils.response(200, "登出成功", null));
}
}
- 修改JWT过滤器,对Token进行校验
加入黑名单后将拦截;
Token过期但在刷新期间内将刷新Token;
超过过期时间且超过刷新时间,将拦截;
请求IP与Token中IP不一致,将拦截。
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import com.c3stones.security.config.JWTConfig;
import com.c3stones.security.entity.SysUserDetails;
import com.c3stones.security.utils.JWTTokenUtils;
import com.c3stones.utils.AccessAddressUtils;
import com.c3stones.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
/**
* JWT权限过滤器,用于验证Token是否合法
*
* @author CL
*
*/
@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
// 取出Token
String token = request.getHeader(JWTConfig.tokenHeader);
if (token != null && token.startsWith(JWTConfig.tokenPrefix)) {
// 是否在黑名单中
if (JWTTokenUtils.isBlackList(token)) {
ResponseUtils.responseJson(response, ResponseUtils.response(505, "Token已失效", "Token已进入黑名单"));
return;
}
// 是否存在于Redis中
if (JWTTokenUtils.hasToken(token)) {
String ip = AccessAddressUtils.getIpAddress(request);
String expiration = JWTTokenUtils.getExpirationByToken(token);
String username = JWTTokenUtils.getUserNameByToken(token);
// 判断是否过期
if (JWTTokenUtils.isExpiration(expiration)) {
// 加入黑名单
JWTTokenUtils.addBlackList(token);
// 是否在刷新期内
String validTime = JWTTokenUtils.getRefreshTimeByToken(token);
if (JWTTokenUtils.isValid(validTime)) {
// 刷新Token,重新存入请求头
String newToke = JWTTokenUtils.refreshAccessToken(token);
// 删除旧的Token,并保存新的Token
JWTTokenUtils.deleteRedisToken(token);
JWTTokenUtils.setTokenInfo(newToke, username, ip);
response.setHeader(JWTConfig.tokenHeader, newToke);
log.info("用户{}的Token已过期,但为超过刷新时间,已刷新", username);
token = newToke;
} else {
log.info("用户{}的Token已过期且超过刷新时间,不予刷新", username);
// 加入黑名单
JWTTokenUtils.addBlackList(token);
ResponseUtils.responseJson(response, ResponseUtils.response(505, "Token已过期", "已超过刷新有效期"));
return;
}
}
SysUserDetails sysUserDetails = JWTTokenUtils.parseAccessToken(token);
if (sysUserDetails != null) {
// 校验IP
if (!StringUtils.equals(ip, sysUserDetails.getIp())) {
log.info("用户{}请求IP与Token中IP信息不一致", username);
// 加入黑名单
JWTTokenUtils.addBlackList(token);
ResponseUtils.responseJson(response, ResponseUtils.response(505, "Token已过期", "可能存在IP伪造风险"));
return;
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
sysUserDetails, sysUserDetails.getId(), sysUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
4. 测试
- 用户登录

- 查看Redis:
配置文件中配置Token过期时间为300秒即5分钟,刷新时间为7天之内有效。

- 超过有效时间但为超过刷新时间测试
注意看响应头中Authorization值的变化。

- 使用之前的Token再次请求

- 在其他IP主机上访问接口,使用本机申请的Token
# 请求:
curl -X POST -H "Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoidXNlciIsImlhdCI6MTU5NDc3NjYzMiwiaXNzIjoiQzNTdG9uZXMiLCJleHAiOjE1OTQ3NzY5MzIsImF1dGhvcml0aWVzIjoiW3tcImF1dGhvcml0eVwiOlwiUk9MRV9VU0VSXCJ9XSIsImlwIjoiMTI3LjAuMC4xIn0.ogo8Q3-MOj5bFLA01bxM1Fh0plaB8tvwwSnDlgHQqxVMm0zOiKvsQhsqO673xeFPMpSXhFhPu57coaHX1J5E0A" "http://192.168.0.100:8080/user/info"
# 结果:
{"code":505,"data":"可能存在IP伪造风险","msg":"Token已过期"}
- 登出,使用响应头中的新的Token

注意:每次测试后,请查看Redis中的变化。
5. 项目地址
spring-security-jwt-redis-demo
SpringBoot + SpringSecurity + Mybatis-Plus + JWT + Redis 实现分布式系统认证和授权(刷新Token和Token黑名单)的更多相关文章
- springboot+springsecurity+mybatis plus注解实现对方法的权限处理
文章目录 接上文 [springboot+springsecurity+mybatis plus之用户授权](https://blog.csdn.net/Kevinnsm/article/detail ...
- 基于SpringSecurity和JWT的用户访问认证和授权
发布时间:2018-12-03 技术:springsecurity+jwt+java+jpa+mysql+mysql workBench 概述 基于SpringSecurity和JWT的用户访 ...
- Shiro+springboot+mybatis(md5+salt+散列)认证与授权-02
代码延续地址:Shiro+springboot+mybatis(md5+salt+散列)认证与授权-01 1.创建t_role角色表(比如管理员admin,普通用户user等),创建t_pers权限表 ...
- .NetCore WebApi——基于JWT的简单身份认证与授权(Swagger)
上接:.NetCore WebApi——Swagger简单配置 任何项目都有权限这一关键部分.比如我们有许多接口.有的接口允许任何人访问,另有一些接口需要认证身份之后才可以访问:以保证重要数据不会泄露 ...
- SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权
1. 简介 Spring Security是一个功能强大且易于扩展的安全框架,主要用于为Java程序提供用户认证(Authentication)和用户授权(Authorization)功能. ...
- SpringBoot+Security+MyBatis+ES+MQ+Redis+Docker+Vue的电商系统
今天鹏哥给大家推荐的项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现. 前台商城系统包含首页门户.商品推荐.商品搜索.商品展示.购物车.订单流程.会员中 ...
- SpringBoot之Mybatis操作中使用Redis做缓存
上一博客学习了SpringBoot集成Redis,今天这篇博客学习下Mybatis操作中使用Redis做缓存.这里其实主要学习几个注解:@CachePut.@Cacheable.@CacheEvict ...
- Shiro+springboot+mybatis(md5+salt+散列)认证与授权-01
这个小项目包含了注册与登录,使用了springboot+mybatis+shiro的技术栈:当用户在浏览器登录时发起请求时,首先这一系列的请求会被拦截器进行拦截(ShiroFilter),然后拦截器根 ...
- spring-boot集成mybatis,用redis做缓存
网上有很多例子了,执行源码起码有3个,都是各种各样的小问题. 现在做了个小demo,实现spring-boot 用redis做缓存的实例,简单记录下思路,分享下源码. 缓存的实现,分担了数据库的压力, ...
随机推荐
- 用JavaScript实现全选-反选
实现全选-反选 在日常生活我们会遇到需要全选-反选的地方,其实用JavaScript也能实现. 样式如下所示: 样式代码如下所示: <!DOCTYPE html PUBLIC "-// ...
- Linux(CentOS6.8)配置ActiveMQ
1.下载ActiveMQ http://activemq.apache.org/ 注:若是想下载老版本的ActiveMQ可以通过以下链接下载 http://activemq.apache.org/do ...
- 解决docker镜像无vim
docker拉取的镜像一般都是ubantu系统 安装vim apt-get update apt-get vim
- Logstash使用mongodb插件报错: ArgumentError: wrong number of arguments (given 2, expected 1)
目录 背景 安装插件过程 背景 今天在使用logstash收集日志存储到mongodb的安装过程遇到了个错误,记录下来,错误就是下面这样: 配置文件很简单,由于是测试环境,命令行传入日志输入由ruby ...
- 写代码有这16个好习惯,可以减少80%非业务的bug
前言 每一个好习惯都是一笔财富,本文整理了写代码的16个好习惯,每个都很经典,养成这些习惯,可以规避多数非业务的bug!希望对大家有帮助哈,谢谢阅读,加油哦~ github地址,感谢每颗star ❝ ...
- testlink——解决测试度量与报告或图表中中文显示乱码问题
解决问题之前的图表: 解决方法: (1)下载SimHei.TTF字体(可以在自己电脑的C:/windows/fonts目录下找到,若找不到,可以在网上下载) (2)将SimHei.TTF文件拷贝到te ...
- HDU 4920 Matrix multiplication 题解(内存访问连续性/卡常)
题目链接 题目大意 多组输入,给你两个n×n的矩阵,要你求他们相乘%3的值 题目思路 这个题目主要是要了解内存访问连续化,要尽量每次访问连续的内存 所以第一种方法会超时,第二种则AC.一种卡常技巧 代 ...
- 如何测试一个APP
1.是否支持各种手机系统 2.是否会因为分辨率而出错 3.不同机型能否安装 4.老旧机型 能否通用 5.广告时长 6.测试能否登陆注册 7.卸载时是否会发生意外 8.安装时会不会误认为带病毒 9.用户 ...
- 在运行tsc编译.ts文件时,“因为在此系统上禁止运行脚本” 怎么解决?
tsc : 无法加载文件 C:\Users\Administrator\AppData\Roaming\npm\tsc.ps1,因为在此系统上禁止运行脚本.有关详细信息,请参阅 https:/go.m ...
- npm常用操作
Npm常用操作 1. 淘宝镜像 1.1 npm临时使用淘宝镜像安装依赖包 npm i -g express --registry https://registry.npm.taobao.org 1.2 ...