会话技术
用户打开浏览器,访问服务器资源,则会话建立。直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。一般认为打开一个浏览器就是一次会话。HTTP是无状态的,服务器并不知道你之前登陆过没有,需要用会话跟踪技术记录状态。
会话跟踪是一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案包括客户端会话跟踪技术Cookie、服务端会话跟踪技术Session、令牌技术。
Cookie
第一次发送请求,服务端的响应头中包含 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); } }
|
过滤器链执行流程是和过滤器类名的字典序相关的,比如 AFilter
在 BFilter
前先执行。
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 环境中的资源。