当前位置: 代码网 > it编程>编程语言>Java > SpringBoot实现JWT令牌失效的6种方案

SpringBoot实现JWT令牌失效的6种方案

2025年06月16日 Java 我要评论
一、jwt基础与失效挑战1.1 jwt的基本结构jwt由三部分组成,以点(.)分隔:header(头部) :包含令牌类型和使用的签名算法payload(负载) :包含声明(claims),如用户信息和

一、jwt基础与失效挑战

1.1 jwt的基本结构

jwt由三部分组成,以点(.)分隔:

  • header(头部) :包含令牌类型和使用的签名算法
  • payload(负载) :包含声明(claims),如用户信息和权限
  • signature(签名) :用于验证令牌的完整性和真实性

一个典型的jwt看起来像这样:

eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioiixmjm0nty3odkwiiwibmftzsi6ikpvag4grg9liiwiawf0ijoxnte2mjm5mdiyfq.sflkxwrjsmekkf2qt4fwpmejf36pok6yjv_adqssw5c

1.2 jwt的特点与失效挑战

jwt的主要特点是无状态性,服务器不需要存储会话信息。这带来了以下挑战:

  • jwt一旦签发,在其有效期内始终有效
  • 无法直接撤销或使令牌失效
  • 服务器默认无法跟踪已发行的令牌

这些特性使得实现jwt的提前失效变得困难,特别是在以下场景:

  • 用户登出系统
  • 用户权限变更
  • 账户被盗,需要使所有令牌失效
  • 密码更改后使旧令牌失效

二、短期令牌+刷新令牌方案

2.1 基本原理

该方案使用两种令牌:

  • 短期访问令牌(access token) :有效期短(如15分钟),用于api访问
  • 长期刷新令牌(refresh token) :有效期长(如7天),用于获取新的访问令牌

当用户需要登出时,只需使刷新令牌失效,短期访问令牌会自然过期。

2.2 springboot实现

首先,添加必要的依赖:

<dependency>
    <groupid>io.jsonwebtoken</groupid>
    <artifactid>jjwt-api</artifactid>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupid>io.jsonwebtoken</groupid>
    <artifactid>jjwt-impl</artifactid>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupid>io.jsonwebtoken</groupid>
    <artifactid>jjwt-jackson</artifactid>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

创建jwt工具类:

@component
public class jwttokenprovider {
    
    @value("${jwt.secret}")
    private string jwtsecret;
    
    @value("${jwt.accesstokenexpiration}")
    private long accesstokenexpiration;
    
    @value("${jwt.refreshtokenexpiration}")
    private long refreshtokenexpiration;
    
    public string generateaccesstoken(userdetails userdetails) {
        return generatetoken(userdetails, accesstokenexpiration);
    }
    
    public string generaterefreshtoken(userdetails userdetails) {
        return generatetoken(userdetails, refreshtokenexpiration);
    }
    
    private string generatetoken(userdetails userdetails, long expiration) {
        date now = new date();
        date expirydate = new date(now.gettime() + expiration);
        
        return jwts.builder()
                .setsubject(userdetails.getusername())
                .setissuedat(now)
                .setexpiration(expirydate)
                .signwith(keys.hmacshakeyfor(jwtsecret.getbytes()), signaturealgorithm.hs512)
                .compact();
    }
    
    public string getusernamefromtoken(string token) {
        claims claims = jwts.parserbuilder()
                .setsigningkey(keys.hmacshakeyfor(jwtsecret.getbytes()))
                .build()
                .parseclaimsjws(token)
                .getbody();
        
        return claims.getsubject();
    }
    
    public boolean validatetoken(string token) {
        try {
            jwts.parserbuilder()
                .setsigningkey(keys.hmacshakeyfor(jwtsecret.getbytes()))
                .build()
                .parseclaimsjws(token);
            return true;
        } catch (exception e) {
            return false;
        }
    }
}

实现刷新令牌服务:

@service
@requiredargsconstructor
public class refreshtokenservice {
    
    private final refreshtokenrepository refreshtokenrepository;
    private final jwttokenprovider jwttokenprovider;
    
    @transactional
    public refreshtoken createrefreshtoken(string username) {
        refreshtoken refreshtoken = new refreshtoken();
        refreshtoken.setusername(username);
        refreshtoken.settoken(uuid.randomuuid().tostring());
        refreshtoken.setexpirydate(instant.now().plusmillis(
                jwttokenprovider.getrefreshtokenexpiration()));
        
        return refreshtokenrepository.save(refreshtoken);
    }
    
