目录
概念
有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。
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: 生成token
String token = jwtUtils.generateToken(objectObjectHashMap);
// 会生成类似该字符串的内容:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOiIxIiwiaWF0IjoxNzI4MDE1NzkzLCJleHAiOjE3MjkyMjUzOTN9.2aLCkg0LpoPZXMyK_VfmGHuDcZB3oLQdFQOg6nfOAnA
System.out.println("token:" + token);
// 测试2: token合法且未过期,返回true,上面生成的token
String 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 {
@Resource
private UserService userService;
@Resource
private 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 {
@Resource
private LogInterceptor logInterceptor;//日志拦截器
@Resource
private LoginInterceptor loginInterceptor;//登录拦截器
@Override
public 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 {
@Resource
private JWTUtils jwtUtils;
/**
* 校验token是否合法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public 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: