网易首页 > 网易号 > 正文 申请入驻

JWT 实现登录认证 + Token 自动续期方案,这才是正确的使用姿势!

0
分享至














过去这段时间主要负责了项目中的用户管理模块,用户管理模块会涉及到加密及认证流程,加密已经在前面的文章中介绍了,可以阅读用户管理模块:

https://juejin.cn/post/6916150628955717646

今天就来讲讲认证功能的技术选型及实现。技术上没啥难度当然也没啥挑战,但是对一个原先没写过认证功能的菜鸡甜来说也是一种锻炼吧

技术选型

要实现认证功能,很容易就会想到JWT或者session,但是两者有啥区别?各自的优缺点?应该Pick谁?夺命三连

图片 区别

基于session和基于JWT的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而JWT是保存在客户端的

认证流程 基于session的认证流程

  • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个session并保存到数据库

  • 服务器为用户生成一个sessionId,并将具有sesssionId的cookie放置在用户浏览器中,在后续的请求中都将带有这个cookie信息进行访问

  • 服务器获取cookie,通过获取cookie中的sessionId查找数据库判断当前请求是否有效

  • springboot系列:https://www.yoodb.com/spring/springboot/knowledge-hierarchy.html

基于JWT的认证流程
  • 用户在浏览器中输入用户名和密码,服务器通过密码校验后生成一个token并保存到数据库

  • 前端获取到token,存储到cookie或者local storage中,在后续的请求中都将带有这个token信息进行访问

  • 服务器获取token值,通过查找数据库判断当前token是否有效

优缺点

保存在客户端,在分布式环境下不需要做额外工作。而session因为保存在服务端,分布式环境下需要实现多机数据共享 session一般需要结合Cookie实现认证,所以需要浏览器支持cookie,因此移动端无法使用session认证方案

安全性

JWT的payload使用的是base64编码的,因此在JWT中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全

图片

如果在JWT中存储了敏感信息,可以解码出来非常的不安全

性能

经过编码之后JWT将非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。并且用户在系统中的每一次http请求都会把JWT携带在Header里面,HTTP请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的HTTP请求比使用session的开销大得多

一次性

无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT

无法废弃

一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。若想废弃,一种常用的处理手段是结合redis

续签

如果使用JWT做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。

最简单的一种方式是每次请求刷新JWT,即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间

选择JWT或session

我投JWT一票,JWT有很多缺点,但是在分布式环境下不需要像session一样额外实现多机数据共享,虽然seesion的多机数据共享可以通过粘性session、session共享、session复制、持久化session、terracoa实现seesion复制等多种成熟的方案来解决这个问题。但是JWT不需要额外的工作,使用JWT不香吗?且JWT一次性的缺点可以结合redis进行弥补。

扬长补短,因此在实际项目中选择的是使用JWT来进行认证
功能实现

JWT所需依赖


com.auth0
java-jwt
3.10.3

JWT工具类

public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);

//私钥
private static final String TOKEN_SECRET = "123456";

/**
* 生成token,自定义过期时间 毫秒
*
* @param userTokenDTO
* @return
*/
public static String generateToken(UserTokenDTO userTokenDTO) {
try {
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");

return JWT.create()
.withHeader(header)
.withClaim("token", JSONObject.toJSONString(userTokenDTO))
//.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
logger.error("generate token occur error, error is:{}", e);
return null;
}
}

/**
* 检验token是否正确
*
* @param token
* @return
*/
public static UserTokenDTO parseToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
String tokenInfo = jwt.getClaim("token").asString();
return JSON.parseObject(tokenInfo, UserTokenDTO.class);
}
}

说明:

  • 生成的token中不带有过期时间,token的过期时间由redis进行管理

  • UserTokenDTO中不带有敏感信息,如password字段不会出现在token中

Redis工具类public final class RedisServiceImpl implements RedisService {
* 过期时长
private final Long DURATION = 1 * 24 * 60 * 60 * 1000L;

@Resource
private RedisTemplate redisTemplate;

private ValueOperations valueOperations;

@PostConstruct
public void init() {
RedisSerializer redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
valueOperations = redisTemplate.opsForValue();
}

@Override
public void set(String key, String value) {
valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS);
log.info("key={}, value is: {} into redis cache", key, value);
}

@Override
public String get(String key) {
String redisValue = valueOperations.get(key);
log.info("get from redis, value is: {}", redisValue);
return redisValue;
}

@Override
public boolean delete(String key) {
boolean result = redisTemplate.delete(key);
log.info("delete from redis, key is: {}", key);
return result;
}

@Override
public Long getExpireTime(String key) {
return valueOperations.getOperations().getExpire(key);
}
}

RedisTemplate简单封装

业务实现 登陆功能public String login(LoginUserVO loginUserVO) {
//1.判断用户名密码是否正确
UserPO userPO = userMapper.getByUsername(loginUserVO.getUsername());
if (userPO == null) {
throw new UserException(ErrorCodeEnum.TNP1001001);
if (!loginUserVO.getPassword().equals(userPO.getPassword())) {
throw new UserException(ErrorCodeEnum.TNP1001002);

//2.用户名密码正确生成token
UserTokenDTO userTokenDTO = new UserTokenDTO();
PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
userTokenDTO.setId(userPO.getId());
userTokenDTO.setGmtCreate(System.currentTimeMillis());
String token = JWTUtil.generateToken(userTokenDTO);

//3.存入token至redis
redisService.set(userPO.getId(), token);
return token;
}

说明:

  • 判断用户名密码是否正确

  • 用户名密码正确则生成token

  • 将生成的token保存至redis

登出功能public boolean loginOut(String id) {
boolean result = redisService.delete(id);
if (!redisService.delete(id)) {
throw new UserException(ErrorCodeEnum.TNP1001003);

return result;
}

将对应的key删除即可

更新密码功能public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
//1.修改密码
UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword())
.id(updatePasswordUserVO.getId())
.build();
UserPO user = userMapper.getById(updatePasswordUserVO.getId());
if (user == null) {
throw new UserException(ErrorCodeEnum.TNP1001001);

if (userMapper.updatePassword(userPO) != 1) {
throw new UserException(ErrorCodeEnum.TNP1001005);
}
//2.生成新的token
UserTokenDTO userTokenDTO = UserTokenDTO.builder()
.id(updatePasswordUserVO.getId())
.username(user.getUsername())
.gmtCreate(System.currentTimeMillis()).build();
String token = JWTUtil.generateToken(userTokenDTO);
//3.更新token
redisService.set(user.getId(), token);
return token;
}

说明:

更新用户密码时需要重新生成新的token,并将新的token返回给前端,由前端更新保存在local storage中的token,同时更新存储在redis中的token,这样实现可以避免用户重新登陆,用户体验感不至于太差
其他说明

在实际项目中,用户分为普通用户和管理员用户,只有管理员用户拥有删除用户的权限,这一块功能也是涉及token操作的,但是我太懒了,demo工程就不写了

在实际项目中,密码传输是加密过的。公众 号Java精选,回复java面试,获取面试资料,支持在线刷题。

拦截器类public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String authToken = request.getHeader("Authorization");
String token = authToken.substring("Bearer".length() + 1).trim();
UserTokenDTO userTokenDTO = JWTUtil.parseToken(token);
//1.判断请求是否有效
if (redisService.get(userTokenDTO.getId()) == null
|| !redisService.get(userTokenDTO.getId()).equals(token)) {
return false;

//2.判断是否需要续期
if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) {
redisService.set(userTokenDTO.getId(), token);
log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token);
}
return true;
}
说明:

拦截器中主要做两件事,一是对token进行校验,二是判断token是否需要进行续期

token校验:

  • 判断id对应的token是否不存在,不存在则token过期

  • 若token存在则比较token是否一致,保证同一时间只有一个用户操作

token自动续期:

为了不频繁操作redis,只有当离过期时间只有30分钟时才更新过期时间

拦截器配置类@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticateInterceptor())
.excludePathPatterns("/logout/**")
.excludePathPatterns("/login/**")
.addPathPatterns("/**");

@Bean
public AuthenticateInterceptor authenticateInterceptor() {
return new AuthenticateInterceptor();
}
}

作者:何甜甜在吗 https://juejin.cn/post/6932702419344162823

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

------ 特别推荐 ------

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
1-2到3-2!中超神剧情:王秋明补时绝杀,李霄鹏不敢相信

1-2到3-2!中超神剧情:王秋明补时绝杀,李霄鹏不敢相信

足球狗说
2024-09-29 21:30:01
8名公职人员截访中“因公殉职”,五大“痛点”曝光!

8名公职人员截访中“因公殉职”,五大“痛点”曝光!

兵叔评说
2024-09-28 23:46:21
华为被碾压!中国第一民企诞生,狂赚2015亿,估值14480亿.

华为被碾压!中国第一民企诞生,狂赚2015亿,估值14480亿.

中国先进制造技术论坛
2024-09-29 07:26:20
内维尔:B费抬脚有膝盖高了,他在铲球前滑倒了但不会逃脱惩罚