    @transactional
    public void deletebyusername(string username) {
        refreshtokenrepository.deletebyusername(username);
    }
    
    public optional<refreshtoken> findbytoken(string token) {
        return refreshtokenrepository.findbytoken(token);
    }
    
    public refreshtoken verifyexpiration(refreshtoken token) {
        if (token.getexpirydate().compareto(instant.now()) < 0) {
            refreshtokenrepository.delete(token);
            throw new tokenrefreshexception(token.gettoken(), 
                "refresh token was expired. please make a new signin request");
        }
        
        return token;
    }
}

实现认证控制器:

@restcontroller
@requestmapping("/api/auth")
@requiredargsconstructor
public class authcontroller {
    
    private final authenticationmanager authenticationmanager;
    private final userdetailsservice userdetailsservice;
    private final jwttokenprovider jwttokenprovider;
    private final refreshtokenservice refreshtokenservice;
    
    @postmapping("/login")
    public responseentity<?> authenticateuser(@valid @requestbody loginrequest loginrequest) {
        authentication authentication = authenticationmanager.authenticate(
            new usernamepasswordauthenticationtoken(
                loginrequest.getusername(), 
                loginrequest.getpassword()
            )
        );
        
        securitycontextholder.getcontext().setauthentication(authentication);
        userdetails userdetails = (userdetails) authentication.getprincipal();
        
        string accesstoken = jwttokenprovider.generateaccesstoken(userdetails);
        refreshtoken refreshtoken = refreshtokenservice.createrefreshtoken(userdetails.getusername());
        
        return responseentity.ok(new jwtresponse(accesstoken, refreshtoken.gettoken()));
    }
    
    @postmapping("/refresh")
    public responseentity<?> refreshtoken(@valid @requestbody tokenrefreshrequest request) {
        string requestrefreshtoken = request.getrefreshtoken();
        
        return refreshtokenservice.findbytoken(requestrefreshtoken)
            .map(refreshtokenservice::verifyexpiration)
            .map(refreshtoken::getusername)
            .map(username -> {
                userdetails userdetails = userdetailsservice.loaduserbyusername(username);
                string accesstoken = jwttokenprovider.generateaccesstoken(userdetails);
                return responseentity.ok(new tokenrefreshresponse(accesstoken, requestrefreshtoken));
            })
            .orelsethrow(() -> new tokenrefreshexception(requestrefreshtoken,
                "refresh token is not in database!"));
    }
    
    @postmapping("/logout")
    public responseentity<?> logoutuser(@valid @requestbody logoutrequest logoutrequest) {
        refreshtokenservice.deletebyusername(logoutrequest.getusername());
        return responseentity.ok(new messageresponse("log out successful!"));
    }
}

application.properties配置:

jwt.secret=yourverylongandsecuresecretkeyherepleasemakeitatleast256bits
jwt.accesstokenexpiration=900000    # 15分钟
jwt.refreshtokenexpiration=604800000 # 7天

2.3 优缺点分析

优点:

  • 无需维护黑名单,降低服务器负担
  • 访问令牌有效期短,安全性较高
  • 用户体验良好,透明刷新令牌
  • 实现简单,容易理解

缺点:

  • 无法即时使访问令牌失效,最多等待其自然过期
  • 需要额外存储刷新令牌,增加了状态性
  • 增加了客户端复杂度,需要处理令牌刷新逻辑
  • 如果刷新令牌泄露,可能导致长期安全风险

2.4 适用场景

  • 一般的web应用和移动应用
  • 对令牌即时失效要求不严格的场景
  • 希望减轻服务器负担的系统
  • 用户会话时间较长的应用

三、redis黑名单机制

3.1 基本原理

黑名单机制将已注销或失效的令牌存储在redis等高性能缓存中,每次验证令牌时都会检查它是否在黑名单中。

这种方法允许即时使令牌失效,同时保持良好的性能。

3.2 springboot实现

首先,添加redis依赖:

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>

创建redis配置类:

@configuration
public class redisconfig {
    
    @bean
    public redistemplate<string, string> redistemplate(redisconnectionfactory factory) {
        redistemplate<string, string> template = new redistemplate<>();
        template.setconnectionfactory(factory);
        template.setkeyserializer(new stringredisserializer());
        template.setvalueserializer(new stringredisserializer());
        return template;
    }
}

实现jwt黑名单服务:

@service
@requiredargsconstructor
public class jwtblacklistservice {
    
    private final redistemplate<string, string> redistemplate;
    private final jwttokenprovider jwttokenprovider;
    
