一 前言
在现代社会中,随着互联网的快速发展,WEB应用的安全性问题变得越来越突出。作为一名程序员,我们不仅要注重WEB应用的功能实现,还需要重视安全性问题。在实际开发中,登录校验是非常重要的安全措施,能够有效地保护用户数据和系统信息免受攻击。本文将从薛慕昭的角度出发,针对WEB案例中的登录校验进行探讨,帮助jym更好地理解和实践这个关键安全功能。
二 登录校验
1 理论
* 我们的系统目前在不经过登录的情况下,直接输入员工页面地址就可以访问,这是非常不安全的。
* 正确的流程应该是:当访问请求到达服务器后,服务器要校验当前用户是否已经登录过
如果登录过,就放行请求
如果未登录过,就禁止请求访问
* 那如何知道用户是否已经登录过呢?这就需要在用户登录成功后,由服务器为其颁发一个token(身份标识)
然后后面用户每次发送请求,都会携带着这个token
而作为系统会对每次的请求进行拦截,校验token的合法性即可
2 JWT
介绍
全称:JSON Web Token (https://jwt.io/),用于对应用程序上的用户进行身份标记
本质上就是一个经过加密处理与校验处理的字符串,它由三部分组成:
- 头信息(Header):记录令牌类型和签名算法,例如:{“alg”: “HS256”,“typ”: “JWT”}
- 有效载荷(Payload):记录一些自定义能够区分身份的非敏感信息,例如:{“id”: “1”,“username”: “tom”}
- 签名(Signature):用于保证Token在传输过程中不被篡改,它是header、payload,加入指定算法计算得来的
使用流程
代码测试
① 在pom.xml中引入依赖
<!--Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
② 生成并校验token
package ***.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)) //有效期
.***pact();
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 过滤器入门
介绍
当用户访问服务器资源时,过滤器将请求拦截下来,完成一些通用的功能,比如:登录校验、统一编码处理、敏感字符处理等
入门案例
实现一个服务器资源访问,然后使用过滤器拦截住请求,打印下日志。
1. 定义Filter:定义一个类实现 Filter 接口,并重写其所有方法。 2. 配置Filter:Filter类上加 @WebFilter 注解,配置拦截资源的路径。 3. 引导类上加 @Servlet***ponentScan 开启Servlet组件支持
① 导入提料中提供的测试项目
② 创建LogFilter类
③ 开启Filter支持
执行流程
一个Filter的访问流程
1. 客户端向服务器发起访问资源的请求
2. Filter将请求拦截住,开始处理访问资源之前的逻辑
3. Filter决定是否要放行访问请求,如果放行,请求继续向后运行
4. 请求访问到相关资源,然后服务器给出响应
5. Filter将响应拦截住,开始处理访问资源之后的逻辑
6. 服务器将响应返回给浏览器
拦截路径
Filter的拦截路径支持下面三种匹配方式
1. 精确匹配:直接匹配到某个资源上,例如 `/a` `/a/b` 2. 路径匹配:匹配某个目录,要求以/开头,以`*`结尾,例如 `/a/*` `/*` 3. 后缀匹配:根据后缀匹配,要求以`*.`开头,例如 `*.html *.do`
过滤器链
程序有时需要对同一个资源进行多重过滤,这就可以配置多个过滤器,称为过滤器链。
只有过滤器链中的所有的过滤器都对请求放行,请求才能访问到目标资源。
过滤器的执行顺序是
按照过滤器类名(字符串)的自然排序
==先进后出,有头有尾==
4 过滤器实现访问校验
下面使用过滤器是请求的访问校验,在实现之前先考虑两个问题
- 所有的请求,拦截到了之后,都需要校验令牌吗?
- 拦截到请求后,在满足什么条件下才可以放行?
思路分析
代码实现
① 创建Filter
创建
***.itheima.filter.LoginCheckFilter
编写过滤逻辑
package ***.itheima.filter;
import ***.fasterxml.jackson.databind.ObjectMapper;
import ***.itheima.util.JwtUtil;
import ***.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支持
在启动类上添加
@Servlet***ponentScan
注解
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 拦截器实现访问校验
注释掉过滤器代码
<!--添加依赖-->
<dependency>
<groupId>***.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
① 创建Interceptor
创建
***.itheima.interceptor.LoginCheckInterceptor
编写过滤逻辑
package ***.itheima.interceptor;
import ***.alibaba.fastjson.JSON;
import ***.itheima.util.JwtUtil;
import ***.itheima.vo.Result;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.***ponent;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 登录拦截器
@***ponent
@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拦截器
过滤器和拦截器实现的功能基本相似,不同点在于:
- 技术范围不同:过滤器需要JavaWeb技术,而拦截器属于Spring提供的技术
- 拦截范围不同:过滤器会拦截所有的资源,而拦截器只会拦截Spring环境中的资源
- 如果项目中同时出现了过滤器和拦截器,它们的执行位置如下
三 日志记录
本小节我们要实现的功能是,要记录所有到controller中方法的运行日志保存到日志表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法作用、方法运行时参数、返回值、方法执行时长
1 准备工作
创建数据表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment ***ment 'ID',
class_name varchar(100) ***ment '操作的类名',
method_name varchar(100) ***ment '操作的方法名',
method_desc varchar(100) ***ment '方法用途',
method_params varchar(1000) ***ment '方法参数',
return_value varchar(2000) ***ment '返回值',
operate_user int unsigned ***ment '操作人ID',
operate_time datetime ***ment '操作时间',
cost_time bigint ***ment '方法执行耗时, 单位:ms'
) ***ment '操作日志表';
创建日志类
package ***.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
package ***.itheima.mapper;
import ***.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的启动器
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
自定义注解
标识切点
创建切面
package ***.itheima.aspect;
import ***.itheima.anno.LogAnno;
import ***.itheima.mapper.OperateLogMapper;
import ***.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.***ponent;
import java.time.LocalDateTime;
import java.util.Arrays;
// 日志切面类
@Aspect
@***ponent
public class LogAspect {
// 设置切点表达式
@Pointcut("@annotation(***.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代码
四 总结
1. 系统登录
设计LoginDto用于前端用户名和密码
登录成功需要制作一个令牌
2. JWT三部分组成
头部、载荷(不能敏感信息)、签名
JwtUtil
制作令牌(登录)
解析令牌(过滤、拦截)
3. 过滤器
JavaWeb技术
自定义类实现Filter接口
doFilter(){
controller执行前
放行
controller执行后
}
实现了登录校验
4. 拦截器
SpringMVC技术
自定义类实现HandlerInterceptor接口
preHandler(){
controller执行前
放行
}
postHandler(){
controller执行后
}
after***plation(){
服务器返回前
}
实现了登录校验
5. 日志记录
自定义注解+切面类
ThreadLocal(工具类操作线程内的map集合,实现数据)