内维尔:B费抬脚有膝盖高了,他在铲球前滑倒了但不会逃脱惩罚

直播吧
2024-09-30 00:35:10
布林肯当面告诉王毅外长,若中国继续“援俄”,美将对华发起制裁

布林肯当面告诉王毅外长,若中国继续“援俄”,美将对华发起制裁

贺文萍
2024-09-28 14:40:28
他曾被作为接班人培养,称得上党的良心人物

他曾被作为接班人培养,称得上党的良心人物

霹雳炮
2024-09-28 23:19:58
张含韵近照震撼曝光!甜美少女变“韵味大妈”?网友热议:40多了

张含韵近照震撼曝光!甜美少女变“韵味大妈”?网友热议:40多了

娱不咸
2024-09-29 21:30:08
一颗71.6元!女子遭遇话梅刺客一斤3580元,商家称明码标价!市监部门回应

一颗71.6元!女子遭遇话梅刺客一斤3580元,商家称明码标价!市监部门回应

上观新闻
2024-09-29 17:36:12
住建部:全力促进房地产市场止跌回稳

住建部:全力促进房地产市场止跌回稳

界面新闻
2024-09-29 22:36:34
要钱还是要命?陕西某停车场一小时200?官方回应:收费合理合规

要钱还是要命?陕西某停车场一小时200?官方回应:收费合理合规

大川哥
2024-09-29 12:56:33
太刺激了,早知道泳池这么炸裂就去学游泳了。

太刺激了,早知道泳池这么炸裂就去学游泳了。

有趣的火烈鸟
2024-09-18 20:36:13
真脏!上海女律师自曝与上司开房性爱,尺度惊人,本人被扒出!

真脏!上海女律师自曝与上司开房性爱,尺度惊人,本人被扒出!

阿伧说事
2024-09-29 14:44:36
小学生被罚深蹲300个,有人住院,校方回应

小学生被罚深蹲300个,有人住院,校方回应

界面新闻
2024-09-29 21:01:14
真主党新头领刚上任就被干掉了,还有人敢继任吗?

真主党新头领刚上任就被干掉了,还有人敢继任吗?

近距离
2024-09-29 14:59:16
米莱在联合国大会的演讲,像砸场来了,看他讲了什么

米莱在联合国大会的演讲,像砸场来了,看他讲了什么

长平投研
2024-09-28 12:54:52
珠海第十一中学教育集团总校长谢晟接受审查调查

珠海第十一中学教育集团总校长谢晟接受审查调查

南方都市报
2024-09-27 12:58:09
宁德时代Z基地电池厂火情已初步得到控制

宁德时代Z基地电池厂火情已初步得到控制

财联社
2024-09-29 18:19:09
楼市春天又来了?多家房企宣布涨价,会否有更多房企跟进

楼市春天又来了?多家房企宣布涨价,会否有更多房企跟进

上游新闻
2024-09-29 12:39:19
上海再出7条楼市新政:调整限购降低首付比例!权威解读来了

上海再出7条楼市新政:调整限购降低首付比例!权威解读来了

上观新闻
2024-09-29 21:52:34
伊朗向文明跨出重要一步:不再强制女性佩戴头巾

伊朗向文明跨出重要一步:不再强制女性佩戴头巾

难得君
2024-09-29 12:42:37
2024-09-30 00:48:49
Java精选
Java精选
一场永远也演不完的戏
1550文章数 3854关注度
往期回顾 全部

科技要闻

电池工厂着火是常事,但在宁德时代很意外

头条要闻

深圳优化分区住房限购政策 首套房最低首付比例15%

头条要闻

深圳优化分区住房限购政策 首套房最低首付比例15%

体育要闻

张帅横扫米内恩 晋级中网女单16强

娱乐要闻

王灿回应不是名媛,没报过名媛培训班

财经要闻

存量房贷利率降了 十大问题权威解读

汽车要闻

焕新上市 全新凯迪拉克XT5售26.59万起

态度原创

艺术
游戏
数码
旅游
公开课

艺术要闻

故宫珍藏的墨迹《十七帖》,比拓本更精良,这才是地道的魏晋写法

运营已近4年,这款产品藏着仙侠RPG领头羊的“流量密码”

数码要闻

普及100寸 打造画质新标杆!海信发布新一代AI电视:原生4K 165Hz高刷屏加持

旅游要闻

九寨沟景区10月2日、3日门票已售罄

公开课

眼花失眠抽筋,你的肝该调调了

无障碍浏览 进入关怀版