    private static final string blacklist_prefix = "jwt:blacklist:";
    
    public void blacklisttoken(string token) {
        try {
            // 获取令牌过期时间
            claims claims = jwttokenprovider.getclaimsfromtoken(token);
            date expiration = claims.getexpiration();
            long ttl = (expiration.gettime() - system.currenttimemillis()) / 1000;
            
            // 仅当令牌未过期时添加到黑名单
            if (ttl > 0) {
                string key = blacklist_prefix + token;
                redistemplate.opsforvalue().set(key, "blacklisted", ttl, timeunit.seconds);
            }
        } catch (exception e) {
            // 令牌已无效,无需加入黑名单
        }
    }
    
    public boolean isblacklisted(string token) {
        string key = blacklist_prefix + token;
        return boolean.true.equals(redistemplate.haskey(key));
    }
}

更新jwt工具类:

@component
public class jwttokenprovider {
    
    // ... 之前的代码 ...
    
    public claims getclaimsfromtoken(string token) {
        return jwts.parserbuilder()
                .setsigningkey(keys.hmacshakeyfor(jwtsecret.getbytes()))
                .build()
                .parseclaimsjws(token)
                .getbody();
    }
}

添加jwt过滤器,检查黑名单:

@component
@requiredargsconstructor
public class jwttokenfilter extends onceperrequestfilter {
    
    private final jwttokenprovider jwttokenprovider;
    private final jwtblacklistservice blacklistservice;
    private final userdetailsservice userdetailsservice;
    
    @override
    protected void dofilterinternal(httpservletrequest request, httpservletresponse response, 
                                   filterchain filterchain) throws servletexception, ioexception {
        try {
            string jwt = getjwtfromrequest(request);
            
            if (stringutils.hastext(jwt) && jwttokenprovider.validatetoken(jwt)) {
                // 检查令牌是否在黑名单中
                if (blacklistservice.isblacklisted(jwt)) {
                    response.setstatus(httpservletresponse.sc_unauthorized);
                    response.getwriter().write("token has been revoked");
                    return;
                }
                
                string username = jwttokenprovider.getusernamefromtoken(jwt);
                userdetails userdetails = userdetailsservice.loaduserbyusername(username);
                
                usernamepasswordauthenticationtoken authentication = 
                    new usernamepasswordauthenticationtoken(userdetails, null, userdetails.getauthorities());
                authentication.setdetails(new webauthenticationdetailssource().builddetails(request));
                
                securitycontextholder.getcontext().setauthentication(authentication);
            }
        } catch (exception ex) {
            logger.error("could not set user authentication in security context", ex);
        }
        
        filterchain.dofilter(request, response);
    }
    
    private string getjwtfromrequest(httpservletrequest request) {
        string bearertoken = request.getheader("authorization");
        if (stringutils.hastext(bearertoken) && bearertoken.startswith("bearer ")) {
            return bearertoken.substring(7);
        }
        return null;
    }
}

实现登出端点:

@restcontroller
@requestmapping("/api/auth")
@requiredargsconstructor
public class authcontroller {
    
    // ... 之前的代码 ...
    
    private final jwtblacklistservice blacklistservice;
    
    @postmapping("/logout")
    public responseentity<?> logoutuser(httpservletrequest request) {
        string jwt = getjwtfromrequest(request);
        if (stringutils.hastext(jwt)) {
            blacklistservice.blacklisttoken(jwt);
        }
        return responseentity.ok(new messageresponse("log out successful!"));
    }
    
    private string getjwtfromrequest(httpservletrequest request) {
        string bearertoken = request.getheader("authorization");
        if (stringutils.hastext(bearertoken) && bearertoken.startswith("bearer ")) {
            return bearertoken.substring(7);
        }
        return null;
    }
}

3.3 优缺点分析

优点:

  • 可以即时使令牌失效
  • 不影响令牌原有的有效期管理
  • 无需修改客户端逻辑
  • redis高性能,对系统影响小

缺点:

  • 引入了状态存储,部分牺牲jwt的无状态特性
  • redis需要存储所有已注销但未过期的令牌,增加存储开销
  • 每次api请求都需要检查黑名单,增加了延迟

3.4 适用场景

  • 对安全性要求较高的应用
  • 需要即时令牌失效功能的系统

四、令牌版本/计数器机制

4.1 基本原理

该方案为每个用户维护一个令牌版本号或计数器。当用户登出或需要使令牌失效时,增加用户的令牌版本号。

令牌中包含发行时的版本号,验证时比较令牌中的版本号与用户当前的版本号,如果不匹配则拒绝访问。

4.2 springboot实现

首先,创建用户令牌版本实体:

@entity
@table(name = "user_token_versions")
@data
public class usertokenversion {
    
    @id
    private string username;
    
    private int tokenversion;
    
    public void incrementversion() {
        this.tokenversion++;
    }
}

创建令牌版本仓库:

@repository
public interface usertokenversionrepository extends jparepository<usertokenversion, string> {
}

实现令牌版本服务:

@service
@requiredargsconstructor
public class tokenversionservice {
    
    private final usertokenversionrepository repository;
    
    @transactional
    public int getcurrentversion(string username) {
        return repository.findbyid(username)
            .orelseget(() -> {
                usertokenversion newversion = new usertokenversion();
                newversion.setusername(username);
                newversion.settokenversion(0);
                return repository.save(newversion);
            })
            .gettokenversion();
    }
    
    @transactional
    public void incrementversion(string username) {
        usertokenversion version = repository.findbyid(username)
            .orelseget(() -> {
                usertokenversion newversion = new usertokenversion();
                newversion.setusername(username);
                newversion.settokenversion(0);
                return newversion;
            });
        
        version.incrementversion();
        repository.save(version);
    }
}

修改jwt工具类,在令牌中包含版本信息:

@component
@requiredargsconstructor
public class jwttokenprovider {
    
    @value("${jwt.secret}")
    private string jwtsecret;
    
    @value("${jwt.expiration}")
    private long jwtexpiration;
    
    private final tokenversionservice tokenversionservice;
    
    public string generatetoken(userdetails userdetails) {
        date now = new date();
        date expirydate = new date(now.gettime() + jwtexpiration);
        
        // 获取当前令牌版本
        int tokenversion = tokenversionservice.getcurrentversion(userdetails.getusername());
        
        return jwts.builder()
                .setsubject(userdetails.getusername())
                .claim("tokenversion", tokenversion)  // 添加版本信息
                .setissuedat(now)
                .setexpiration(expirydate)
                .signwith(keys.hmacshakeyfor(jwtsecret.getbytes()), signaturealgorithm.hs512)
                .compact();
    }
    
    public boolean validatetoken(string token, userdetails userdetails) {
        try {
            claims claims = getclaimsfromtoken(token);
            
            // 验证用户名
            boolean usernamematches = claims.getsubject().equals(userdetails.getusername());
            
            // 验证令牌未过期
            boolean isnotexpired = claims.getexpiration().after(new date());
            
            // 验证令牌版本
            int tokenversion = claims.get("tokenversion", integer.class);
            int currentversion = tokenversionservice.getcurrentversion(userdetails.getusername());
            boolean versionmatches = tokenversion == currentversion;
            
            return usernamematches && isnotexpired && versionmatches;
        } catch (exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

更新jwt过滤器:

@component
@requiredargsconstructor
public class jwttokenfilter extends onceperrequestfilter {
    
    private final jwttokenprovider jwttokenprovider;
    private final userdetailsservice userdetailsservice;
    
    @override
    protected void dofilterinternal(httpservletrequest request, httpservletresponse response, 
                                   filterchain filterchain) throws servletexception, ioexception {
        try {
            string jwt = getjwtfromrequest(request);
            
            if (stringutils.hastext(jwt)) {
                string username = jwttokenprovider.getusernamefromtoken(jwt);
                userdetails userdetails = userdetailsservice.loaduserbyusername(username);
                
                // 使用版本验证令牌
                if (jwttokenprovider.validatetoken(jwt, userdetails)) {
                    usernamepasswordauthenticationtoken authentication = 
                        new usernamepasswordauthenticationtoken(userdetails, null, userdetails.getauthorities());
                    authentication.setdetails(new webauthenticationdetailssource().builddetails(request));
                    
                    securitycontextholder.getcontext().setauthentication(authentication);
                }
            }
        } catch (exception ex) {
            logger.error("could not set user authentication in security context", ex);
        }
        
        filterchain.dofilter(request, response);
    }
    
    // ... getjwtfromrequest方法 ...
}

实现登出端点:

@restcontroller
@requestmapping("/api/auth")
@requiredargsconstructor
public class authcontroller {
    
    // ... 其他代码 ...
    
    private final tokenversionservice tokenversionservice;
    
    @postmapping("/logout")
    public responseentity<?> logoutuser(authentication authentication) {
        string username = authentication.getname();
        
        // 增加令牌版本号,使所有现有令牌失效
        tokenversionservice.incrementversion(username);
        
        return responseentity.ok(new messageresponse("log out successful!"));
    }
}

4.3 优缺点分析

优点:

  • 存储开销小,只需记录用户的当前版本号
  • 无需维护黑名单,降低了内存需求
  • 可以选择性地使部分令牌失效

缺点:

  • 需要存储用户令牌版本
  • 每次验证令牌都需要查询数据库或缓存
  • 可能影响系统性能,特别是在用户量大的情况下

4.4 适用场景

  • 需要用户主动登出功能的系统
  • 用户量适中的系统
  • 需要在特定操作后使令牌失效的场景

五、密钥轮换策略

5.1 基本原理

密钥轮换策略通过定期更换用于签名jwt的密钥来实现令牌失效。

当系统需要使所有令牌失效时,立即轮换密钥,所有使用旧密钥签名的令牌将无法通过验证。

为了支持平滑过渡,系统通常保留多个最近的密钥版本。

5.2 springboot实现

创建密钥管理服务:

@service
@slf4j
public class keyrotationservice {
    
    private final map<string, key> keystore = new concurrenthashmap<>();
    private string currentkeyid;
    
    @postconstruct
    public void init() {
        // 初始化第一个密钥
        rotatekey();
    }
    
    @scheduled(cron = "${jwt.key-rotation-cron:0 0 0 * * ?}") // 默认每天零点
    public void scheduledrotation() {
        log.info("performing scheduled key rotation");
        rotatekey();
    }
    
    public synchronized void rotatekey() {
        string keyid = uuid.randomuuid().tostring();
        key key = generatekey();
        keystore.put(keyid, key);
        
        // 只保留最近3个密钥
        if (keystore.size() > 3) {
            list<string> keyids = new arraylist<>(keystore.keyset());
            keyids.sort(null); // 自然排序
            
            for (int i = 0; i < keyids.size() - 3; i++) {
                keystore.remove(keyids.get(i));
            }
        }
        
        currentkeyid = keyid;
        log.info("key rotated, new key id: {}", keyid);
    }
    
    public string getcurrentkeyid() {
        return currentkeyid;
    }
    
    public key getkey(string keyid) {
        return keystore.get(keyid);
    }
    
    public key getcurrentkey() {
        return keystore.get(currentkeyid);
    }
    
    private key generatekey() {
        return keys.secretkeyfor(signaturealgorithm.hs512);
    }
    
    public void forcerotation() {
        log.info("forcing key rotation to invalidate all tokens");
        rotatekey();
    }
}

更新jwt工具类以支持密钥轮换:

@component
@requiredargsconstructor
public class jwttokenprovider {
    
    @value("${jwt.expiration}")
    private long jwtexpiration;
    
    private final keyrotationservice keyrotationservice;
    
    public string generatetoken(userdetails userdetails) {
        date now = new date();
        date expirydate = new date(now.gettime() + jwtexpiration);
        
        string keyid = keyrotationservice.getcurrentkeyid();
        key key = keyrotationservice.getcurrentkey();
        
        return jwts.builder()
                .setsubject(userdetails.getusername())
                .setissuedat(now)
                .setexpiration(expirydate)
                .setheaderparam("kid", keyid) // 设置密钥id
                .signwith(key, signaturealgorithm.hs512)
                .compact();
    }
    
    public claims getclaimsfromtoken(string token) {
        // 从令牌头部提取密钥id
        string kid = extractkeyid(token);
        if (kid == null) {
            throw new jwtexception("invalid jwt: missing key id");
        }
        
        // 获取对应的密钥
        key key = keyrotationservice.getkey(kid);
        if (key == null) {
            throw new jwtexception("invalid jwt: unknown key id");
        }
        
        return jwts.parserbuilder()
                .setsigningkey(key)
                .build()
                .parseclaimsjws(token)
                .getbody();
    }
    
    private string extractkeyid(string token) {
        try {
            string header = token.split("\.")[0];
            string decodedheader = new string(base64.getdecoder().decode(header));
            jsonnode headernode = new objectmapper().readtree(decodedheader);
            return headernode.get("kid").astext();
        } catch (exception e) {
            return null;
        }
    }
    
    public boolean validatetoken(string token) {
        try {
            getclaimsfromtoken(token);
            return true;
        } catch (exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

创建管理员控制器,提供强制失效所有令牌的功能:

@restcontroller
@requestmapping("/api/admin")
@requiredargsconstructor
@preauthorize("hasrole('admin')")
public class admincontroller {
    
    private final keyrotationservice keyrotationservice;
    
    @postmapping("/invalidate-all-tokens")
    public responseentity<?> invalidatealltokens() {
        keyrotationservice.forcerotation();
        return responseentity.ok(new messageresponse("all tokens have been invalidated"));
    }
}

5.3 优缺点分析

优点:

  • 可以立即使所有令牌失效
  • 可以实现平滑过渡,支持旧密钥一段时间
  • 符合安全最佳实践,定期轮换密钥

缺点:

  • 无法选择性使单个用户的令牌失效
  • 可能导致所有用户被迫重新登录
  • 需要妥善管理密钥

5.4 适用场景

  • 安全要求高,需要定期轮换密钥的系统
  • 发生安全事件时,需要紧急使所有令牌失效
  • 偏好无状态设计的应用
  • 系统重大升级或维护时

六、集中式令牌存储

6.1 基本原理

这种方法将jwt作为访问标识符,但在服务器端维护一个集中式的令牌存储,存储介质可以使用数据库或者缓存。

每次验证时,不仅检查jwt的签名和有效期,还查询存储库确认令牌是否仍然有效。

这种方式结合了jwt的便利性和会话管理的灵活性。

6.2 springboot实现

创建令牌实体:

@entity
@table(name = "active_tokens")
@data
public class activetoken {
    
    @id
    private string tokenid;
    
    private string username;
    
    private date expirydate;
    
    private boolean revoked;
    
    @creationtimestamp
    private date createdat;
    
    public boolean isexpired() {
        return expirydate.before(new date());
    }
}

创建令牌仓库:

@repository
public interface activetokenrepository extends jparepository<activetoken, string> {
    
    list<activetoken> findbyusername(string username);
    
    @modifying
    @query("update activetoken t set t.revoked = true where t.username = :username")
    void revokeallusertokens(@param("username") string username);
    
    @modifying
    @query("delete from activetoken t where t.expirydate < :now")
    void deleteexpiredtokens(@param("now") date now);
}

实现令牌服务:

@service
@requiredargsconstructor
public class tokenstorageservice {
    
    private final activetokenrepository tokenrepository;
    
    @transactional
    public void savetoken(string tokenid, string username, date expirydate) {
        activetoken token = new activetoken();
        token.settokenid(tokenid);
        token.setusername(username);
        token.setexpirydate(expirydate);
        token.setrevoked(false);
        
        tokenrepository.save(token);
    }
    
    @transactional(readonly = true)
    public boolean istokenvalid(string tokenid) {
        return tokenrepository.findbyid(tokenid)
            .map(token -> !token.isrevoked() && !token.isexpired())
            .orelse(false);
    }
    
    @transactional
    public void revoketoken(string tokenid) {
        tokenrepository.findbyid(tokenid).ifpresent(token -> {
            token.setrevoked(true);
            tokenrepository.save(token);
        });
    }
    
    @transactional
    public void revokeallusertokens(string username) {
        tokenrepository.revokeallusertokens(username);
    }
    
    @scheduled(fixedrate = 86400000) // 每天清理一次
    @transactional
    public void cleanexpiredtokens() {
        tokenrepository.deleteexpiredtokens(new date());
    }
}

更新jwt工具类:

@component
@requiredargsconstructor
public class jwttokenprovider {
    
    @value("${jwt.secret}")
    private string jwtsecret;
    
    @value("${jwt.expiration}")
    private long jwtexpiration;
    
    private final tokenstorageservice tokenstorageservice;
    
    public string generatetoken(userdetails userdetails) {
        date now = new date();
        date expirydate = new date(now.gettime() + jwtexpiration);
        
        // 生成唯一的令牌id
        string tokenid = uuid.randomuuid().tostring();
        
        string token = jwts.builder()
                .setsubject(userdetails.getusername())
                .setissuedat(now)
                .setexpiration(expirydate)
                .setid(tokenid)  // 设置jwt id (jti)
                .signwith(keys.hmacshakeyfor(jwtsecret.getbytes()), signaturealgorithm.hs512)
                .compact();
        
        // 将令牌保存到存储中
        tokenstorageservice.savetoken(tokenid, userdetails.getusername(), expirydate);
        
        return token;
    }
    
    public string gettokenid(string token) {
        return getclaimsfromtoken(token).getid();
    }
    
    public boolean validatetoken(string token) {
        try {
            claims claims = getclaimsfromtoken(token);
            
            // 验证jwt基本属性
            boolean isnotexpired = claims.getexpiration().after(new date());
            
            // 验证令牌是否在存储中有效
            string tokenid = claims.getid();
            boolean isvalidinstorage = tokenstorageservice.istokenvalid(tokenid);
            
            return isnotexpired && isvalidinstorage;
        } catch (exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

实现登出功能:

@restcontroller
@requestmapping("/api/auth")
@requiredargsconstructor
public class authcontroller {
    
    // ... 其他代码 ...
    
    private final jwttokenprovider jwttokenprovider;
    private final tokenstorageservice tokenstorageservice;
    
    @postmapping("/logout")
    public responseentity<?> logoutuser(httpservletrequest request) {
        string jwt = getjwtfromrequest(request);
        
        if (stringutils.hastext(jwt)) {
            string tokenid = jwttokenprovider.gettokenid(jwt);
            tokenstorageservice.revoketoken(tokenid);
        }
        
        return responseentity.ok(new messageresponse("log out successful!"));
    }
    
    @postmapping("/logout-all")
    public responseentity<?> logoutalldevices(authentication authentication) {
        string username = authentication.getname();
        tokenstorageservice.revokeallusertokens(username);
        
        return responseentity.ok(new messageresponse("logged out from all devices"));
    }
    
    // ... 其他方法 ...
}

6.3 优缺点分析

优点:

  • 能够即时使单个令牌或所有令牌失效
  • 提供精细的令牌管理,如查看活跃会话
  • 可以实现"记住我"等高级功能
  • 便于审计和监控

缺点:

  • 完全放弃了jwt的无状态优势
  • 每次请求都需要查询存储库
  • 系统复杂度提高

6.4 适用场景

  • 对安全性要求极高的系统
  • 需要精细令牌管理的应用
  • 已有会话管理需求的项目
  • 多设备登录管理
  • 企业级应用,需要详细的审计日志

七、会话状态监控机制

7.1 基本原理

会话状态监控机制在保持jwt无状态特性的同时,通过跟踪用户会话状态来间接控制令牌有效性。

系统维护用户登录状态(如最后活动时间、登录设备等),当状态变更(如密码修改、异常登录)时,可以拒绝特定令牌的访问。

7.2 springboot实现

创建用户会话状态实体:

@entity
@table(name = "user_sessions")
@data
public class usersessionstatus {
    
    @id
    private string username;
    
    private date passwordlastchanged;
    
    private date lastforcedlogout;
    
    private string securitycontext;
    
    @version
    private long version;
    
    public boolean haschangedafter(date tokenissuedat) {
        return (passwordlastchanged != null && passwordlastchanged.after(tokenissuedat)) ||
               (lastforcedlogout != null && lastforcedlogout.after(tokenissuedat));
    }
}

创建会话状态仓库:

@repository
public interface usersessionstatusrepository extends jparepository<usersessionstatus, string> {
}

实现会话状态服务:

@service
@requiredargsconstructor
public class usersessionservice {
    
    private final usersessionstatusrepository repository;
    
    @transactional(readonly = true)
    public usersessionstatus getsessionstatus(string username) {
        return repository.findbyid(username)
            .orelseget(() -> {
                usersessionstatus status = new usersessionstatus();
                status.setusername(username);
                return status;
            });
    }
    
    @transactional
    public void updatepasswordchanged(string username) {
        usersessionstatus status = getsessionstatus(username);
        status.setpasswordlastchanged(new date());
        repository.save(status);
    }
    
    @transactional
    public void forcelogout(string username) {
        usersessionstatus status = getsessionstatus(username);
        status.setlastforcedlogout(new date());
        repository.save(status);
    }
    
    @transactional
    public void updatesecuritycontext(string username, string securitycontext) {
        usersessionstatus status = getsessionstatus(username);
        status.setsecuritycontext(securitycontext);
        repository.save(status);
    }
    
    public boolean istokenvalid(string username, date tokenissuedat, string tokensecuritycontext) {
        usersessionstatus status = getsessionstatus(username);
        
        // 检查令牌是否在密码更改或强制登出之前签发
        if (status.haschangedafter(tokenissuedat)) {
            return false;
        }
        
        // 检查安全上下文是否匹配(可选)
        if (status.getsecuritycontext() != null && tokensecuritycontext != null) {
            return status.getsecuritycontext().equals(tokensecuritycontext);
        }
        
        return true;
    }
}

更新jwt工具类:

@component
@requiredargsconstructor
public class jwttokenprovider {
    
    @value("${jwt.secret}")
    private string jwtsecret;
    
    @value("${jwt.expiration}")
    private long jwtexpiration;
    
    private final usersessionservice sessionservice;
    
    public string generatetoken(userdetails userdetails, string securitycontext) {
        date now = new date();
        date expirydate = new date(now.gettime() + jwtexpiration);
        
        return jwts.builder()
                .setsubject(userdetails.getusername())
                .setissuedat(now)
                .setexpiration(expirydate)
                .claim("securitycontext", securitycontext)
                .signwith(keys.hmacshakeyfor(jwtsecret.getbytes()), signaturealgorithm.hs512)
                .compact();
    }
    
    public boolean validatetoken(string token) {
        try {
            claims claims = getclaimsfromtoken(token);
            
            // 基本验证
            boolean isnotexpired = claims.getexpiration().after(new date());
            if (!isnotexpired) {
                return false;
            }
            
            // 验证会话状态
            string username = claims.getsubject();
            date issuedat = claims.getissuedat();
            string securitycontext = claims.get("securitycontext", string.class);
            
            return sessionservice.istokenvalid(username, issuedat, securitycontext);
        } catch (exception e) {
            return false;
        }
    }
    
    // ... 其他方法 ...
}

实现认证和密码更改接口:

@restcontroller
@requiredargsconstructor
public class authcontroller {
    
    // ... 其他依赖 ...
    
    private final usersessionservice sessionservice;
    private final userservice userservice;
    
    @postmapping("/login")
    public responseentity<?> login(@requestbody loginrequest loginrequest) {
        // ... 认证逻辑 ...
        
        // 生成安全上下文(例如,设备信息、ip地址等)
        string securitycontext = generatesecuritycontext(request);
        
        // 更新用户会话状态
        sessionservice.updatesecuritycontext(userdetails.getusername(), securitycontext);
        
        // 生成令牌,包含安全上下文
        string token = jwttokenprovider.generatetoken(userdetails, securitycontext);
        
        // ... 返回令牌 ...
    }
    
    @postmapping("/change-password")
    public responseentity<?> changepassword(@requestbody passwordchangerequest request, 
                                           authentication authentication) {
        string username = authentication.getname();
        
        // 更改密码
        userservice.changepassword(username, request.getoldpassword(), request.getnewpassword());
        
        // 更新密码更改时间,使旧令牌失效
        sessionservice.updatepasswordchanged(username);
        
        return responseentity.ok(new messageresponse("password changed successfully"));
    }
    
    @postmapping("/logout-all-devices")
    public responseentity<?> logoutalldevices(authentication authentication) {
        string username = authentication.getname();
        
        // 强制所有设备登出
        sessionservice.forcelogout(username);
        
        return responseentity.ok(new messageresponse("logged out from all devices"));
    }
    
    private string generatesecuritycontext(httpservletrequest request) {
        // 生成包含设备信息、ip地址等的安全上下文
        string ipaddress = request.getremoteaddr();
        string useragent = request.getheader("user-agent");
        
        return digestutils.md5digestashex((ipaddress + ":" + useragent).getbytes());
    }
}

7.3 优缺点分析

优点:

  • 保持了jwt的大部分无状态特性
  • 可以基于用户状态变更使令牌失效
  • 可以实现细粒度的会话控制
  • 安全上下文可以防止令牌被盗用

缺点:

  • 每次请求需要检查用户会话状态
  • 状态管理增加了系统复杂性
  • 安全上下文验证可能导致合法用户被拒绝(如ip变化)

7.4 适用场景

  • 需要账户安全功能(如密码更改后使令牌失效)的系统
  • 对可疑活动监控有需求的应用
  • 需要防止令牌盗用的场景
  • 平衡无状态性和安全性的应用

八、六种方案对比与选择指南

方案即时失效存储需求性能影响实现复杂度维护成本适用场景
短期令牌+刷新令牌部分(仅刷新令牌)一般web/移动应用
redis黑名单完全安全性要求高的应用
令牌版本/计数器完全特定操作下需要控制token有效性需求的应用
密钥轮换全局极低需要定期轮换密钥的系统
集中式令牌存储完全企业级应用,多设备管理
会话状态监控条件性平衡安全和性能的系统

九、总结

每种方案都有其优缺点和适用场景,选择合适的方案取决于应用的安全需求、性能要求和架构设计。

在实际应用中,常常需要组合使用多种策略,构建多层次的安全防护。

以上就是springboot实现jwt令牌失效的6种方案的详细内容,更多关于springboot jwt令牌失效的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com