JavaWeb25.02.27
JavaWeb学习记录
2025.02.27 学习小总结
今天主要看的是登录验证这个板块,认识了三个登录会话技术,两个登录校验的方法,以及SpringAOP的入门案例。
登录校验
学习的三个会话技术分别是:Cookie,Session和令牌技术,主要使用的还是令牌技术,另外两个技术大概了解了一下,这里就不赘述了。
学习的令牌技术是JWT令牌技术,目前应用的比较广泛,JWT令牌由三个部分组成
-
头部:(typ:标识令牌的类型 ,alg:使用的签名算法)
-
有效载荷 :有效载荷包含了声明,声明是关于实体以及其他数据的声明,写的案例是关于校验登陆的案例,所以令牌里面携带的是id(唯一值)和用户名
-
签名:为了防止数据被篡改,JWT需要使用密钥进行签名,密钥是由服务器端自己提供的
下面的代码就是关于JWT的使用:生成JWT令牌和解析JWT令牌,里面还可以添加上过期时间
public class JwtUtils {
private static String signKey = "SVRIRUlNQQ==";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @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)
.getBody();
return claims;
}
}
JWT令牌会储存在每一个请求的token里面,会一直携带着,可以根据这个特点来进行登陆校验的功能。但是因为一个案例中的控制层和服务层的方法有很多。不可能在每一个方法中都添加上校验,这样特别的麻烦,于是就有了过滤器和拦截器。
过滤器可以拦截所有的请求,浏览器发送请求的时候回先到达过滤器,如果通过验证,过滤器就放行,再到控制层,服务层和数据持久层。然后返回的时候,也会经过过滤器,再返回到浏览器。所以可以在过滤器中解析令牌,判断该请求是否合法。
下面是代码示例:
@Slf4j
@WebFilter("/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//1.首先将协议转化为HTTP类型的协议
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//2.获取请求路径
String requestURI = request.getRequestURI();
//3.判断路径中是否包含登录请求,如果是就放行
if(requestURI.contains("login")){
filterChain.doFilter(request,response);
return;
}
//4.获取请求头,判断是否为空,为空就响应401状态码
String token = request.getHeader("token");
if(token==null ||token.isEmpty()){
log.info("令牌为空,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//5.解析请求头token
try {
Claims claims = JwtUtils.parseJWT(token);
Integer empId=Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);
log.info("解析成功,放行");
filterChain.doFilter(request,response);
} catch (Exception e) {
log.info("令牌解析失败,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
CurrentHolder.remove();
}
}
需要拦截的路径在注解@WebFilter中添加 /* 就是拦截所有的路径
同时在启动Spring方法上还要添加注解@ServletComponentScan,这样能够扫描到所有的拦截。过滤器的顺序是按照自然排序的。
拦截器也差不多是一个道理,在过滤器后面,是先到过滤器再到拦截器,再到控制层然后往下。但拦截器只能拦截Spring的请求。
下面是拦截器的代码:
这是注册拦截器的代码:
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();
//2.判断路径中是否包含登录请求,如果是就放行
if(requestURI.contains("login")){
return true;
}
//4.获取请求头,判断是否为空,为空就响应401状态码
String token = request.getHeader("token");
if(token==null ||token.isEmpty()){
log.info("令牌为空,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
//5.解析请求头token
try {
JwtUtils.parseJWT(token);
return true;
} catch (Exception e) {
log.info("令牌解析失败,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
}
还需要实现WebMvcConfigure接口 启动拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加拦截器
*/
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
这样就能够顺利的完成登录校验的功能了
SpringAOP
SpringAOP解决了一些重复代码的问题。例如日志记录,比如要记录对员工表的操作,添加,删除,更新员工表的时间,案例里这样的表和同样的操作有很多,如果一个一个写,记录操作前时间,操作后时间,操作时间,代码重复很高,而且也很麻烦,这个时候就有AOP技术,使用代理模式的方法,在目标资源执行前执行,在目标资源执行后执行,资源就省略了一大部分的代码。
下面是代码示例:
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
// 环绕通知
@Around("@annotation(com.itheima.anno.LogOperation)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
// 执行方法
Object result = joinPoint.proceed();
// 当前时间
long endTime = System.currentTimeMillis();
// 耗时
long costTime = endTime - startTime;
// 构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
operateLog.setReturnValue(result.toString());
operateLog.setCostTime(costTime);
// 插入日志
operateLogMapper.insert(operateLog);
return result;
}
// 示例方法,获取当前用户ID
private int getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
}
这一句代码,就是执行目标资源的代码
Object result = joinPoint.proceed();
这是指定生效的目标资源,annotion是采用了注解的方式,在需要执行这个放的方法名上加上对应的注释,就可以生效了
@Around("@annotation(com.itheima.anno.LogOperation)")
自定义的注解如下:target注解是使方法生效 retention是在运行时生效
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
整个案例需要完成的是记录员工的操作日志,所以在执行每一个方法的时候需要知道员工的名字,这个时候就要想什么是一直携带着这个信息的。令牌技术里的token,就记录了员工的id,所以我们可以解析令牌来获取员工的id,然后记录到日志里面。这个时候就有了ThreadLocal,这是一个线程的存储空间。
每一次请求都是一个单独的线程,所以资源互不影响,创建一个关于ThreadLocal工具类的方法,来设置和获取id,在最后执行完的时候还要销毁id,避免资源空间的浪费。
public class CurrentHolder {
private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId) {
CURRENT_LOCAL.set(employeeId);
}
public static Integer getCurrentId() {
return CURRENT_LOCAL.get();
}
public static void remove() {
CURRENT_LOCAL.remove();
}
}
这样登录校验功能和日志记录功能就顺利完成了!
题外话:
希望自己能够坚持记录和总结下学习的内容(虽然有点潦草),但希望自己每天的学习是有收获的,不能因为着急学习技术,就囫囵吞枣!希望能尽快学好,找到实习吧!