当前位置: 首页 > news >正文

有状态(Session) VS 无状态(Token)

目录

概念

JWT

Token在项目中使用


概念

有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。

1、有状态服务
是指程序在执行过程中生成的中间数据,服务器端一般都要保存请求的相关信息,每个请求可以默认地使用以前的请求信息。示意图如下:

上图中,浏览器客户端请求后台数据,通过nginx负载均衡到3个tomcat服务器上,并且将Session信息保存在redis中,下一次客户端发送请求时,服务器检查session信息是否存在、是否有效、是否过期,检查无问题后,接受客户端请求并返回数据到客户端。此种情况为有状态,即服务端保存了客户信息(session中的信息)。

2、无状态服务。
是指容器在运行时,不在容器中保存任何数据,而将数据统一保存在容器外部。服务器端所能够处理的过程必须全部来自于请求所携带的信息,以及其他服务器端自身所保存的、并且可以被所有请求所使用的公共信息。

上图中,浏览器客户端请求后台数据时,携带了token信息,nginx负载均衡到tomcat服务器上,服务器通过解密token,判断用户的请求是否有效(用户是否登录,是否过期,token是否伪造),对真实有效的token,服务端接受客户端请求并返回数据到客户端。此种情况为无状态,即服务端没有保存客户信息。

3、有状态、无状态比较

特点有状态无状态
优点

服务端控制状态,方便处理

比如设置Session过期时间;

强制设置Session下线

无存储,简单方便

去中心化(有状态下session集中存放Redis)

缺点服务端存储了客户信息,增加服务器压力                        服务器控制能力弱

技术趋势:新架构的项目往往采用无状态模式

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。一个 JWT 实际上就是一个字符串,它由三部分组成,头部、载荷与签名。前两部分需要经过 Base64 编码,后一部分通过前两部分 Base64 编码后再加密而成。

JWT组成:Header + Payload + Signature

Header:头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等,如{"type":"JWT","alg":"HS256"},Base64 加密header后的字符串为(JWT官网 可以验证):eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload:一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。如:{"id":"1","name":"张三","sex":"male"},Base64 加密Payload后的字符串为(JWT官网 可以验证):eyJpZCI6IjEiLCJuYW1lIjoi5byg5LiJIiwic2V4IjoibWFsZSJ9

Signature:这个部分需要 Base64 加密后的 header 和 Base64 加密后的 payload 使用 “.” 连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 salt组合加密,然后就构成了 jwt 的第三部分。如:salt设置为abc,Signature字符串为:mZKsezNd5e5Q0Gi4vdeyEH3-ilxG_qEHkZp0gn7ayr0

综上,公式如下:

Token = Base64(Header).Base64(Payload).Base64(Signature)

Signature = Header指定的签名算法(Base64(header).Base64(payload),秘钥)

生成后的token信息为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoi5byg5LiJIiwic2V4IjoibWFsZSJ9.mZKsezNd5e5Q0Gi4vdeyEH3-ilxG_qEHkZp0gn7ayr0

Token在项目中使用

使用场景:在实际开发中,用户登录成功后,后端生成 jwt 返回给前端,之后前端与后端交互时携带 jwt 信息,让后端验证 jwt 的合法性。使用步骤如下:

1、添加依赖pom.xml

主要依赖:jsonwebtoken

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.6</version><relativePath/></parent><groupId>com.gingko</groupId><artifactId>springboot</artifactId><version>0.0.1-SNAPSHOT</version><name>springboot</name><dependencies><!-- 依赖spring-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot默认数据源(HikariCP)--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.16</version></dependency><!-- 整合MyBatis --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.2</version></dependency><!-- SpringBoot 的AOP实现 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- junit 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!-- SpringBoot 健康检查 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.0</version><scope>provided</scope></dependency><!-- jwt token --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.7</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.7</version><scope>runtime</scope></dependency><!-- fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>${maven.compiler.source}</source><target>${maven.compiler.target}</target><encoding>${project.build.sourceEncoding}</encoding></configuration></plugin></plugins></build></project>

2、token生成工具类【JWTUtils】

属性:密钥secret、token过期时间(秒)expireTimeSeconds 来自配置文件application.yml:

package com.gingko.common;import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Slf4j
@RequiredArgsConstructor
@Component
public class JWTUtils {/*** 秘钥*/@Value("${JWT.secret}")private String secret;/*** 有效期,单位秒* - 默认2周*/@Value("${JWT.expireTimeSeconds}")private Long expireTimeSeconds;/*** 从token中获取claim** @param token token* @return claim*/public Claims getClaimsFromToken(String token) {try {return Jwts.parser().setSigningKey(this.secret.getBytes()).parseClaimsJws(token).getBody();} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {log.error("token解析错误", e);throw new IllegalArgumentException("Token invalided.");}}/*** 获取token的过期时间** @param token token* @return 过期时间*/public Date getExpirationDateFromToken(String token) {return getClaimsFromToken(token).getExpiration();}/*** 判断token是否过期** @param token token* @return 已过期返回true,未过期返回false*/private Boolean isTokenExpired(String token) {Date expiration = getExpirationDateFromToken(token);return expiration.before(new Date());}/*** 计算token的过期时间** @return 过期时间*/private Date getExpirationTime() {return new Date(System.currentTimeMillis() + this.expireTimeSeconds * 1000);}/*** 为指定用户生成token** @param claims 用户信息* @return token*/public String generateToken(Map<String, Object> claims) {Date createdTime = new Date();Date expirationTime = this.getExpirationTime();byte[] keyBytes = secret.getBytes();SecretKey key = Keys.hmacShaKeyFor(keyBytes);return Jwts.builder().setClaims(claims).setIssuedAt(createdTime).setExpiration(expirationTime).signWith(key, SignatureAlgorithm.HS256).compact();}/*** 判断token是否非法** @param token token* @return 未过期返回true,否则返回false*/public Boolean validateToken(String token) {return !isTokenExpired(token);}/*** 测试类* @param args*/public static void main(String[] args) {// 1. 初始化JWTUtils jwtUtils = new JWTUtils();jwtUtils.expireTimeSeconds = 1209600L;jwtUtils.secret = "abcdefghijklmnopqrstuvwxyzgingkoabcdefghijklmnopqrstuvwxyz";// 2.设置用户信息HashMap<String, Object> objectObjectHashMap = new HashMap<>();objectObjectHashMap.put("id", "1");objectObjectHashMap.put("name", "张三");// 测试1: 生成tokenString token = jwtUtils.generateToken(objectObjectHashMap);// 会生成类似该字符串的内容:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOiIxIiwiaWF0IjoxNzI4MDE1NzkzLCJleHAiOjE3MjkyMjUzOTN9.2aLCkg0LpoPZXMyK_VfmGHuDcZB3oLQdFQOg6nfOAnASystem.out.println("token:" + token);// 测试2: token合法且未过期,返回true,上面生成的tokenString someToken = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOiIxIiwiaWF0IjoxNzI4MDE1NzkzLCJleHAiOjE3MjkyMjUzOTN9.2aLCkg0LpoPZXMyK_VfmGHuDcZB3oLQdFQOg6nfOAnA";Boolean validateToken = jwtUtils.validateToken(someToken);System.out.println("token是否合法:" + validateToken);// 测试3: 获取用户信息Claims claims = jwtUtils.getClaimsFromToken(someToken);System.out.println("用户信息:" + claims);// 测试4: 解密Header,token的第一段(以.为边界)String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";byte[] header = Base64.decodeBase64(encodedHeader.getBytes());System.out.println("header:" + new String(header));// 测试5: 解密Payload,token的第二段(以.为边界)String encodedPayload = "eyJuYW1lIjoi5byg5LiJIiwiaWQiOiIxIiwiaWF0IjoxNzI4MDE1NzkzLCJleHAiOjE3MjkyMjUzOTN9";byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());System.out.println("payload:" + new String(payload));// 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的boolean tokenValidFlag = jwtUtils.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOiIxIiwiaWF0IjoxNzI4MDE1NzkzLCJleHAiOjE3MjkyMjUzOTN9.C2aLCkg0LpoPZXMyK_VfmGHuDcZB3oLQdFQOg6nfOAnA");System.out.println("token被修改验证是否有效:" + tokenValidFlag);}
}

3、登录请求/toLogin

代码说明:登录成功后,返回前台token信息,前台可以保存token信息,发送其他请求时在请求的header带上token

package com.gingko.controller;import com.gingko.common.GenericWebResult;
import com.gingko.common.JWTUtils;
import com.gingko.entity.User;
import com.gingko.service.UserService;
import com.gingko.vo.req.UserLoginReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("user")
@Slf4j
public class UserController {@Resourceprivate UserService userService;@Resourceprivate JWTUtils jwtUtils;@PostMapping("/toLogin")public GenericWebResult toLogin(@RequestBody UserLoginReq userLoginReq) {GenericWebResult result = null;String userId = userLoginReq.getUserId();String userPassword = userLoginReq.getUserPassword();User userFromDB = this.userService.getByUserId(userId);if(userFromDB != null) {if(!userPassword.equals(userFromDB.getUserPassword())) {result = GenericWebResult.error("用户名或密码不正确");}}else {result = GenericWebResult.error("用户名或密码不正确");}//生成token并返回到前台Map<String, Object> claims = new HashMap<>();claims.put("userId",userLoginReq.getUserId());String token = jwtUtils.generateToken(claims);result = GenericWebResult.ok("登录成功",token);return result;}
}

4、配置登录拦截器

配置登录拦截器,并注册到webConfig中,拦截除登录之外的所有请求,拦截器中校验请求header中是否存在token及校验token是否合法

package com.gingko.config;import com.gingko.interceptor.LogInterceptor;
import com.gingko.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** web配置类,配置拦截器*/
@Configuration
public class WebConfig implements WebMvcConfigurer {@Resourceprivate LogInterceptor logInterceptor;//日志拦截器@Resourceprivate LoginInterceptor loginInterceptor;//登录拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(logInterceptor).addPathPatterns("/**").excludePathPatterns("/test/*");//测试相关的不用日志拦截registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/toLogin");//登录不用拦截}
}
package com.gingko.interceptor;import com.alibaba.fastjson.JSON;
import com.gingko.common.GenericWebResult;
import com.gingko.common.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;//登录拦截器,校验token是否合法
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Resourceprivate JWTUtils jwtUtils;/*** 校验token是否合法* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");response.setContentType("application/json");response.setCharacterEncoding("UTF-8");log.info("登录校验开始,token:{}", token);if (token == null || token.isEmpty()) {log.info("token为空,请求被拦截");response.setStatus(HttpStatus.UNAUTHORIZED.value());GenericWebResult genericWebResult = GenericWebResult.error("token为空,请求被拦截");String resultStr = JSON.toJSONString(genericWebResult);response.getWriter().write(resultStr);return false;}Boolean validateToken = null;try {validateToken = jwtUtils.validateToken(token);}catch (Exception e) {log.warn( "token无效,请求被拦截" );GenericWebResult genericWebResult = GenericWebResult.error("token无效,请求被拦截");String resultStr = JSON.toJSONString(genericWebResult);response.getWriter().write(resultStr);return false;}if(!validateToken) {log.warn( "token无效,请求被拦截" );GenericWebResult genericWebResult = GenericWebResult.error("token无效,请求被拦截");String resultStr = JSON.toJSONString(genericWebResult);response.getWriter().write(resultStr);return false;}return true;}
}

5、postman方式测试接口

假设数据库user表存在userId=thunder、密码为123456的记录

模拟其他请求,前台带上正确的token:

模拟其他请求,前台带上伪造的token:

http://www.lryc.cn/news/452909.html

相关文章:

  • 天坑!Spark+Hive+Paimon+Dolphinscheduler
  • JAVA——IO框架
  • 项目管理系统如何实现项目申报流程自动化?
  • ndb9300public-ndb2excel简介
  • C++:const成员
  • 基于ROS的激光雷达点云物体检测
  • 大模型训练环境搭建
  • 使用Java调用GeoTools实现全球国家矢量数据入库实战
  • 计算机毕业设计 基于Python的广东旅游数据分析系统的设计与实现 Python+Django+Vue Python爬虫 附源码 讲解 文档
  • Springboo通过http请求下载文件到服务器
  • 使用CSS实现酷炫加载
  • 【STM32-HAL库】AHT10温湿度传感器使用(STM32F407ZGT6配置i2c)(附带工程下载连接)
  • 深入理解网络通信: 长连接、短连接与WebSocket
  • Linux·环境变量与进程地址空间
  • MYSQL 乐观锁
  • SpringCloud入门(十二)全局过滤器和跨域
  • 51单片机系列-按键检测原理
  • 基于元神操作系统实现NTFS文件操作(五)
  • AutoCAD学习
  • go的一些知识点
  • 前端 vue3 对接科大讯飞的语音在线合成API
  • 缺省参数
  • Stable Diffusion绘画 | 来训练属于自己的模型:炼丹启动
  • 08_OpenCV文字图片绘制
  • 【笔记】选择题笔记+数据结构笔记
  • 浅谈汽车智能座舱如何实现多通道音频
  • 系统架构设计师教程 第13章 13.1层次式体系结构概述 笔记
  • cnn突破一(先搞定三层反馈神经网络bpnet,c#实现)
  • 如何创建一个docker,给它命名,且下次重新打开它
  • 【D3.js in Action 3 精译_025】3.4 让 D3 数据适应屏幕(中)—— 线性比例尺的用法