效果展示:
token时间续签
思路:
1:在登录接口中 如果校验账号密码成功 则根据用户id和用户类型创建jwt token(有效期设置为-1,即永不过期),得到A
2:更新登录日期(当前时间new Date()即可)(业务上可选),得到B
3:在redis中缓存key为ACCESS_TOKEN:userId:A(加上A是为了防止用户多个客户端登录 造成token覆盖),value为B的毫秒数(转换成字符串类型),过期时间为7天(7 * 24 * 60 * 60)
4:在登录结果中返回json格式为{“result”:“success”,“token”: A}
5:用户在接口请求header中携带token进行登录,后端在所有接口前置拦截器进行拦截,作用是解析token 拿到userId和用户类型(用户调用业务接口只需要传token即可), 如果解析失败(抛出SignatureException),则返回json(code = 0 ,info= Token验证不通过, errorCode = ‘1001’); 此外如果解析成功,验证redis中key为ACCESS_TOKEN:userId:A 是否存在 如果不存在 则返回json(code = 0 ,info= 会话过期请重新登录, errorCode = ‘1002’); 如果缓存key存在,则自动续7天超时时间(value不变),实现频繁登录用户免登陆。
6:把userId和用户类型放入request参数中 接口方法中可以直接拿到登录用户信息
7:如果是修改密码或退出登录 则废除access_tokens(删除key)
后台实现:
1:导入redis jar包
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties配置文件:
#用户
spring.datasource.username=root
#密码
spring.datasource.password=123123
#连接是数据库
spring.datasource.url=jdbc:mysql://localhost:3306/jin1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#打印Sql
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#扫描.xml文件
mybatis-plus.mapper-locations=classpath*:xml/*.xml
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端?
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=30
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
#链接超时时间
spring.redis.timeout=PT10S
登录控制器中:
package com.ff.controller;
import com.ff.common.ResultCode;
import com.ff.common.ResultObj;
import com.ff.entity.User;
import com.ff.service.UserService;
import com.ff.util.JWTUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/login")
@CrossOrigin
public class LoginController {
@Autowired
private UserService userService;
private static final String ACCESS_TOKEN="ACCESS_TOKEN";
@Autowired
private RedisTemplate redisTemplate;
//登录
@PostMapping("login")
public ResultObj login(String username,String password) {
//判断用户名密码为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
return ResultObj.error(ResultCode.USERNAME_PASSWORD_ISNULL);
}
//判断用户名是否存在
User user = userService.queryUserByUsername(username);
if (user == null) {
return ResultObj.error(ResultCode.USER_NOEXIST);
}
//判断密码是否正确
if (!password.equals(user.getPassword())) {
return ResultObj.error(ResultCode.PASSWORD_ERROR);
}
//登录成功,生成token
String token = JWTUtil.createToken(user);
//获取当前时间的毫秒值
String currentTime = String.valueOf(System.currentTimeMillis());
//拼接存到redis中的Key中
String accessKey=ACCESS_TOKEN+user.getId()+":"+token;
//往拼接存到redis中的Key存value值
redisTemplate.opsForValue().set(accessKey,currentTime);
//设置redis key的过期时间 2分钟 可自定义
redisTemplate.expire(accessKey,2, TimeUnit.MINUTES);
return ResultObj.success(token);
}
}
登录切面类代码:
package com.ff.common;
import com.ff.annotation.LoginAnnotation;
import com.ff.util.JWTUtil;
import io.jsonwebtoken.Claims;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
public class LoginAop {
@Resource
private HttpServletRequest request;
private static final String ACCESS_TOKEN="ACCESS_TOKEN";
@Autowired
private RedisTemplate redisTemplate;
//扫描controller层所有自定义注解
@Around(value = "execution(* com.fh.controller.*.*(..)) && @annotation(loginAnnotation)")
public Object loginAround(ProceedingJoinPoint joinPoint, LoginAnnotation loginAnnotation) {
Object proceed=null;
//验证token的值
String token = request.getHeader("Authorization-token");
ResultObj resultObj = JWTUtil.verifToken(token);
//失败的情况
if(resultObj.getCode()!= 200){
return resultObj;
}
//获取data中数据
Claims claims = (Claims)resultObj.getData();
//拼接存到redis中的Key中
String accessKey=ACCESS_TOKEN+claims.get("id")+":"+token;
//验证redis中的token得值是否存在
if(!redisTemplate.hasKey(accessKey)){
//登录失败
return ResultObj.error(ResultCode.TOKEN_ERROR);
}
//如果redis中的token得值存在,续签
//获取当前时间的毫秒值
String currentTime = String.valueOf(System.currentTimeMillis());
//往拼接存到redis中的Key存value值
redisTemplate.opsForValue().set(accessKey,currentTime);
//设置redis key的过期时间 2分钟
redisTemplate.expire(accessKey,2, TimeUnit.MINUTES);
try {
//执行目标方法
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
JWTUtil工具类:设置token时间
package com.ff.util;
import com.ff.common.ResultCode;
import com.ff.common.ResultObj;
import com.ff.entity.User;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtil {
//生成Toke的方法
public static String createToken(User user) {
//token分为3部分
//第一部分我们称它为头部(header),
// 第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
// 第三部分是签证(signature).
//头部
Map<String, Object> headerMap = new HashMap<String, Object>();
//HS256加密方式
headerMap.put("alg", "HS256");
headerMap.put("type", "JWT");
//有效载荷
Map<String, Object> payloadMap = new HashMap<String, Object>();
payloadMap.put("username", user.getUsername());
payloadMap.put("id", user.getId());
//失效时间
long timeMillis = System.currentTimeMillis();
//设置token时间 999999999 毫秒=11.5740740625 天
//6000000为1分钟
long endTime = timeMillis + 6000000;
//签证,签名
String token = null;
try {
token = Jwts.builder()
.setHeader(headerMap)
.setClaims(payloadMap)
.setExpiration(new Date(endTime))
.signWith(SignatureAlgorithm.HS256, "rtet")
.compact();
System.out.println(token);
} catch (ExpiredJwtException e) {
e.getClaims();
}
return token;
}
//解密token
public static ResultObj verifToken(String token) {
try {
//通过TWTs的parser 方法 接受
//根据人名key解密
Claims claims = Jwts.parser().setSigningKey("rtet")
.parseClaimsJws(token)
.getBody();
return ResultObj.success(claims);
} catch (Exception e) {
return ResultObj.error(ResultCode.TOKEN_ERROR);
}
}
}
最后使用俩个类序列化Redis参数,不然乱码
package com.ff.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return (T) JSON.parseObject(str, clazz);
}
}
package com.ff.cache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
com.ff.cache.FastJsonRedisSerializer fastJsonRedisSerializer = new com.ff.cache.FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}