登陆拦截器:

  1. 从请求中获取到 JWT 令牌
  2. 利用 JWT 的 SDK 对令牌进行解析,判断是否能够通过校验
  3. 只要令牌解析可以通过,就代表令牌是有效的,用户是登陆过的。

Token 续期:

  • 使用 access_token 和 refresh_token,其中 refresh_token 的过期时间是 access_token 的两倍,当 access_token 过期后使用 refresh_token 重新获取 access_token 和 refresh_token,如果 refresh_token 过期则重新登陆。
  • 使用 Redis + JWT:JWT不设置过期时间,其中保存Redis key,拦截器解析出token中的Redis key,从Redis中获取用户信息,从用户信息中取出过期时间,在过期前重新设置Redis key的过期时间以及更新用户的过期时间和登陆时间属性

拦截哪些方法?:

  • 使用自定注解的方式,需要登陆的接口添加自定义注解即可

初始化模块

trip_common父模块下创建子模块trip-common-security,pom文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>com.swx</groupId>
<artifactId>trip-common</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>trip-common-security</artifactId>

<dependencies>
<dependency>
<groupId>com.swx</groupId>
<artifactId>trip-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
</dependency>
</dependencies>
</project>

创建包名:com.swx.common.security

配置类

创建包名:com.swx.common.redis.configure

Jwt配置类

创建:JwtProperties,需要从配置文件中读取两个配置信息。

JwtProperties
@Getter
@Setter
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
private String secret;
private Integer expireTime;
}

Web配置类

注册自定义的拦截器

WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {

private final LoginInterceptor loginInterceptor;

public WebConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor);
WebMvcConfigurer.super.addInterceptors(registry);
}
}

自动配置类

创建:JwtAutoConfiguration,用于引入其他配置

JwtAutoConfiguration
@Configuration
@Import(WebConfig.class)
@EnableConfigurationProperties(JwtProperties.class)
public class JwtAutoConfiguration {

@Bean
public LoginInterceptor loginInterceptor(TokenService tokenService) {
return new LoginInterceptor(tokenService);
}
}

Redis Key

安全模块下的Redis Key 应该遵循设计规范,因此创建类: LoginRedisKeyPrefix,继承 BaseKeyPrefix

在模块trip-common-security中创建包:com.swx.common.security.key,在该包下创建类:LoginRedisKeyPrefix

LoginRedisKeyPrefix
public class LoginRedisKeyPrefix extends BaseKeyPrefix {
public static final LoginRedisKeyPrefix USER_LOGIN_INFO_STRING = new LoginRedisKeyPrefix("USER:LOGIN:INFO");

public LoginRedisKeyPrefix(String prefix) {
super(prefix);
}

public LoginRedisKeyPrefix(String prefix, Long timeout, TimeUnit unit) {
super(prefix, timeout, unit);
}
}

Token工具服务

主要负责生成 JWT Token,从Token中解析Redis key,并从Redis中获取用户信息,登陆状态的续期

需要定义一个能保存用户信息和登陆时间过期时间的对象类,用于续期

com.swx.common.security.vo下创建: LoginUser

@Getter
@Setter
public class LoginUser {
private Long id;
private String nickname; // 昵称
private Long loginTime; // 登陆时间
private Long expireTime; // 过期时间
private String token;
}
TokenService
/**
* token验证处理
*/
@Component
public class TokenService {
public static final String TOKEN_HEADER = "Token";
private static final String LOGIN_USER_REDIS_UUID = "uuid";
private static final long MINUTES_MILLISECONDS = 60 * 1000L;
private static final long TWENTY_MILLISECONDS = 20 * MINUTES_MILLISECONDS;
private final SecretKey key;

private final JwtProperties jwtProperties;
private final RedisService redisService;

public TokenService(JwtProperties jwtProperties, RedisService redisService) {
this.jwtProperties = jwtProperties;
this.redisService = redisService;
this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecret()));
}

/**
* 创建 Token
* @param loginUser 用户信息
* @return token
*/
public String createToken(LoginUser loginUser) {
// 设置登陆时间和过期时间
long now = System.currentTimeMillis();
loginUser.setLoginTime(now);
long expireTime = now + jwtProperties.getExpireTime() * MINUTES_MILLISECONDS;
loginUser.setExpireTime(expireTime);

// 生成UUID
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
loginUser.setToken(uuid);

// 将用户信息缓存到Redis中, 设置过期时间
String redisKey = UserRedisKeyPrefix.USER_LOGIN_INFO_STRING.fullKey(uuid);
redisService.setCacheObject(redisKey, loginUser, expireTime, TimeUnit.MICROSECONDS);

// 4. 使用JWT生成TOKEN,存入基础信息
return Jwts.builder()
.id(loginUser.getId().toString())
.claim(LOGIN_USER_REDIS_UUID, uuid)
.signWith(key).compact();
}

/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(String token) {
// 判断 Token 是否有效
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecret()));
Claims body = Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();
String uuid = (String) body.get(LOGIN_USER_REDIS_UUID);

