• SpringBoot + SpringSecurity + Mybatis-Plus + JWT + Redis 实现分布式系统认证和授权(刷新Token和Token黑名单)


    1. 前提

      本文在基于SpringBoot整合SpringSecurity实现JWT的前提中添加刷新Token以及添加Token黑名单。在浏览之前,请查看博客:
      SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权

    2. 添加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

  • 相关阅读:
    状态模式
    maven-war-plugin 插件 web.xml 缺失时忽略
    Java远程方法协议(JRMP)
    Java Singleton的3种实现方式
    浅谈分布式消息技术 Kafka
    浅谈分布式事务
    J2EE开发时的包命名规则,养成良好的开发习惯
    使用Dom4j创建xml文档
    Java HttpClient Basic Credential 认证
    Spring MVC的Post请求参数中文乱码的原因&处理
  • 原文地址:https://www.cnblogs.com/cao-lei/p/13300955.html
Copyright © 2020-2023  润新知