JAVA WEB案例-登录校验-日志记录

一 前言

在现代社会中,随着互联网的快速发展,WEB应用的安全性问题变得越来越突出。作为一名程序员,我们不仅要注重WEB应用的功能实现,还需要重视安全性问题。在实际开发中,登录校验是非常重要的安全措施,能够有效地保护用户数据和系统信息免受攻击。本文将从薛慕昭的角度出发,针对WEB案例中的登录校验进行探讨,帮助jym更好地理解和实践这个关键安全功能。

二 登录校验

1 理论

markdown 复制代码
* 我们的系统目前在不经过登录的情况下,直接输入员工页面地址就可以访问,这是非常不安全的。
* 正确的流程应该是:当访问请求到达服务器后,服务器要校验当前用户是否已经登录过
	如果登录过,就放行请求
	如果未登录过,就禁止请求访问
	
* 那如何知道用户是否已经登录过呢?这就需要在用户登录成功后,由服务器为其颁发一个token(身份标识)
	然后后面用户每次发送请求,都会携带着这个token
	而作为系统会对每次的请求进行拦截,校验token的合法性即可

2 JWT

介绍

全称:JSON Web Token (jwt.io/),用于对应用程序上的...

本质上就是一个经过加密处理与校验处理的字符串,它由三部分组成:

  • 头信息(Header):记录令牌类型和签名算法,例如:{"alg": "HS256","typ": "JWT"}
  • 有效载荷(Payload):记录一些自定义能够区分身份的非敏感信息,例如:{"id": "1","username": "tom"}
  • 签名(Signature):用于保证Token在传输过程中不被篡改,它是header、payload,加入指定算法计算得来的

使用流程

代码测试

① 在pom.xml中引入依赖

xml 复制代码
<!--Token生成与解析-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

② 生成并校验token

java 复制代码
package com.itheima.test;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTest {

    //生成token
    @Test
    public void genJwt() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 1);
        claims.put("username", "Tom");

        String jwt = Jwts.builder().
                setClaims(claims) //自定义内容(载荷)
                .signWith(SignatureAlgorithm.HS256, "itheima") //签名算法和盐
                .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)) //有效期
                .compact();
        System.out.println(jwt);
    }

    //校验token
    @Test
    public void checkJwt() {
        Claims claims = Jwts.parser()
                .setSigningKey("itheima")//盐
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjc3MzU3MjE0LCJ1c2VybmFtZSI6IlRvbSJ9.RBtRZGHUefLElDWWIlQRoy0_Dl71sZysPP61vVa46oo")//上一步得到的值
                .getBody();
        System.out.println(claims);
    }
}

功能改进

修改目前的登录功能,当登录成功后,创建token并返回给客户端

3 过滤器入门

介绍

当用户访问服务器资源时,过滤器将请求拦截下来,完成一些通用的功能,比如:登录校验、统一编码处理、敏感字符处理等

入门案例

实现一个服务器资源访问,然后使用过滤器拦截住请求,打印下日志。

markdown 复制代码
1. 定义Filter:定义一个类实现 Filter 接口,并重写其所有方法。
2. 配置Filter:Filter类上加 @WebFilter 注解,配置拦截资源的路径。
3. 引导类上加 @ServletComponentScan 开启Servlet组件支持

① 导入提料中提供的测试项目

② 创建LogFilter类

③ 开启Filter支持

执行流程

一个Filter的访问流程

markdown 复制代码
1. 客户端向服务器发起访问资源的请求
2. Filter将请求拦截住,开始处理访问资源之前的逻辑
3. Filter决定是否要放行访问请求,如果放行,请求继续向后运行
4. 请求访问到相关资源,然后服务器给出响应
5. Filter将响应拦截住,开始处理访问资源之后的逻辑
6. 服务器将响应返回给浏览器

拦截路径

Filter的拦截路径支持下面三种匹配方式

markdown 复制代码
1. 精确匹配:直接匹配到某个资源上,例如 `/a`    `/a/b`

2. 路径匹配:匹配某个目录,要求以/开头,以`*`结尾,例如 `/a/*`   `/*`

3. 后缀匹配:根据后缀匹配,要求以`*.`开头,例如 `*.html   *.do`

过滤器链

程序有时需要对同一个资源进行多重过滤,这就可以配置多个过滤器,称为过滤器链。

只有过滤器链中的所有的过滤器都对请求放行,请求才能访问到目标资源。

过滤器的执行顺序是按照过滤器类名(字符串)的自然排序

==先进后出,有头有尾==

4 过滤器实现访问校验

下面使用过滤器是请求的访问校验,在实现之前先考虑两个问题

  1. 所有的请求,拦截到了之后,都需要校验令牌吗?
  2. 拦截到请求后,在满足什么条件下才可以放行?

思路分析

代码实现

① 创建Filter

创建com.itheima.filter.LoginCheckFilter编写过滤逻辑

java 复制代码
package com.itheima.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.util.JwtUtil;
import com.itheima.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter("/*")
@Slf4j
public class LoginCheckFilter 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. 获取请求url
        String uri = request.getRequestURI();
        log.info("请求路径{}", uri);

        //3. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if (uri.equals("/login")) {
            filterChain.doFilter(servletRequest, servletResponse);//放行请求
            return;//结束判断
        }

        //4. 获取请求头中的令牌(token)
        String token = request.getHeader("token");

        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtil.parseJWT(token);
        }catch (Exception e){
            log.info("token错误");
            //返回错误消息
            String json = new ObjectMapper().writeValueAsString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(json);
            return;//结束判断
        }

        //6.放行
        filterChain.doFilter(request, response);
    }
}

② 开启Filter支持

在启动类上添加@ServletComponentScan注解

5 拦截器入门

介绍

拦截器是Spring提供的一种技术,它的功能似于过滤器,它会在进入controller之前,离开controller之后以及响应离开服务时进行拦截。

入门案例

① 开发拦截器

作用:要对拦截的资源做什么

语法:实现HandlerInterceptor接口,重写3个方法

② 配置拦截器

作用:确定你要拦截那些资源

语法:在配置类中添加拦截路径的配置

拦截路径

拦截器的路径写法相对简单,其实只有两个:*表示一层路径 **表示多层路径

路径 解释 备注
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

拦截器链

多个拦截器也可以同时使用,一条拦截器链,他们的顺序是有.order()方法控制

6 拦截器实现访问校验

注释掉过滤器代码

xml 复制代码
<!--添加依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.76</version>
</dependency>

① 创建Interceptor

创建com.itheima.interceptor.LoginCheckInterceptor编写过滤逻辑

java 复制代码
package com.itheima.interceptor;

import com.alibaba.fastjson.JSON;
import com.itheima.util.JwtUtil;
import com.itheima.vo.Result;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 登录拦截器
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

    // 登录拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.取出token
        String token = request.getHeader("token");
        // 2.判断token是否正确
        try {
            Claims claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
            // e.printStackTrace();
            log.error("令牌失效");
            // 返回错误信息
            // String json = new ObjectMapper().writeValueAsString(Result.error("NOT_LOGIN"));
            String json = JSON.toJSONString(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(json);
            // 拦截
            return false;
        }
        // 3.放行
        return true;
    }
}

② 配置Interceptor

7 过滤器VS拦截器

过滤器和拦截器实现的功能基本相似,不同点在于:

  1. 技术范围不同:过滤器需要JavaWeb技术,而拦截器属于Spring提供的技术
  2. 拦截范围不同:过滤器会拦截所有的资源,而拦截器只会拦截Spring环境中的资源
  3. 如果项目中同时出现了过滤器和拦截器,它们的执行位置如下

三 日志记录

本小节我们要实现的功能是,要记录所有到controller中方法的运行日志保存到日志表中

日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法作用、方法运行时参数、返回值、方法执行时长

1 准备工作

创建数据表

sql 复制代码
-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
	class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
	method_desc varchar(100) comment '方法用途',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
	operate_user int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

创建日志类

java 复制代码
package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private String className; //操作类名
    private String methodName; //操作方法名
	private String methodDesc; //方法用途
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
	private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private Long costTime; //操作耗时
}

创建日志的Mapper

java 复制代码
package com.itheima.mapper;

import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name,method_desc, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName},#{methodDesc}, #{methodParams}, #{returnValue}, #{costTime});")
    public void insert(OperateLog log);

}

2 制作切面

添加aop的启动器

xml 复制代码
<!--aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

自定义注解

标识切点

创建切面

java 复制代码
package com.itheima.aspect;

import com.itheima.anno.LogAnno;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

// 日志切面类
@Aspect
@Component
public class LogAspect {

    // 设置切点表达式
    @Pointcut("@annotation(com.itheima.anno.LogAnno)")
    public void pt() {
    }

    @Autowired
    private OperateLogMapper operateLogMapper;

    // 环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) {
        // 开始时间
        long start = System.currentTimeMillis();

        OperateLog log = new OperateLog();
        // 记录类名
        log.setClassName(pjp.getTarget().getClass().getName());
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 记录方法名
        log.setMethodName(methodSignature.getMethod().getName());
        // 记录方法描述
        log.setMethodDesc(methodSignature.getMethod().getAnnotation(LogAnno.class).methodDesc());
        // 记录方法参数
        log.setMethodParams(Arrays.toString(pjp.getArgs()));
        // 记录调用时间
        log.setOperateTime(LocalDateTime.now());
        // 记录操作人
        log.setOperateUser(1); // 暂时写死1

        Object obj = null;
        try {
            // 执行切点原有功能
            obj = pjp.proceed();
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        } finally {
            // 结束时间
            long end = System.currentTimeMillis();
            // 记录方法返回值
            log.setReturnValue(obj.toString());
            // 记录耗时
            log.setCostTime(end-start);
            // 保存到数据库
            operateLogMapper.insert(log);
        }
        return obj;
    }
}

3 用户信息共享

目前代码问题

目前的代码中是这样设置操作用户Id的operateLog.setOperateUser(1);

那么怎样才能在切面中获取当前登录的用户的信息呢?

ThreadLocal

线程局部变量,该变量对其他线程而言是隔离的;在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

ThreadLocal的三个方法:

  • set(T value) :设置当前线程绑定的变量
  • get():获取当前线程绑定的变量
  • remove() :移除当前线程绑定的变量

代码实现

① EmpContext

② 修改LoginCheckInterceptor

③ 修改LogAspect代码

四 总结

markdown 复制代码
1. 系统登录
	设计LoginDto用于前端用户名和密码
	登录成功需要制作一个令牌
	
2. JWT三部分组成
	头部、载荷(不能敏感信息)、签名
	JwtUtil
		制作令牌(登录)
		解析令牌(过滤、拦截)
		
3. 过滤器
	JavaWeb技术
	自定义类实现Filter接口
		doFilter(){
			controller执行前
			放行
			controller执行后
		}
	实现了登录校验
	
4. 拦截器
	SpringMVC技术
	自定义类实现HandlerInterceptor接口
		preHandler(){
			controller执行前
			放行
		}
		postHandler(){
			controller执行后
		}
		afterComplation(){
			服务器返回前
		}
	实现了登录校验
	

5. 日志记录
	自定义注解+切面类
	ThreadLocal(工具类操作线程内的map集合,实现数据)
相关推荐
古城小栈4 小时前
Spring Boot + 边缘 GenAI:智能座舱应用开发实战
java·spring boot·后端
twl4 小时前
注意力机制在Code Agent的应用
前端
j_hy4 小时前
OOP组件及事件处理(一)
java·开发语言
无名之辈J5 小时前
IDEA插件
java
开心就好20255 小时前
使用 Ipa Guard 应对 App Store 4.3 风险的一些实践
后端
涔溪5 小时前
如何使用 CSS Grid 实现响应式布局?
前端·css
想用offer打牌5 小时前
一站式了解数据库三大范式(库表设计基础)
数据库·后端·面试
金牌归来发现妻女流落街头5 小时前
【阻塞队列的等待唤醒机制】
java·开发语言·阻塞队列
骚戴5 小时前
深入解析:Gemini 3.0 Pro 的 SSE 流式响应与跨区域延迟优化实践
java·人工智能·python·大模型·llm
毕设源码-朱学姐5 小时前
【开题答辩全过程】以 基于Java技术的羽毛球积分赛管理系统的设计与实现 为例,包含答辩的问题和答案
java·开发语言