会话技术
1.Cookie
优点:http协议支持技术
缺点:移动端app无法使用cookie
不安全,用户可以禁用cookie
cookie不能跨域
2.session
底层基于cookie
优点:存在服务器中安全
缺点:集群环境下重复登录
cookie全部缺点
package com.itheima.controller;
import com.itheima.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
//存值
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
//取值
@GetMapping("/s2")
public Result session2(HttpSession session){
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
3.令牌
简洁自包含的格式,用于两方通信以json格式安全传输信息
组成
- Header:令牌类型,签名算法 如{"alg":"HS256","type":"JWT"}
- Payload(有效载荷)携带一些自定义数据,默认信息,如{"id":"1","username":"Tom"}
- Signature(签名) 负责Token被篡改,确保安全性,将header和palyoad融,并加入指定密钥,通过签名算法计算来的
加依赖jjwt
优点:支持pc,移动端
解决集群环境下认证问题
缓解服务器压力
缺点:要自己实现
过滤器
将资源请求拦截下来,实现特殊功能(登录校验,同一编码处理,敏感字符处理)
并且还有在启动类加
@ServletComponentScan //开启了SpringBoot对Servlet组件的支持
package com.itheima.filter;
import com.itheima.utils.CurrentHolder;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1. 获取到请求路径
String requestURI = request.getRequestURI(); // /employee/login
//2. 判断是否是登录请求, 如果路径中包含 /login, 说明是登录操作, 放行
if (requestURI.contains("/login")){
log.info("登录请求, 放行");
filterChain.doFilter(request, response);
return;
}
//3. 获取请求头中的token
String token = request.getHeader("token");
//4. 判断token是否存在, 如果不存在, 说明用户没有登录, 返回错误信息(响应401状态码)
if (token == null || token.isEmpty()){
log.info("令牌为空, 响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//5. 如果token存在, 校验令牌, 如果校验失败 -> 返回错误信息(响应401状态码)
try {
Claims claims = JwtUtils.parseToken(token);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId); //存入
log.info("当前登录员工ID: {}, 将其存入ThreadLocal", empId);
} catch (Exception e) {
log.info("令牌非法, 响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//6. 校验通过, 放行
log.info("令牌合法, 放行");
filterChain.doFilter(request, response);
//7. 删除ThreadLocal中的数据
CurrentHolder.remove();
}
}
三个部分
环境释放,资源清理
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("init 初始化方法 ....");
}
//拦截到请求之后, 执行, 会执行多次
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("拦截到了请求.... 放行前 .... ");
//放行
filterChain.doFilter(servletRequest, servletResponse);
log.info("拦截到了请求.... 放行后 .... ");
}
//销毁方法, web服务器关闭的时候执行, 只执行一次
@Override
public void destroy() {
log.info("destroy 销毁方法 ....");
}
拦截路径
过滤器链
过滤器执行属性按照字母排序先后执行
拦截器
实现HandlerInterceptor
@preHandle资源方法运行前 返回值true 同意放行
@postHandle资源方法运行后运行
@afterCompletion 视图渲染完毕后运行
配置还要WebMvcConfigurer 接口,负责拦截
拦截路径
registry.addInterceptor(tokenInterceptor)
// .addPathPatterns("/**") // 拦截所有请求
// .excludePathPatterns("/login"); // 不拦截哪些请求
package com.itheima.interceptor;
import com.itheima.utils.JwtUtils;
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;
/**
* 令牌校验的拦截器
*/
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// //1. 获取到请求路径
// String requestURI = request.getRequestURI(); // /employee/login
//
// //2. 判断是否是登录请求, 如果路径中包含 /login, 说明是登录操作, 放行
// if (requestURI.contains("/login")){
// log.info("登录请求, 放行");
// return true;
// }
//3. 获取请求头中的token
String token = request.getHeader("token");
//4. 判断token是否存在, 如果不存在, 说明用户没有登录, 返回错误信息(响应401状态码)
if (token == null || token.isEmpty()){
log.info("令牌为空, 响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
//5. 如果token存在, 校验令牌, 如果校验失败 -> 返回错误信息(响应401状态码)
try {
JwtUtils.parseToken(token);
} catch (Exception e) {
log.info("令牌非法, 响应401");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
//6. 校验通过, 放行
log.info("令牌合法, 放行");
return true;
}
}
package com.itheima.config;
import com.itheima.interceptor.DemoInterceptor;
import com.itheima.interceptor.TokenInterceptor;
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;
/**
* 配置类
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// @Autowired
// private DemoInterceptor demoInterceptor ;
//@Autowired
//private TokenInterceptor tokenInterceptor;
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
// registry.addInterceptor(tokenInterceptor)
// .addPathPatterns("/**") // 拦截所有请求
// .excludePathPatterns("/login"); // 不拦截哪些请求
// }
}
Filter与Interceptor区别
- 接口范围不同:过滤器实现Filter接口,拦截器实现HandlerInterceptor
- 拦截范围不同:过滤器会拦截所有资源,Interceptor只会拦截Spring环境的资源
AOP
面向切面编程,面向特定方法编程
应用通用逻辑(如日志、事务、安全)与核心业务逻辑解耦
引入spring-boot-starter-aop
连接点:JoinPoint 被AOP控制的方法
通知:Advice:指重复逻辑,共性功能
切入点:PointCut,匹配连接点条件,通知只会在切入点方法执行时被应用
切面:Asperct,描述通知和切入点的对应关系
目标对象:target,通知的应用对象
@Aspect标识当前是一个AOP类
@Around("execution(*com.itheim.servicr.impl.*.*(..))")设置这个包下任意方法都执行
AOP执行流程
动态代理:代理对象
通知类型
@Around:环绕通知,此注解标注的通知方法在目标方法执行前,后都被执行
@Before:前置通知 此注解标注的通知方法在目标方法执行前被执行
@After:后置通知此注解标注的通知方法在目标方法执行后被执行
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法执行后被执行,有异常不执行
@AfterThrowing:异常后通知,此注解标注的通知方法发生有异常执行
@pointCut
通知顺序
多个切面点匹配都到目标方法,目标方法运行时,多个通知方法都会执行
执行顺序
默认按切面名执行
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠后的先执行
用Order(数字)加载切面上控制顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
切入点表达式 execution
切入点表达式 @annotation
当在方法中加上@Log就会执行Aop
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
package com.itheima.aop;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.CurrentHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.itheima.anno.Log)")
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
// 计算耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 构建日志实体
OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentUserId()); // 这里需要你根据实际情况获取当前用户ID
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result != null ? result.toString() : "void");
olog.setCostTime(costTime);
// 保存日志
log.info("记录操作日志: {}", olog);
operateLogMapper.insert(olog);
return result;
}
private Integer getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
}
连接点
如上代码
通过JoinPoint抽象连接点,可以获取他方法执行时的信息,如目标类名,方法,方法参数
- 对于@Around通知,只能用ProceedingJoinPoint
- 对于其他4中只能用JoinPoint
ThreadLocal
什么是ThreadLocal
是线程局部变量,有隔离性,不同线程不会干扰
ThreadLocal应用场景
同一个线程/同一个请求,数据共享,比如说记录日志,谁操作的,一个请求 利用aop来绑定操作方法,获取操作人id
package com.itheima.utils;
/**
* 当前用户信息持有者工具类
* 使用ThreadLocal存储当前线程的用户信息,确保线程安全
*/
public class CurrentHolder {
// 存储当前用户ID的ThreadLocal变量
private static final ThreadLocal<Integer> CURRENT_ID_LOCAL = new ThreadLocal<>();
// 存储当前用户名的ThreadLocal变量
private static final ThreadLocal<String> CURRENT_USERNAME_LOCAL = new ThreadLocal<>();
private static final ThreadLocal<String> CURRENT_PASSWORD_LOCAL = new ThreadLocal<>();
/**
* 设置当前用户ID
* @param employeeId 用户ID
*/
public static void setCurrentId(Integer employeeId) {
CURRENT_ID_LOCAL.set(employeeId);
}
/**
* 获取当前用户ID
* @return 用户ID
*/
public static Integer getCurrentId() {
return CURRENT_ID_LOCAL.get();
}
/**
* 设置当前用户名
* @param username 用户名
*/
public static void setUsername(String username) {
CURRENT_USERNAME_LOCAL.set(username);
}
/**
* 获取当前用户名
* @return 用户名
*/
public static String getUsername() {
return CURRENT_USERNAME_LOCAL.get();
}
public static void setPassword(String password) {
CURRENT_PASSWORD_LOCAL.set( password);
}
public static String getPassword( String password) {
return CURRENT_PASSWORD_LOCAL.get();
}
/**
* 清除当前线程存储的所有用户信息
*/
public static void remove() {
CURRENT_ID_LOCAL.remove();
CURRENT_USERNAME_LOCAL.remove();
CURRENT_PASSWORD_LOCAL.remove();
}
}
配置文件的优先级
properties yml yaml
也可以通过java系统属性和命令行参数完成属性配置 命令行优先级高
Bean管理
bean作用域
bean默认单例 singleton 默认单例的bean在项目启动时,创建的,创建完毕后放入IOC容器
@Lazy延迟初始化,延迟到第一次使用的时候,创建bean
单例的bean用于没有修改数据的,不存在多线程共享数据的问题,减少bean的创建和销毁
多例的bean用于没有修改时间的,
面试题:Spring容器的bean是单列还是多例的,bean什么时候实例化
默认单例,一般是启动的时候,可以通过@Lazy懒加载 (懒加载好处:加快启动速度,减少不必要资源浪费,解决依赖循环问题)
Spring容器的bean是否线程安全
bean的线程安全取决于bean的状态和bean的作用域
单例bean:如果是无状态的bean,内部不保存任何状态,就是安全,有状态的bean,数据不同就有安全
第三方Bean
要管理第三方Bean,可以将这些bean集中管理,用@Configuration声明一个注解声明一个配置类
什么时候用@component声明bean,什么时候用@Bean注解
如果是项目中的自定义类用@component,第三方依赖的类用@Bean
SpringBoot原理
起步依赖
依赖传递
自动配置
什么是自动配置,
第三方依赖使用的@Component及衍生的注解声明@Bean不生效,需要被组件扫描注解扫描到
什么方案可以生效
第一种配置方案@ComponentScan组件扫描注解,,想要加入第三方依赖包,不仅要在pom文件加依赖,还有加扫描路径:太繁琐,性能不高
第二种@Import
导入普通类
导入配置类
导入ImportSelector接口实现类
第四种:通过@EnableXXX,封装@Import注解第三方依赖会自己配置好注解,
@Conditional及其衍生注解的作用是什么
满足给定条件,注册对于@Bean对象到IOC容器中
@Conditional及其衍生注解作用在什么地方
方法,类