首页
留言
导航
统计
Search
1
追番推荐!免费看动漫的网站 - 支持在线观看和磁力下载
2,510 阅读
2
推荐31个docker应用,每一个都很实用
1,311 阅读
3
PVE自动启动 虚拟机 | 容器 顺序设置及参数说明
931 阅读
4
一条命令,永久激活!Office 2024!
618 阅读
5
优选 Cloudflare 官方 / 中转 IP
490 阅读
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
登录
Search
标签搜索
Java
小程序
Redis
SpringBoot
docker
Typecho
Cloudflare
docker部署
虚拟机
WordPress
群晖
uni-app
CentOS
Vue
Java类库
Linux命令
防火墙配置
Mysql
脚本
Nginx
微醺
累计撰写
264
篇文章
累计收到
11
条评论
首页
栏目
默认分类
服务器
宝塔
VPS
Docker
OpenWRT
Nginx
群晖
前端编程
Vue
React
Angular
NodeJS
uni-app
后端编程
Java
Python
SpringBoot
SpringCloud
流程引擎
检索引擎
Linux
CentOS
Ubuntu
Debian
数据库
Redis
MySQL
Oracle
虚拟机
VMware
VirtualBox
PVE
Hyper-V
计算机
网络技术
网站源码
主题模板
页面
留言
导航
统计
搜索到
2
篇与
的结果
2024-02-26
后端如何做到无感刷新Token?
前言为什么需要无感刷新Token?自动刷新token前端token续约疑问及思考为什么需要无感刷新Token?最近浏览到一个文章里面的提问,是这样的: 当我在系统页面上做业务操作的时候会出现突然闪退的情况,然后跳转到登录页面需要重新登录系统,系统使用了Redis做缓存来存储用户ID,和用户的token信息,这是什么问题呢?解答: 突然闪退,一般都是由于你的token过期的问题,导致身份失效。解决方案: 自动刷新tokentoken续约思路 如果Token即将过期,你在验证用户权限的同时,为用户生成一个新的Token并返回给客户端,客户端需要更新本地存储的Token,还可以做定时任务来刷新Token,可以不生成新的Token,在快过期的时候,直接给Token增加时间自动刷新token自动刷新token是属于后端的解决方案,由后端来检查一个Token的过期时间是否快要过期了,如果快要过期了,就往请求头中重新放一个token,然后前端那边做拦截,拿到请求头里面的新的token,如果这个新的token和老的token不一致,直接将本地的token更换接下来拿代码举例子先引入依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>这是一个生成token的例子import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; public class JwtUtil { // 有效期为 public static final Long JWT_TTL = 60 * 60 * 1000 * 24;// 60 * 60 * 1000 * 24 一个小时 // 设置秘钥明文 --- 自己改就行 public static final String JWT_KEY = "qx"; // 用于生成uuid,用来标识唯一 public static String getUUID(){ String uuid = UUID.randomUUID().toString().replaceAll("-", "");//token用UUID来代替 return uuid; } /** id : 标识唯一 subject : 我们想要加密存储的数据 ttl : 我们想要设置的过期时间 */ // 生成token jwt加密 subject token中要存放的数据(json格式) public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } // 生成token jwt加密 public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } // 创建token jwt加密 public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("sg") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } // 生成加密后的秘钥 secretKey public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } // jwt解密 public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }写个单元测试,测试一下@Test void test() throws Exception { String token = JwtUtil.createJWT("1735209949551763457"); System.out.println("Token: " + token); Date tokenExpirationDate = getTokenExpirationDate(token); System.out.println(tokenExpirationDate); System.out.println(tokenExpirationDate.toString()); long exp = tokenExpirationDate.getTime(); long cur = System.currentTimeMillis(); System.out.println(exp); System.out.println(cur); System.out.println(exp - cur); } // 解析令牌并获取过期时间 public static Date getTokenExpirationDate(String token) { try { SecretKey secretKey = generalKey(); Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); return claims.getExpiration(); } catch (ExpiredJwtException | SignatureException e) { throw new RuntimeException("Invalid token", e); } }可以看到我们的 exp 过期时间的毫秒数为 1703651262000可以看到我们的 cur 当前时间的毫秒数为 1703564863035我们将两者相减得到的值为 86398965ms,我们可以算一下一天的毫秒数是多少 1000 60 60 * 24 ms = 86400000ms这样我们就能够拿到token的过期时间tokenExpirationDate了我们就可以通过在校验token的时候,如果token校验通过了,此时我们拿到该token的过期时间,以(过期时间 - 当前时间)进行判断如果说 (过期时间 - 当前时间) 小于约定的值,那么我们就重新根据token里面的信息,重新创建一个token,将新的token放到请求头中返回给前端,前端去进行本地存储更新token前端token续约token的续约偏向于前端的解决方案,即由前端来进行token的过期时间的判断,首先前后端需要对接商量好一个token续约的接口,当前端发现这个token快要过期的时候,向后端发送该token,然后后端将该token的过期时间延长。「前端采用的是双Token的方式,access-token 和 refresh-token即 AT 和 RT」「而对于纯后端的方式,就是只有access-token这一个token」「那么问题来了 AT 和 RT 到底有什么区别?为什么需要RT?」「在前端实现方案来说,RT是用来在AT即将过期的时候,用RT获取最新的token」我解释一下我的观点:AT的暴露机会更多,每个请求都要携带,所以设置的过期时间短一点,「减少劫持风险」RT只会暴露在auth服务中用来刷新at,设置的过期时间长一点,「增加便利性。」AT 和 RT 是为了网络传输安全,网络传输中,容易暴露 AT,因为 AT 时间短,暴露后风险系数才低「这种是标准的安全处理,其实已经无需探讨他的合理性,就好像 https 之于 http 一样」疑问及思考要是前端有一个表单页面,长时间不进行请求的发送,此时用户填写完表单了,再点击提交的时候,后端返回401了,怎么办?也就是说,虽然你后端可以无感刷新Token,但是你后端无感刷新Token的前提是:前端得发请求,如果用户长时间不进行页面的交互,即没有进行任何业务逻辑的跳转什么的,就单纯的往表单上面填东西,什么请求也没发的情况下,后端是无法感知Token过期的这种情况怎么解决? 对于纯后端的解决方案,我是这样想的让前端在表单填写内容的时候做处理,如果提交返回的是401,那么前端就需要获取表单存在本地存储 然后跳转登录页,登录成功后,返回这个页面,然后从本地存储取出来再回显到表单上面。对于前端的解决方案,我是这样想的对于后端来说就是AT过期了,而对于前端来说就是AT和RT都过期了,怎么处理?需要监听refresh token的过期时间,在接近过期的时候向后端发起请求来刷新refresh token 或者是定期刷新一下refresh token和后端的解决方案一样,前端做一个类似草稿箱的功能对表单等元素进行保存
2024年02月26日
29 阅读
0 评论
0 点赞
2023-02-11
SpringBoot集成JWT实现token验证
JWT官网:https://jwt.io/ 什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。 JWT请求流程 1. 用户使用账号和面发出post请求; 2. 服务器使用私钥创建一个jwt; 3. 服务器返回这个jwt给浏览器; 4. 浏览器将该jwt串在请求头中像服务器发送请求; 5. 服务器验证该jwt; 6. 返回响应的资源给浏览器。 JWT的主要应用场景 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。 优点 1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库 3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。 4.不需要在服务端保存会话信息,特别适用于分布式微服务。 JWT的结构 JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。 就像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ JWT包含了三部分: Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型) Payload 负载 (类似于飞机上承载的物品) Signature 签名/签证 Header JWT的头部承载两部分信息:token类型和采用的加密算法。 { "alg": "HS256", "typ": "JWT" } 声明类型:这里是jwt 声明加密的算法:通常直接使用 HMAC SHA256 加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。 MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值 SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5 HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证 Payload 载荷就是存放有效信息的地方。 有效信息包含三个部分 1.标准中注册的声明 2.公共的声明 3.私有的声明 标准中注册的声明 (建议但不强制使用) : iss: jwt签发者 sub: 面向的用户(jwt所面向的用户) aud: 接收jwt的一方 exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间) nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密. 私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 Signature jwt的第三部分是一个签证信息 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。 密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。 下面来进行SpringBoot和JWT的集成 引入JWT依赖,由于是基于Java,所以需要的是java-jwt <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> 需要自定义两个注解 用来跳过验证的PassToken @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; } 需要登录才能进行操作的注解UserLoginToken @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; } @Target:注解的作用目标 @Target(ElementType.TYPE)——接口、类、枚举、注解 @Target(ElementType.FIELD)——字段、枚举的常量 @Target(ElementType.METHOD)——方法 @Target(ElementType.PARAMETER)——方法参数 @Target(ElementType.CONSTRUCTOR) ——构造函数 @Target(ElementType.LOCAL_VARIABLE)——局部变量 @Target(ElementType.ANNOTATION_TYPE)——注解 @Target(ElementType.PACKAGE)——包 @Retention:注解的保留位置 RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。 RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。 RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。 @Document:说明该注解将被包含在javadoc中 @Inherited:说明子类可以继承父类中的该注解 简单自定义一个实体类User,使用lombok简化实体类的编写 @Data @AllArgsConstructor @NoArgsConstructor public class User { String Id; String username; String password; } 需要写token的生成方法 public String getToken(User user) { String token=""; token= JWT.create().withAudience(user.getId()) .sign(Algorithm.HMAC256(user.getPassword())); return token; } Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。 withAudience()存入需要保存在token的信息,这里我把用户ID存入token中 接下来需要写一个拦截器去获取token并验证token public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } 实现一个拦截器就需要实现HandlerInterceptor接口 HandlerInterceptor接口主要定义了三个方法 1.boolean preHandle (): 预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行 postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。 2.void postHandle(): 后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。 3.void afterCompletion(): 整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中 主要流程: 1.从 http 请求头中取出 token, 2.判断是否映射到方法 3.检查是否有passtoken注释,有则跳过认证 4.检查有没有需要用户登录的注解,有则需要取出并验证 5.认证通过则可以访问,不通过会报相关错误信息 配置拦截器 在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内 @Configuration public class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } } WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口 WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在 WebMvcConfigurerAdapter子类中@Override对应方法就可以了。 注: 在SpringBoot2.0及Spring 5.0中WebMvcConfigurerAdapter已被废弃 网上有说改为继承WebMvcConfigurationSupport,不过试了下,还是过期的 解决方法: 直接实现WebMvcConfigurer (官方推荐) @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } } InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。 这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录 在数据访问接口中加入登录操作注解 @RestController @RequestMapping("api") public class UserApi { @Autowired UserService userService; @Autowired TokenService tokenService; //登录 @PostMapping("/login") public Object login(@RequestBody User user){ JSONObject jsonObject=new JSONObject(); User userForBase=userService.findByUsername(user); if(userForBase==null){ jsonObject.put("message","登录失败,用户不存在"); return jsonObject; }else { if (!userForBase.getPassword().equals(user.getPassword())){ jsonObject.put("message","登录失败,密码错误"); return jsonObject; }else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } @UserLoginToken @GetMapping("/getMessage") public String getMessage(){ return "你已通过验证"; } } 不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问 下面进行测试,启动项目,使用postman测试接口 在没token的情况下访问api/getMessage接口 我这里使用了统一异常处理,所以只看到错误message 下面进行登录,从而获取token 登录操作我没加验证注解,所以可以直接访问 把token加在请求头中,再次访问api/getMessage接口 注意:这里的key一定不能错,因为在拦截器中是取关键字token的值 String token = httpServletRequest.getHeader("token"); 加上token之后就可以顺利通过验证和进行接口访问了
2023年02月11日
158 阅读
1 评论
0 点赞