登陆拦截器:
- 从请求中获取到 JWT 令牌
- 利用 JWT 的 SDK 对令牌进行解析,判断是否能够通过校验
- 只要令牌解析可以通过,就代表令牌是有效的,用户是登陆过的。
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> <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> </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
LoginRedisKeyPrefixpublic 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
@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())); }
public String createToken(LoginUser loginUser) { long now = System.currentTimeMillis(); loginUser.setLoginTime(now); long expireTime = now + jwtProperties.getExpireTime() * MINUTES_MILLISECONDS; loginUser.setExpireTime(expireTime);
String uuid = UUID.randomUUID().toString().replaceAll("-", ""); loginUser.setToken(uuid);
String redisKey = UserRedisKeyPrefix.USER_LOGIN_INFO_STRING.fullKey(uuid); redisService.setCacheObject(redisKey, loginUser, expireTime, TimeUnit.MICROSECONDS);
return Jwts.builder() .id(loginUser.getId().toString()) .claim(LOGIN_USER_REDIS_UUID, uuid) .signWith(key).compact(); }
public LoginUser getLoginUser(String 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);
String redisKey = UserRedisKeyPrefix.USER_LOGIN_INFO_STRING.fullKey(uuid); return redisService.getCacheObject(redisKey); }
public void refreshToken(LoginUser loginUser) { long loginTime; if ((loginUser.getExpireTime() - (loginTime = System.currentTimeMillis())) <= TWENTY_MILLISECONDS) { loginUser.setLoginTime(loginTime); long expireTime = loginTime + jwtProperties.getExpireTime() * MINUTES_MILLISECONDS; loginUser.setExpireTime(expireTime); 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); }
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; }
public static void removeUser() { USER_HOLDER.remove(); } }
|
为了方便在非Spring管理的类中获取Srping Bean,我们需要使用如下工具
SpringContextUtil
@Component public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@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
流程如下:
- 从request中解析
Token
信息
- 校验并解析
Token
中的Redis key,从Redis中获取用户信息
- 登陆状态续期
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)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Class<?> clazz = handlerMethod.getBeanType(); Method method = handlerMethod.getMethod(); if (clazz.isAnnotationPresent(RequireLogin.class) || method.isAnnotationPresent(RequireLogin.class)) { String token = request.getHeader(TokenService.TOKEN_HEADER); try { LoginUser loginUser = tokenService.getLoginUser(token); if (loginUser == null) { throw new BizException(401, "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
|