分享一个自己想出来的一个jwt + Caffeine 的用户身份认证方式。
- token 和 session
简单了解下 token 和 session 的认证区别, token 在认证结束之后无需记录认证的用户状态信息,所以被称为无状态认证方式,基于session 的方式的话后端就需要记录本次认证的用户状态 ,所以被称为有状态认证。token 携带了用户的一些基本的信息,在前端访问后端服务的时候,都需要携带token进行数据访问。
- Caffeine
一个本地的缓存框架,可以持久化。
- 获取token
需要通过用户名和密码获取一个token ,我们使用Java生成对应的token ,我们创建token都创建为永久有效的token,将token的管理交给Caffeine缓存框架去处理
public interface IToken {
String TOKEN_HEADER = "Authorization-Token";
String claimName();
}
@Data
public class UsernamePasswordToken implements IToken{
private String userName;
private Long userId;
}
public class JwtTokenGenerator {
// 默认密钥
public final static String DEFAULT_SECRET = "DEFAULT_SECRET";
// token有效期,单位分钟;
// 创建token
public static String createToken(IToken tokenData) {
try {
// 创建签名的算法实例
Algorithm algorithm = Algorithm.HMAC256(DEFAULT_SECRET);
String token = JWT.create()
.withClaim(tokenData.claimName(),JSONObject.toJSONString(tokenData))
.sign(algorithm);
return token;
} catch (JWTCreationException e) {
log.error("创建token失败:",e);
}
return null;
}
public static IToken analyzeToken( String token,Class<? extends IToken > clazz) throws Exception{
try {
// 因此获取载荷信息不需要密钥
DecodedJWT jwt = JWT.decode(token);
String dataStr = jwt.getClaim((clazz.newInstance()).claimName()).asString();
return JSONObject.parseObject(dataStr,clazz);
} catch (JWTDecodeException jwtDecodeException) {
return null;
}
}
}
- 对token进行维护管理
@Component
@Slf4j
public class TokenCache {
@Value("${cache.dir.login}")
private String tmpDir;
private static Long EXPIRE_TIME = 1 * 1000 * 60 * 60L; // 默认是一个小时有效
Cache<String, String> cache = Caffeine.newBuilder()
//最后一次访问之后一小时后过期
.expireAfterAccess(EXPIRE_TIME, TimeUnit.MILLISECONDS)
.maximumSize(10_000)
.removalListener((key, val, removalCause) -> {
Path path = Paths.get(tmpDir + key.toString());
try {
if (Files.exists(path)) {
Files.delete(path);
}
} catch (IOException e) {
log.error("删除持久化文件失败,:{}",path,e);
}
})
.writer(new CacheWriter<Object, Object>() {
@Override
public void write(@NonNull Object o, @NonNull Object o2) {
Path path = Paths.get(tmpDir + o.toString());
try {
if (Files.exists(path)) {
Files.delete(path);
}
Files.createFile(path);
Files.write(path,o2.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE);
} catch (IOException e) {
log.error("删除持久化文件失败,:{}",path,e);
}
}
@Override
public void delete(@NonNull Object o, @Nullable Object o2, @NonNull RemovalCause removalCause) {
Path path = Paths.get(tmpDir + o.toString());
try {
if (Files.exists(path)) {
Files.delete(path);
}
} catch (IOException e) {
log.error("删除持久化文件失败,:{}",path,e);
}
}
})
//自动加载
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String s) throws Exception {
Path dirPath = Paths.get(tmpDir);
Iterator<Path> iterator = Files.list(dirPath).iterator();
while (iterator.hasNext()){
Path filePath = iterator.next();
if(filePath.getFileName().toString().equals(s)){
return Files.readAllLines(filePath).get(0);
}
return null;
}
return null;
}
});
//支持刷新
public Boolean add(String key,String token){
if( cache.asMap().containsKey(key) ){
cache.invalidate(key);
}
//创建time 实现对应的数据
Long time = new Date().getTime();
String content = token+"\n"+time; // 使用分行进行数据处理
cache.put(key,content);
return true;
}
public Boolean remove(String key){
cache.invalidate(key);
return true;
}
public String get(String key){
String content = cache.getIfPresent(key);
if(StringUtils.hasText(content)){
return content.split("\n")[1];
}else {
return null;
}
}
@PostConstruct
public void init() throws Exception{
Stream<Path> paths = Files.list(Paths.get(tmpDir));
paths.forEach(path -> {
try {
String timeLine = Files.readAllLines(path).get(1);
Long expaire_time = Long.valueOf(timeLine);
if ( EXPIRE_TIME + expaire_time < new Date().getTime() ) {
log.info("已经过期");
try {
if (Files.exists(path)) {
Files.delete(path);
}
} catch (IOException e) {
log.error("删除持久化文件失败,:{}",path,e);
}
} else {
log.info("还没有过期:{}",path.getFileName());
}
} catch (Exception e) {
log.error("获取token失败",e);
}
});
}
}
管理token的逻辑就是,每一次生成token之后存放在缓存中,并且持久化到本地,如果用户注销,就让token失效,并且从本地文件中删除,每一个token的过期时间是一个小时,缓存的数据包含了token的失效时间,在应用启动的时候检查每一个token是否失效,如果失效就从本地文件中删除。
- 使用token
好了我们的toekn 以及通过 Caffeine 对token的管理已经完成了,那么我们该如何在用户认证中使用到呢, 我们可以自定义Filter ,通过filter进行拦截想要认证的api路径,比如只有/auth 开头的api需要认证token,在Filter中判定请求是否携带了token,并且token是否在缓存中存在。
后知后觉:通过这种方式我们可以实现多种含义的token,例如我们可以对不同的api路径实现匿名token处理、用户密码token,以及如果我们需要将部分api对外使用作为单独的售卖服务而不是将系统暴露,也可以手动生成 token 发送给客户。