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集合,实现数据)
相关推荐
激流丶3 分钟前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue7 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
EricWang135818 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning18 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
让学习成为一种生活方式24 分钟前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
web行路人28 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
晨曦_子画29 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法