会话技术
会话:用户打开浏览器,访问 web 服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
Cookie
Cookie 是 HTTP 协议支持的技术,在浏览器第一次访问时,服务端请求头中设置 Cookie
Set-Cookie: your_cookie
,浏览器会自动把 Cookie 存在本地,并在下次访问时自动在请求头中加上 Cookie Cookie: your_cookie
@RestController
public class SessionController {
// 设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_name", "yexca"));
return Result.success();
}
// 获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie c : cookies){
if(c.getName().equals("login_name")){
System.out.println("login_name:"+c.getValue());
}
}
return Result.success();
}
}
不过移动端不能使用 Cookie,并且 Cookie 无法跨域
相同域:同协议、IP/域名、端口
Session
基于 Cookie 实现,在浏览器第一次请求时生成一个 Session,然后在响应头返回 Session 的 ID Set-Cookie: JSESSIONID=session_id
,然后浏览器在下次请求时自动带上该 ID
@RestController
public class SessionController {
// 往HttpSession中存储值
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession_set:{}", session.hashCode());
session.setAttribute("login_user","yexca");
return Result.success();
}
// 从HttpSession中获取值
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession_get:{}", session.hashCode());
Object loginUser = session.getAttribute("login_user");
log.info("loginUser:{}", loginUser);
return Result.success();
}
}
而 Session 的问题在于若服务端使用负载均衡,即有多台服务端,若第一次响应的为服务器一,而第二次响应为服务器二,由于服务器二里没有 Session,所以无法使用
JWT 令牌
令牌技术支持 PC 端、移动端,可以解决集群环境下的认证问题,减轻服务器端存储压力,但需要自己实现
JWT 全称 JSON Web Token,官网: https://jwt.io/ ,定义了一种简洁的、自包含的格式,用于在通信双方以 JSON 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的
组成:
- 第一部分:Header,记录令牌类型、签名算法等
- 第二部分:Payload (有效载荷),记录一些自定义信息、默认信息等
- 第三部分:Signature (签名),防止 Token 被篡改、确保安全性。将 Header、Payload 和指定密钥,通过指定签名算法计算而来
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成与解析
class WebApplicationTests {
// JWT生成
@Test
public void jwtGenTest(){
Map<String,Object> claims = new HashMap<>();
claims.put("id", "233");
claims.put("user", "yexca");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "yexca") // 签名算法
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 有效期为1h
.compact();
System.out.println(jwt);
}
// 输出
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU
// JWT解析
@Test
public void jwtParseTest(){
Claims claims = Jwts.parser()
.setSigningKey("yexca") // 密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU")
.getBody(); // 获得第二部分
System.out.println(claims);
}
// 输出
// {id=233, exp=1702797088, user=yexca}
}
登录校验
过滤器 (Filter)
Filter 是早期 JavaWeb 三大组件 (Servlet, Filter, Listener) 之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能,如登录校验、统一编码处理、敏感字符处理等
快速入门
创建 Filter 类
@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("Filter 初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 拦截");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
// Filter.super.destroy();
System.out.println("Filter 销毁");
}
}
在 Application 加上注解
@ServletComponentScan // 开启了对servlet组件的支持
@SpringBootApplication
拦截路径
可以根据需求调整,以下为示例
拦截类型 | urlPatterns | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 才会被拦截 |
目录拦截 | /emps/* | 访问 /emps 下所有资源都会被拦截,/emps 也会拦截 |
拦截所有 | /* | 访问所有资源都会被拦截 |
单个过滤器执行逻辑
浏览器发送请求 -> 请求被拦截 -> 执行放行前逻辑 -> 放行 -> 执行放行后逻辑 -> 浏览器收到响应
@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("Filter 初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter 拦截,放行前逻辑");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Filter 拦截,放行后逻辑");
}
@Override
public void destroy() {
// Filter.super.destroy();
System.out.println("Filter 销毁");
}
}
过滤器链
一个 Web 应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
顺序:注解配置的 Filter 优先级是通过过滤器类名的自然排序
逻辑:浏览器发送请求 -> 请求被 A 拦截 -> 执行 A 放行前逻辑 -> A 放行 -> 请求被 B 拦截 -> 执行 B 放行前逻辑 -> B 放行 -> 执行 B 放行后逻辑 -> 执行 A 放行后逻辑 -> 浏览器收到响应
登录校验
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取请求url
String requestURI = request.getRequestURI();
log.info("请求URL:{}",requestURI);
// 判断是否有login
if(requestURI.contains("login")){
log.info("登录操作,放行");
filterChain.doFilter(servletRequest, servletResponse);
return; // 因为登录操作不需要以下逻辑
}
// 非登录,获取令牌
String jwt = request.getHeader("token");
// 判断令牌是否有效
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空");
// 将对象转换为JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return;
}
// 校验令牌
try {
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败");
// 将对象转换为JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return;
}
// 放行
log.info("令牌合法,放行");
filterChain.doFilter(servletRequest,servletResponse);
}
}
拦截器 (Interceptor)
快速入门
创建拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标方法运行前运行,返回ture放行,false不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// return HandlerInterceptor.super.preHandle(request, response, handler);
System.out.println("preHandle");
return true;
}
@Override // 目标方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}
@Override // 视图渲染完成后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}
创建配置
@Configuration // 配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
拦截路径
路径 | 含义 | 例子 |
---|---|---|
/* | 一级路径 | /emps, /login 不能匹配 /emps/1 |
/** | 任意级路径 | /emps, /emps/1, /emps/1/2 |
/emps/* | /emps 下的一级路径 | /emps/1 不能匹配 /emps, /emps/1/2 |
/emps/** | /emps 下的任意级路径 | /emps, /emps/1, /emps/1/2 |
拦截流程
如果同时存在过滤器和拦截器
浏览器访问 -> filter 放行前逻辑 -> filter 放行 -> DispatcherServlet -> Interceptor preHandle -> Controller -> postHandle -> afterCompletion -> DispatcherServlet -> filter 放行后逻辑 -> 响应浏览器
Filter 会拦截所有的请求,而 Interceptor 只会拦截 Spring 环境中的资源
登录校验
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标方法运行前运行,返回ture放行,false不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// return HandlerInterceptor.super.preHandle(request, response, handler);
System.out.println("preHandle");
// 获取请求url
String requestURI = request.getRequestURI();
log.info("请求URL:{}",requestURI);
// 判断是否有login
if(requestURI.contains("login")){
log.info("登录操作,放行");
return true;
}
// 非登录,获取令牌
String jwt = request.getHeader("token");
// 判断令牌是否有效
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空");
// 将对象转换为JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 校验令牌
try {
JwtUtils.parseJWT(jwt);
}catch (Exception e){
e.printStackTrace();
log.info("解析令牌失败");
// 将对象转换为JSON
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 放行
log.info("令牌合法,放行");
return true;
}
@Override // 目标方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
System.out.println("postHandle");
}
@Override // 视图渲染完成后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion");
}
}