使用 jwt + Caffeine 实现认证流程

134 阅读3分钟

分享一个自己想出来的一个jwt + Caffeine 的用户身份认证方式。

  1. token 和 session

简单了解下 token 和 session 的认证区别, token 在认证结束之后无需记录认证的用户状态信息,所以被称为无状态认证方式,基于session 的方式的话后端就需要记录本次认证的用户状态 ,所以被称为有状态认证。token 携带了用户的一些基本的信息,在前端访问后端服务的时候,都需要携带token进行数据访问。

  1. Caffeine

一个本地的缓存框架,可以持久化。

  1. 获取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;
        }
    }
}
  1. 对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是否失效,如果失效就从本地文件中删除。

  1. 使用token

好了我们的toekn 以及通过 Caffeine 对token的管理已经完成了,那么我们该如何在用户认证中使用到呢, 我们可以自定义Filter ,通过filter进行拦截想要认证的api路径,比如只有/auth 开头的api需要认证token,在Filter中判定请求是否携带了token,并且token是否在缓存中存在。

后知后觉:通过这种方式我们可以实现多种含义的token,例如我们可以对不同的api路径实现匿名token处理、用户密码token,以及如果我们需要将部分api对外使用作为单独的售卖服务而不是将系统暴露,也可以手动生成 token 发送给客户。