// 从Redis中获取用户对象
String redisKey = UserRedisKeyPrefix.USER_LOGIN_INFO_STRING.fullKey(uuid);
return redisService.getCacheObject(redisKey);
}

/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
long loginTime;
if ((loginUser.getExpireTime() - (loginTime = System.currentTimeMillis())) <= TWENTY_MILLISECONDS) {
// 如果用户过期剩余时间小于20分钟,刷新过期时间
loginUser.setLoginTime(loginTime);
long expireTime = loginTime + jwtProperties.getExpireTime() * MINUTES_MILLISECONDS;
loginUser.setExpireTime(expireTime);
// 将刷新后的时间覆盖Redis
String redisKey = UserRedisKeyPrefix.USER_LOGIN_INFO_STRING.fullKey(loginUser.getToken());
redisService.setCacheObject(redisKey, loginUser, expireTime, TimeUnit.MICROSECONDS);
}
}

public static void main(String[] args) {
Key key = Jwts.SIG.HS256.key().build();
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
}
}

获取用户工具

创建包:com.swx.common.security.util,在包下创建类: AuthenticationUtil

public class AuthenticationUtil {
private static final ThreadLocal<LoginUser> USER_HOLDER = new ThreadLocal<>();

public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
throw new BizException("该方法只能在Spring MVC 环境下调用");
}
return requestAttributes.getRequest();
}

public static String getToken() {
return getRequest().getHeader(TokenService.TOKEN_HEADER);
}

/**
* 从Token或者缓存中获取User
* @return 登陆用户
*/
public static LoginUser getLoginUser() {
LoginUser cacheUser = USER_HOLDER.get();
if (cacheUser != null) {
return cacheUser;
}
String token = getToken();
if (StringUtils.isEmpty(token) || token.equals("undefined")) {
return null;
}
TokenService tokenService = SpringContextUtil.getBean(TokenService.class);
LoginUser loginUser = tokenService.getLoginUser(token);
USER_HOLDER.set(loginUser);
return loginUser;
}

/**
* 清空缓存中的User
*/
public static void removeUser() {
USER_HOLDER.remove();
}
}

为了方便在非Spring管理的类中获取Srping Bean,我们需要使用如下工具

SpringContextUtil
/**
* Spring工具类,获取Spring上下文对象等
*
* @author swcode
* @since 2023/10/26 13:31
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext = null;

/**
* 1. SpringContextUtil 被 JVM 加载时, applicationContext 作为静态属性, 就被初始化了, 但是此时是 null 值
* 2. 当 Spring 容器初始化以后, 会管理 SpringContextUtil Bean 对象
* 3. 当 Spring 创建 SpringContextUtil 实例对象时,
* 在初始化阶段会自动调用实现了 ApplicationContextAware 的 setApplicationContext 方法,
* 此时该类中原本静态容器属性就从 null 变成了容器对象
* 4. 当容器启动成功后, 其他业务代码通过该类的静态成员, 就可以直接的访问容器对象, 从容器对象中获取其他 Bean 对象
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextUtil.applicationContext == null) {
SpringContextUtil.applicationContext = applicationContext;
System.out.println("----------" + applicationContext);
}
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}

public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}

public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}

登陆拦截器

创建包:com.swx.common.security.interceptor,在包下创建类: LoginInterceptor

流程如下:

  1. 从request中解析Token信息
  2. 校验并解析Token中的Redis key,从Redis中获取用户信息
  3. 登陆状态续期
LoginInterceptor
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

private final TokenService tokenService;

public LoginInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
// handler => 静态资源
// handler => CORS 的预请求
return true;
}
// 将 handler 对象转换为 HandlerMethod 对象
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 从 HandlerMethod 对象中获取对应的 Controller 对象
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
// 从 Controller 和 HandlerMethod 上获取 @RequireLogin 注解
if (clazz.isAnnotationPresent(RequireLogin.class) || method.isAnnotationPresent(RequireLogin.class)) {
// 1. 从请求头中获取 jwt token
String token = request.getHeader(TokenService.TOKEN_HEADER);
// 2. 基于 jwt sdk 解析 token,解析失败
try {
LoginUser loginUser = tokenService.getLoginUser(token);
if (loginUser == null) {
throw new BizException(401, "Token 已失效");
}
// 未失效,刷新Token
tokenService.refreshToken(loginUser);
} catch (BizException e) {
throw e;
} catch (Exception e) {
log.warn("[登陆拦截] jwt token 解析失败");
throw new BizException(401, "用户未认证");
}
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 请求执行完成之后,准备响应之前
// 线程即将完成本次请求,将当前线程存储的数据清楚
AuthenticationUtil.removeUser();
}
}

别忘了注册登陆拦截器

自动装配

在其他模块依赖本模块时,自动导入配置类

在resource下创建目录: META-INF,在该目录下创建文件 spring.factories:,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.swx.common.security.configure.JwtAutoConfiguration, \
com.swx.common.security.service.TokenService