关于会话:单个客户端浏览器之间与服务器进行关联,即便客户端发出多次请求,只要是同一个浏览器,就被成为一次会话;同理,3个浏览器分别发出1次请求,那么就是3次会话;
会话跟踪技术:其技术的核心要点就是解决服务器如何知道你是你的问题。http每次请求都是独立的,那么每次发出请求的时候,该如何知道这个请求是由你当前的浏览器发出的呢?再比如,你登录了网页,服务器是如何知道你已经登录了呢?
就引出了3种技术cookie、session、jwt令牌三种技术,我就在此说明其中最主要的区别;
cookie技术:初次访问服务器,在服务器中生成cookie类似以下:
Set-Cookie: sessionId=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT; HttpOnly; Secure
中随响应头( Set-Cookie)返回,后续访问一直在请求头中携带(cookie),你只要带着这个cookie,服务器就知道你是你了;
session技术:同理,也是cookie技术的一种应用,使用过程与cookie类似;
次访问服务器中随响应头( Set-Cookie)返回得来,后续访问一直在请求头中携带(cookie),你只要带着这个cookie,服务器就知道你是你了;
Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly; Secure
两者区别在于:
客户端只传递Session ID,不包含实际的会话数据。不是传递整个Session对象,只传递Session ID,存在Session中的私密数据无法被客户端直接访问到。通过这个Session ID,服务端就可以知道你是你了,而且cookie几乎是明文的数据。
通过F12,应用程序(application),存储中的cookie可以查看本次会话的cookie;
两者的缺点也是共有的:最主要还是不支持跨域(部署多台服务器会导致IP不同,所以会产生跨域问题),若是使用这两种技术,会导致
不同IP之间cookie/session不能共享
- URL1:
http://192.168.1.1/page1
- URL2:
http://192.168.1.2/page2
不同端口之间cookie/session不能共享
- URL1:
http://192.168.1.1:80/page1
- URL2:
http://192.168.1.1:8080/page2
不能共享意味着服务器就不知道你是你了。
由此就引出了JWT(JSON Web Token)令牌的技术,JWT令牌的使用过程和上述两种技术类似,同样是第一次请求服务端时,在响应体中携带(token)返回
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123
Date: Sat, 05 Nov 2023 12:00:00 GMT
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
之后每次访问服务器的时候都带上这个token,服务器就知道你是你了。
关于这点
首先解释下token
/**
*token被分为3个部分:header头部/payload载荷/signature签名
*header头部/payload载荷是由base64将原始jason编码而来,能编码就能解码。
*signature签名:将header、payload,并加入指定秘钥(自己设置),通过指定签名算法(header中的HS256算法)计算而来
*分别如下
*/
//header头部,解码后为:{"alg": "HS256", "typ": "JWT"}
//加密算法(algorithm)为:HS256
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
//payload载荷,解码后为:{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
//signature签名
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
signature签名部分是由前面可以直接解码的header、payload通过header头部的“HS256”算法和
下面例子中的秘钥(“itniuma”)经过计算而成;
若是token中的header、payload、signature任何一部分部分发生变动,验证就会失败,验证大概逻辑可以表示为:header+payload不等于signature;而token只要没有变动,header+payload等于signature,验证成功;
token只要在服务端经过验证,验证表明信息是确实是通过我服务端的秘钥(“itniuma”)加密而成,那么就可以直接放行了。
只要多个服务器拥有该密钥,就可以给token解密,以下是解密一个的一个工具类
public class JwtUtils {
private static String signKey = "itniuma";//签名密钥
private static Long expire = 43200000L; //有效时间
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)//自定义信息(有效载荷)
.signWith(SignatureAlgorithm.HS256, signKey)//签名算法(头部)
.setExpiration(new Date(System.currentTimeMillis() + expire))//过期时间
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)//指定签名密钥
.parseClaimsJws(jwt)//指定令牌Token
.getBody();
return claims;
}
}
前面已经说了token大概得流程,但是关于token在服务器端更具体的验证流程,涉及到拦截器的使用,接下来就说明拦截器的具体使用方式和基本概念。
拦截器(Interceptor)是用于在Spring MVC中对请求进行预处理和后处理的组件。可以把它想象成一个过滤器,它可以在请求到达控制器之前和响应返回给客户端之后执行一些操作。但是是在Filter过滤器之后执行。
(知道localhost:8080/127.0.0.1:8080 就能找到服务器地址和对应端口号)
具体执行流程为:过滤器Filter-DispatcherServlet-interceptor-controller
关于DispatcherServlet的理解,其实相当于一张路由表,打个比方,浏览器知道目的地("/login")但是不知道具体方位,DispatcherServlet为前端浏览器带路,带路到后端的("/login")的具体地点。折返也是同理。
拦截器(Interceptor),简单的说,他就跟你楼下保安的作用一样,经过保安检查才能让你进小区。
以下是具体的示例代码:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//初入小区, 被保安检查。没问题让进,有问题不让进。
// 在请求到达controller之前执行
System.out.println("前置处理:请求路径 = " + request.getRequestURI());
return true; // 返回 true 继续处理,返回 false 中断请求
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//出门时, 被保安检查。但保安没权力阻止你出去。
// 在controller处理完请求后,但在视图渲染之前执行
System.out.println("后置处理:请求处理完毕");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//出门后, 保安关门。
// 在请求完成后执行(响应已经返回)
System.out.println("请求完成");
}
}
在controller处理完请求后,但在视图渲染之前执行?
关于视图的概念,普遍的来说是指的MVC(Model-View-Controller,模型-视图-控制器)架构中的
View.如下:
模型(Model):模型代表应用程序的数据和业务逻辑。它负责获取、存储和处理数据,比如从数据库中获取用户信息。
控制器(Controller):控制器是连接模型和视图的桥梁。它接收用户的输入(如点击按钮、提交表单等),调用模型进行数据处理,然后选择相应的视图来展示数据。
视图(View):视图是用户界面的表现部分。它从控制器接收数据,并将这些数据呈现给用户。在Web应用中,视图通常以HTML、CSS和JavaScript的形式存在。
虽然还是比较好理解了,但是还是比较难理解。
他们是怎样一种执行过程的呢?
用个比方可能会更好理解,有一家三口.
爸爸(model),负责搬砖(数据),任劳任怨,俗称老实人。
妈妈(controller),善于沟通,但不工作不搬砖,只会命令爸爸model搬砖(数据)。
女儿(View)是一位爱美的女孩,他需要钱买衣服打扮自己,但不会向操劳的父亲直接要钱,她只会向她妈要钱(数据),其实他妈只能找她爸要钱;当她要钱之后他会将钱转化为自己的打扮,将衣服(数据)显示在自己外表上。
View视图通常是动态的页面(展示后端数据)和静态的页面(不展示后端数据)的结合
那么视图的更具体的含义就引申出来了,女儿View 负责展示衣服(数据),这衣服就属于一个动态的页面,而女儿的身体就是一个衣服架子即是不会变化的静态页面。
女儿的衣服可以更换,但是身体确实始终不变的。动态页面中的数据可以变化,但是静态页面的数据不会变化。
在controller处理完请求后,但在视图渲染之前执行?
即是在女儿要到钱,换上新衣服之前。(代码中也提到的出门时保安检查)
即前端页面没有接收到后端返回结果之前。
就很好理解了。
然后就是拦截器的使用,除了示例代码之外还需要再实现WebMvcConfigurer接口,并重写addInterceptors方法
@Configuration
public class MyInterceptor implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(myCustomInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除特定的请求
}
}
设置拦截器的请求路径
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
那么更具体一点讲解拦截器的基本使用:
拦截器在业务中通常用于处理跨切关注点,比如参数处理、安全检查等,以下就举参数处理和安全检查的例子:
参数处理:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//参数处理
public class RequestParamInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String param = request.getParameter("param");
if (param != null) {
// 进行参数处理,比如去除空格、转换大小写等
param = param.trim().toLowerCase();
request.setAttribute("param", param);
}
return true;
}
}
安全检查(验证token):
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//第一步,首先判断请求头中有没有携带token,有就继续没有返回
//第二部,验证token
return true;
}
}
更详细的代码:
import com.alibaba.fastjson.JSONObject;
import com.example.springbootcaseday10.pojo.Result;
import com.example.springbootcaseday10.utils.JwtUtils;
import com.example.springbootcaseday10.utils.ValidationUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
@Slf4j
public class MyCustomInterceptor implements HandlerInterceptor {
public static final String JSON = "application/json;charset=utf-8";
public static final String HTML = "text/html;charset=utf-8";
public static final String TEXT = "text/plain;charset=utf-8";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse= (HttpServletResponse) response;
String token = httpServletRequest.getHeader("token");
if(ValidationUtils.isAnyNullOrEmpty(token)){
//只能返回jason,将统一返回类转化成jason
String notLoginResult = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
httpServletResponse.setContentType(this.JSON);
response.getWriter().write(notLoginResult);
}
try {
JwtUtils.parseJWT(token);
}catch (Exception e) {
log.info("令牌解析失败!");
//只能返回jason,将统一返回类转化成jason
String notLoginResult = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
httpServletResponse.setContentType(this.JSON);
response.getWriter().write(notLoginResult);
}
return true; // 返回true继续处理请求,返回false则终止请求
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 在请求处理之后执行的逻辑
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 在请求完成后执行的逻辑
}
}
maven中需要添加转化为jason的工具类,大概就是这坨:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency>