会话技术

用户打开浏览器,访问服务器资源,则会话建立。直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。一般认为打开一个浏览器就是一次会话。HTTP是无状态的,服务器并不知道你之前登陆过没有,需要用会话跟踪技术记录状态。

会话跟踪是一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

会话跟踪方案包括客户端会话跟踪技术Cookie、服务端会话跟踪技术Session、令牌技术。

第一次发送请求,服务端的响应头中包含 Set-Cookie,浏览器解析响应头,存储在本地。浏览器存储的 Cookie 可以在控制台的“应用->存储”中看到。接下来发送请求,就会在请求头中携带 Cookie 这一项。

优点 缺点
HTTP协议中支持。 移动端APP无法使用。
不安全、用户可以禁用。
不能跨域(协议、IP或域名、端口有一个不同就算)。

Session

浏览器第一次请求,服务器创建一个Session,用 ID 标识。服务器在响应时用 Set-Cookie 字段返回 ID,浏览器存储在本地。接下来发送请求,就会在请求头中携带 Cookie 这一项,即 Session 的 ID。这种技术只在浏览器中存储 ID,不用 Cookie 存储全部信息。

优点 缺点
存储在服务端、安全。 服务器集群环境下无法使用。
基于Cookie,包含Cookie的缺点。

令牌

登陆成功直接生成一个令牌,浏览器接收作为唯一身份认证(不一定要保存在Cookie中,前端可以存放在 LocalStorage)。接下来每次请求都携带令牌。

优点:

  • 支持PC、移动端,
  • 解决集群环境下的认证问题,
  • 伪造的令牌可以被检查到,
  • 减轻服务器存储压力。

定义了一种简洁的、自包含的格式,用于在通信双方以 Json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

过滤器和拦截器

现在主流的方式是使用过滤器 Filter 和拦截器 Interceptor,通过校验 JWT(JSON Web Token) 令牌对请求进行筛选并放行。具体来说:

  • 若发送登陆请求,直接放行;
  • 若没有携带JWT或JWT无效,则不放行;
  • JWT有效,放行。

Filter

Filter 是 Java Web 三大组件之一,还有两个是 Servlet 和 Listener。

实现的 Filter 需要加上注解 @WebFilter,用 urlpattern 设置拦截的路径。此外还需要在主类上添加一个注解 @ServletComponentScan,因为默认是不扫描 Servlet 相关组件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.joe.tlias;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan
@SpringBootApplication
public class TliasApplication {
public static void main(String[] args) {
SpringApplication.run(TliasApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.joe.tlias.filter;

import java.io.IOException;

import org.springframework.util.StringUtils;

import com.alibaba.fastjson2.JSON;
import com.joe.tlias.pojo.Result;
import com.joe.tlias.utils.JwtUtils;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@WebFilter(urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String url = req.getRequestURL().toString();
if (url.contains("login")) {
log.info("直接放行登陆。");
chain.doFilter(req, resp);
return;
}

String jwt = req.getHeader("token");

if (!StringUtils.hasLength(jwt)) {
log.info("token为空,返回未登录。");
Result error = Result.error("NOT_LOGIN");
resp.getWriter().write(JSON.toJSONString(error));
return;
}

try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌错误");
Result error = Result.error("NOT_LOGIN");
resp.getWriter().write(JSON.toJSONString(error));
return;
}
log.info("令牌合法,放行。");
chain.doFilter(request, response);
}
}

过滤器链执行流程是和过滤器类名的字典序相关的,比如 AFilterBFilter 前先执行。

Interceptor

Interceptor 是一种动态拦截方法调用的机制,类似于 Filter。这是 Spring 提供的,用于动态拦截控制器方法的执行。Interceptor 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

定义 Interceptor 需要实现 HandlerInterceptor 接口,并重写其所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.joe.tlias.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson2.JSON;
import com.joe.tlias.pojo.Result;
import com.joe.tlias.utils.JwtUtils;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler)
throws Exception {

String jwt = req.getHeader("token");

if (!StringUtils.hasLength(jwt)) {
log.info("token为空,返回未登录。");
Result error = Result.error("NOT_LOGIN");
resp.getWriter().write(JSON.toJSONString(error));
return false;
}

try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
log.info("解析令牌错误");
Result error = Result.error("NOT_LOGIN");
resp.getWriter().write(JSON.toJSONString(error));
return false;
}

log.info("令牌合法,放行。");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("post");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("after");
}
}

接着要注册拦截器,需要实现 WebMvcConfigurer 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.joe.tlias.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.joe.tlias.interceptor.LoginCheckInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}

对比

请求拦截示意图(来自黑马课程)

两者拦截范围不同,过滤器会拦截所有的资源,而拦截器只会拦截 Spring 环境中的资源。