From e3585408c41f973052afa14d4854dda8d66aef39 Mon Sep 17 00:00:00 2001 From: springboot-plus Date: Fri, 4 Oct 2019 15:00:58 +0800 Subject: [PATCH] :cn: 1.3.0.RELEASE shiro+jwt --- .../common/constant/CommonRedisKey.java | 4 +- .../shiro/cache/LoginRedisService.java | 7 ++++ .../cache/impl/LoginRedisServiceImpl.java | 42 +++++++++++++++++-- .../springbootplus/shiro/jwt/JwtFilter.java | 11 ++++- .../shiro/jwt/JwtProperties.java | 10 +++++ src/main/resources/config/application.yml | 8 +++- .../test/RedisTemplateTest.java | 29 ++++++++++--- 7 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/geekidea/springbootplus/common/constant/CommonRedisKey.java b/src/main/java/io/geekidea/springbootplus/common/constant/CommonRedisKey.java index 75457802..248d76ce 100644 --- a/src/main/java/io/geekidea/springbootplus/common/constant/CommonRedisKey.java +++ b/src/main/java/io/geekidea/springbootplus/common/constant/CommonRedisKey.java @@ -41,8 +41,8 @@ public interface CommonRedisKey { String LOGIN_SALT= "login:salt:%s"; /** - * 登陆user hash key + * 登陆用户username token */ - String LOGIN_USER_HASH = "login:user:hash"; + String LOGIN_USER_TOKEN = "login:user:token:%s:%s"; } diff --git a/src/main/java/io/geekidea/springbootplus/shiro/cache/LoginRedisService.java b/src/main/java/io/geekidea/springbootplus/shiro/cache/LoginRedisService.java index b6dc6c8a..cb12fa6e 100644 --- a/src/main/java/io/geekidea/springbootplus/shiro/cache/LoginRedisService.java +++ b/src/main/java/io/geekidea/springbootplus/shiro/cache/LoginRedisService.java @@ -68,4 +68,11 @@ public interface LoginRedisService { * @return */ boolean exists(String token); + + /** + * 删除用户所有登陆缓存 + * @param username + */ + void deleteUserAllCache(String username); + } diff --git a/src/main/java/io/geekidea/springbootplus/shiro/cache/impl/LoginRedisServiceImpl.java b/src/main/java/io/geekidea/springbootplus/shiro/cache/impl/LoginRedisServiceImpl.java index b1f24fdc..c38f5bbb 100644 --- a/src/main/java/io/geekidea/springbootplus/shiro/cache/impl/LoginRedisServiceImpl.java +++ b/src/main/java/io/geekidea/springbootplus/shiro/cache/impl/LoginRedisServiceImpl.java @@ -14,9 +14,10 @@ package io.geekidea.springbootplus.shiro.cache.impl; import io.geekidea.springbootplus.common.constant.CommonRedisKey; -import io.geekidea.springbootplus.shiro.jwt.JwtToken; import io.geekidea.springbootplus.shiro.cache.LoginRedisService; import io.geekidea.springbootplus.shiro.convert.ShiroMapstructConvert; +import io.geekidea.springbootplus.shiro.jwt.JwtProperties; +import io.geekidea.springbootplus.shiro.jwt.JwtToken; import io.geekidea.springbootplus.shiro.vo.ClientInfo; import io.geekidea.springbootplus.shiro.vo.JwtTokenRedisVo; import io.geekidea.springbootplus.shiro.vo.LoginSysUserRedisVo; @@ -24,12 +25,15 @@ import io.geekidea.springbootplus.util.ClientInfoUtil; import io.geekidea.springbootplus.util.HttpServletRequestUtil; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; +import java.util.List; +import java.util.Set; /** * 登陆信息Redis缓存服务类 @@ -41,6 +45,9 @@ @Service public class LoginRedisServiceImpl implements LoginRedisService { + @Autowired + private JwtProperties jwtProperties; + @Autowired private RedisTemplate redisTemplate; @@ -84,13 +91,21 @@ public void cacheLoginInfo(JwtToken jwtToken, LoginSysUserVo loginSysUserVo, boo // Redis过期时间与JwtToken过期时间一致 Duration expireDuration = Duration.ofSeconds(jwtToken.getExpireSecond()); + // 判断是否启用单个用户登陆,如果是,这每个用户只有一个有效token + boolean singleLogin = jwtProperties.isSingleLogin(); + if (singleLogin) { + deleteUserAllCache(username); + } + // 1. tokenMd5:jwtTokenRedisVo - redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_TOKEN, tokenMd5), jwtTokenRedisVo, expireDuration); + String loginTokenRedisKey = String.format(CommonRedisKey.LOGIN_TOKEN, tokenMd5); + redisTemplate.opsForValue().set(loginTokenRedisKey, jwtTokenRedisVo, expireDuration); // 2. username:loginSysUserRedisVo redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_USER, username), loginSysUserRedisVo, expireDuration); // 3. salt hash,方便获取盐值鉴权 redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_SALT, username), salt, expireDuration); - + // 4. login user token + redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, tokenMd5), loginTokenRedisKey, expireDuration); } @Override @@ -134,6 +149,8 @@ public void deleteLoginInfo(String token, String username) { redisTemplate.delete(String.format(CommonRedisKey.LOGIN_USER, username)); // 3. delete salt redisTemplate.delete(String.format(CommonRedisKey.LOGIN_SALT, username)); + // 4. delete user token + redisTemplate.delete(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, tokenMd5)); } @Override @@ -145,4 +162,23 @@ public boolean exists(String token) { Object object = redisTemplate.opsForValue().get(String.format(CommonRedisKey.LOGIN_TOKEN, tokenMd5)); return object != null; } + + @Override + public void deleteUserAllCache(String username) { + Set userTokenMd5Set = redisTemplate.keys(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, "*")); + if (CollectionUtils.isEmpty(userTokenMd5Set)) { + return; + } + + // 1. 删除登陆用户的所有token信息 + List tokenMd5List = redisTemplate.opsForValue().multiGet(userTokenMd5Set); + redisTemplate.delete(tokenMd5List); + // 2. 删除登陆用户的所有user:token信息 + redisTemplate.delete(userTokenMd5Set); + // 3. 删除登陆用户信息 + redisTemplate.delete(String.format(CommonRedisKey.LOGIN_USER, username)); + // 4. 删除登陆用户盐值信息 + redisTemplate.delete(String.format(CommonRedisKey.LOGIN_SALT, username)); + } + } diff --git a/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtFilter.java b/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtFilter.java index 44dbcd74..b9dc1c19 100644 --- a/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtFilter.java +++ b/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtFilter.java @@ -70,8 +70,17 @@ protected AuthenticationToken createToken(ServletRequest servletRequest, Servlet throw new AuthenticationException("token不能为空"); } if (JwtUtil.isExpired(token)) { - throw new AuthenticationException("token已过期,token:" + token); + throw new AuthenticationException("JWT Token已过期,token:" + token); } + + // 如果开启redis二次校验,或者设置为单个用户token登陆,则先在redis中判断token是否存在 + if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) { + boolean redisExpired = loginRedisService.exists(token); + if (!redisExpired) { + throw new AuthenticationException("Redis Token不存在,token:" + token); + } + } + String username = JwtUtil.getUsername(token); String salt = loginRedisService.getSalt(username); return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond()); diff --git a/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtProperties.java b/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtProperties.java index 8d7bccad..713add9f 100644 --- a/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtProperties.java +++ b/src/main/java/io/geekidea/springbootplus/shiro/jwt/JwtProperties.java @@ -69,4 +69,14 @@ public class JwtProperties { */ private Integer refreshTokenCountdown; + /** + * redis校验jwt token是否存在 + */ + private boolean redisCheck; + + /** + * 单用户登陆,一个用户只能又一个有效的token + */ + private boolean singleLogin; + } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index b3af4a24..75b47ccb 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -120,9 +120,13 @@ spring-boot-plus: # 默认过期时间1小时,单位:秒 expire-second: 3600 # 是否刷新token - refresh_token: true + refresh-token: true # 刷新token的时间间隔,默认10分钟,单位:秒 - refresh_token_countdown: 600 + refresh-token-countdown: 600 + # redis校验jwt token是否存在,可选 + redis-check: true + # true: 同一个账号只能是最后一次登陆token有效,false:同一个账号可多次登陆 + single-login: false ############################ JWT end ############################### ############################### spring-boot-plus end ############################### diff --git a/src/test/java/io/geekidea/springbootplus/test/RedisTemplateTest.java b/src/test/java/io/geekidea/springbootplus/test/RedisTemplateTest.java index 4b9a5f10..af47217c 100644 --- a/src/test/java/io/geekidea/springbootplus/test/RedisTemplateTest.java +++ b/src/test/java/io/geekidea/springbootplus/test/RedisTemplateTest.java @@ -1,5 +1,6 @@ package io.geekidea.springbootplus.test; +import io.geekidea.springbootplus.common.constant.CommonRedisKey; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -7,6 +8,9 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.test.context.junit4.SpringRunner; +import java.util.List; +import java.util.Set; + @RunWith(SpringRunner.class) @SpringBootTest public class RedisTemplateTest { @@ -15,10 +19,25 @@ public class RedisTemplateTest { private RedisTemplate redisTemplate; @Test - public void put(){ - redisTemplate.opsForHash().increment("hello","world",1); - System.out.println(redisTemplate.opsForHash().get("hello","world")); - redisTemplate.opsForHash().increment("hello","world",-1); - System.out.println(redisTemplate.opsForHash().get("hello","world")); + public void put() { + + String username = "junit-test-admin"; + redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, "111"), "111"); + redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, "222"), "222"); + redisTemplate.opsForValue().set(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, "333"), "333"); + + Set set = redisTemplate.keys(String.format(CommonRedisKey.LOGIN_USER_TOKEN, username, "*")); + System.out.println("set = " + set); + + List list = redisTemplate.opsForValue().multiGet(set); + System.out.println("list = " + list); + + Long listResult = redisTemplate.delete(list); + Long setResult = redisTemplate.delete(set); + System.out.println("listResult = " + listResult); + System.out.println("setResult = " + setResult); + + Long count = redisTemplate.countExistingKeys(set); + System.out.println("count = " + count); } }