文章目录
- 登录校验与登录认证
- 一、登录认证
- 1.1 基础登录功能
- 1.2 会话技术
- 1.2.1 介绍
- 1.2.2 方案一 Cookie
- 1.2.2.1 基本介绍
- 1.2.2.2 服务端向浏览器响应Cookie
- 1.2.2.3 浏览器向服务端请求携带Cookie
- 1.2.3 方案二 Session
- 1.2.3.1 基本介绍
- 1.2.3.2 服务端向浏览器响应Session
- 1.2.3.3 浏览器向服务端请求携带Session
- 二、JWT 令牌技术 - 主流
- 2.1 基本介绍
- 2.2 JWT令牌 生成
- 2.2.1 Maven
- 2.2.2 生成JWT令牌
- 2.3 JWT令牌 校验
- 2.4 登录成功下发令牌
- 2.4.1 封装工具类
- 2.4.2 发放JWT令牌
- 三、 Filter
- 3.1 基本介绍
- 3.2 Filter 快速入门
- 3.3 执行流程
- 3.4 拦截路径
- 3.5 过滤器链
- 3.6 登录校验过滤器
- 3.6.1 实现思路
- 3.6.2 代码实现
- 四、Interceptor - 拦截器
- 4.1 入门
- 4.2 详解
- 4.2.1 拦截路径
- 4.2.2 执行流程
- 4.2.3 过滤器与拦截器的区别
- 4.3 登录校验拦截器
- 4.3.1 实现思路
- 4.3.2 代码实现
登录校验与登录认证
一、登录认证
1.1 基础登录功能
接口
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
// 用Emp对象接收用户名和密码,里面将属性封装好了
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录{}",emp);
Emp e = empService.login(emp);
return e!=null? Result.success() :Result.error("用户名或密码错误");
}
}
SQL
@Select("select * from emp where username =#{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
1.2 会话技术
1.2.1 介绍
会话:用户打开浏览器,访问web服务器资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
比如我们打开浏览器与Web服务器建立连接。首先访问login接口,再访问depts接口,最后访问emps接口。只要浏览器和服务器都没有关闭,那么这三次请求都是在一次会话中完成的。
关闭服务器,所有的会话都会关闭
关闭当前浏览器,当前会话结束
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
http请求是无状态的,下一次请求并不会携带上一次请求的数据,每一次请求都是相互独立额,也就保证了http协议它的效率是比较高的。由于是无状态的,我们也无法确定两次请求是否来自于同一个浏览器,是否来自于同一个会话。此时同一个会话的多个请求之间是没有办法共享数据的。
我们要想解决这个问题,就需要会话跟踪技术。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie 存储在客户端浏览器中
- 服务端会话跟踪技术:Session 存储在服务器当中
- 令牌技术
Cookie 与Session 是传统web开发当中所提供的的两种会话跟踪技术,而当前企业开发中最主流的是令牌技术
1.2.2 方案一 Cookie
1.2.2.1 基本介绍
存储在客户端浏览器,使用Cookie跟踪对话,就可以在浏览器第一次发起请求来请求浏览器的时候设置一个Cookie。
我们在Cookie中可以存储一些信息,比如存储用户名,用户id。服务端在给客户端响应数据的时候会自动将Cookie响应给浏览器。
浏览器接收到服务端响应的Cookie后将其存储在浏览器本地。
之后客户端每次发请求都会将本地存储的Cookie携带到服务端。我们在服务端就可以获取用户的信息,也可以判断Cookie是否存在。
如果不存在说明这个客户端之前没有访问登录接口;如果存在说明这个客户端之前已经登录完成了。
这样我们就能在同一次会话的不同请求之前来共享数据
三个自动
- 服务器自动将Cookie响应给浏览器
- 浏览器接收到响应回来的数据之后会自动的将Cookie存储在浏览器本地
- 后续请求中浏览器会自动将Cookie携带到服务端
为什么上面这个三个时自动进行的呢?
因为Cookie是http协议支持的内容,各大浏览器厂商都支持了这一标准。
在HTTP协议中,提供了响应头(setCookie)和一个请求头
HTTP相关技术文档
https://cloud.tencent.com/developer/doc/1117
服务端在给浏览器响应Cookie的时候是以哪种方式响应回去的?
直接设置了一个响应头Set-Cookie,请求头所对应的数据就是Cookie对应的值。
“name”就是Cookie的名称,“value”就是Cookie的值
此响应头返回给浏览器,浏览器会自动解析响应头
优点
-
HTTP 协议中支持的技术
缺点
-
移动端APP无法使用Cookie
-
不安全,用户可以自己禁用Cookie
-
Cookie不能跨域(协议、IP/域名、端口号,这三者有一个不同就是跨域)
1.2.2.2 服务端向浏览器响应Cookie
//设置Cookie - 服务器要给浏览器响应数据
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
效果图
自动保存在下图位置
1.2.2.3 浏览器向服务端请求携带Cookie
服务端解析从浏览器向服务端请求携带的Cookie
//获取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();
}
1.2.3 方案二 Session
1.2.3.1 基本介绍
服务器端会话跟踪技术,存储在服务器端,底层借助Cookie实现。
浏览器在第一次请求服务器时,我们可以直接在服务器当中获取到会话对象Session(第一次请求会话对象Session是不存在的,服务器会自动创建一个会话对象),每一个会话对象都有一个id。
之后服务端再给浏览器响应数据的时候,会将Session的id通过Cookie响应给浏览器,其实就是在响应头重增加了一个Set-Cookie。
在这里Cookie的名字是固定的,就是JSESSIONID,代表的就是服务端会话对象Session的id。
浏览器接收到数据之后会自动将这个Cookie存储在浏览器本地,之后再请求服务端时都会将Cookie数据获取出来并且携带到服务端。
浏览器拿到JSESSIONID也就是Session的id后,会从众多会话对象Session中找到当前请求对应的绘画对象Session。
找到对应Session之后,就可以实现在同一次会话的多次请求之间来共享数据
优点
- 存储在服务端,安全
缺点
- 服务器集群环境下无法直接使用Session
- Cookie的缺点(底层Cookie实现)
1.2.3.2 服务端向浏览器响应Session
// 往HTTPSession中存储值
// 服务器会判断当前这次请求对应的会话对象Session是否存在,
// 如果不存在会新创建一个Session,如果存在会获取当前这一次请求对应的Session
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());//HttpSession-s1: 1750219908
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
Set-Cookie
Application
1.2.3.3 浏览器向服务端请求携带Session
// 这个地方我们可以声明HttpSession对象,也可以使用HttpServletRequest对象
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();//拿到当前这次请求对应的会话对象
log.info("HttpSession-s2: {}", session.hashCode()); //HttpSession-s2: 1750219908
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser); //loginUser: tom
return Result.success(loginUser);
}
注意看上面的几张图,页面中完全没有涉及到“loginUser”的存储,那为什么控制台还能输出“loginUser: tom”?
Session存储在服务器端,每次请求浏览器都会写到Cookie,并且有JSESSIONID,服务端获取到之后就可以解析,确定是哪个Session对象
而且观察控制台SessionID,确实是同一个
二、JWT 令牌技术 - 主流
用户身份表示
存储在客户端。不用担心安全问题,浏览器携带令牌请求时,服务端会进行校验。
无效的,返回错误结果;有效的,访问对应权限。
优点
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器端存储压力
缺点
- 需要自己实现
2.1 基本介绍
官方网站:JSON Web Tokens - jwt.io
简洁、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息都是可靠的。
组成:
-
第一部分:Header(头),记录令牌类型、签名算法等。
例如:[“alg”:“HS256”,“type”:"WT”)
-
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。
例如:“id”."1"“username”."Tom”
-
第三部分: Signature(签名),防止Token被篡改、确保安全性。
将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
第三部分字符的由来不是Base64编码方式生成的,而是前面指定的签名算法
JSON数据格式是怎么变成一些字符的呢?
通过Base64编码方式
应用场景
登录认证。
① 用户登录成功后,服务端生成一个JWT令牌,并传输给前端。
②前端拿到JWT令牌之后会将其存储起来,之后前端的每一次请求都会将JWT令牌携带到服务端,服务端会对请求进行统一拦截,拦截之后先判断有没有把这个令牌带过来。
如果没有令牌就拒绝访问;若有令牌但是无效仍然拒绝访问;若有效则直接放心,并处理对应请求
2.2 JWT令牌 生成
2.2.1 Maven
无论生成和校验都需要下面的工具类
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.2.2 生成JWT令牌
签名算法具体有哪些,可以查看官网
/**
* 生成JWT
*/
@Test
public void testGenJwt() {
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "tom");
// 链式编程 - Jwt令牌在生成的时候所需要设置的一些参数
String jwt = Jwts.builder()
// 存储在第一个部分 参数一 数字签名算法 参数二 秘钥
.signWith(SignatureAlgorithm.HS256, "zhangjingqi")
// JWT令牌所存储的内容(自定义数据,存储在第二个部分,原始自定义数据是JSON格式)
// 可以是Map集合,也可以是Claims对象
.setClaims(map)
// 设置令牌有效期 - 一个小时后过期, 因为是毫秒,3600*1000代表一个小时
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))
// 调用compact会有一个String返回值,就是JWT令牌
.compact();
System.out.println(jwt);//eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NDIxNjUzNX0.s33vECyehznKMrbDqD1Pdx-DrHWkscdyNeWmLnY-ArU
}
将生成的JWT放在官网进行解码,和我们的数据一模一样
2.3 JWT令牌 校验
Jwt令牌生成后,不论我们改哪部分的字符,在解析的时候都会报错,所以Jwt令牌是很安全的。
令牌时间过期之后也不能访问
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
// 指定签名秘钥
.setSigningKey("zhangjingqi")
// 解析JTW令牌
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NDIxNjUzNX0.s33vECyehznKMrbDqD1Pdx-DrHWkscdyNeWmLnY-ArU")
// 拿到了我们自定义的内容,也就是Jwt令牌的第二个部分
.getBody();
System.out.println(claims); //{name=tom, id=1, exp=1684216535}
}
注意事项
-
JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的(编码和解析时秘钥必须相同)
-
如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效,令牌非法
2.4 登录成功下发令牌
- 令牌生成: 登录成功后,生成JWT令牌,返回给前端
- 令牌校验,在请求到达服务端后,对令牌进行统一拦截、校验
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为token,值为登录时下发的JWT令牌
2.4.1 封装工具类
将上面编码和解码汇编成一个工具类供我们使用
public class JwtUtils {
private static String signKey = "zhangjingqi";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @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;
}
}
2.4.2 发放JWT令牌
// 用Emp对象接收用户名和密码,里面将属性封装好了
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录{}",emp);
Emp e = empService.login(emp);
// 登陆成功,生成令牌并下发令牌
if(e!=null){
Map<String, Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
// 生成令牌,员工信息已经在里面了
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
// 登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
网页联调
接收编码后,前端人员将其存储到Local Storage中
这里面的Key “tlias_token”是前端自己定义的
随便抓取一个请求,看此时是否携带Jwt令牌
显然是存在的。这个地方的实现是前端完成的,后端不需要管
三、 Filter
Filter过滤器,是javaWeb三大组件(Servlet、Filter、Listener)之一
3.1 基本介绍
概念:
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
① 想要访问服务器中的某些资源,必须先经过Filter过滤器。
② 再此处进行一些操作,完成之后进行放行,访问对应的资源
③ 资源访问完毕,最后再回到过滤器,然后再给浏览器响应对应的数据
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
如果没有过滤器,我们需要在每一个接口中编写登录校验逻辑。
如果登录了,放行去访问对应的信息,如果没有登录,直接在Filter中返回错误信息,不再访问后面的请求
3.2 Filter 快速入门
-
定义Filter:定义一个类,实现Filter接口,并重写其所有方法
但是开发中我们一般只实现doFilter这个方法,其他两个方式使用默认实现即可
// 定义Filter,并重写三个方法
// 在web服务器启动的时候,会自动创建Filter过滤器对象
//不配置的话过滤器不会生效,urlPatterns表示拦截什么样的请求,/*代表拦截所有请求
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
// 初始化方法,过滤器创建完毕之后会自动调用init方法,只会调用一次(只会在创建时调用一次)
// 一般在这里完成一些资源及环境的准备操作
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("init 初始化方法执行了");
}
// 每一次拦截到请求都会调用的方法,最为重要的方法,是会被调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
System.out.println("拦截方法执行,拦截到了请求 ...");
// 往下放行去访问对应的资源,如果不放行,页面获取不到对应数据(接口资源是获取不到的)
chain.doFilter(request, response);
}
// 销毁方法。服务区关闭时调用,只调用一次
// 一般在这里完成资源的释放和环境的清理操作
public void destroy() {
// Filter.super.destroy();
System.out.println("destroy 销毁方法执行了 ");
}
}
- 配置Filter:Filter类上添加@WebFilter注解,配置拦截资源路径。引导类上添加@ServletComponentScan注解开启Servlet组件支持
//Filter是javaweb三大组件之一,不是Spring提供的,如果想要使用三大组件,需要添加这个注解
@ServletComponentScan
@SpringBootApplication
public class SpringbootWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebApplication.class, args);
}
}
启动程序,观察控制台
init方法只会执行这一次
随意访问几个接口,观察控制台
3.3 执行流程
当我们的过滤器拦截到请求之后我们需要完成一个非常重要的操作,那就是放行。
chain.doFilter(request, response);
在过滤器放行之前我们可以执行一段逻辑。
”放行“就是让其去访问对应的web资源,访问完过滤器之后还会回到过滤器当中,执行“放行”语句后面的代码,执行完毕之后再给浏览器响应数据
chain.doFilter(request, response);
System.out.println("执行放行后逻辑 ...");
- 放行后访问对应资源,资源访问完成后还会回到Filter中吗?
会
- 如果回到Filter中,是重新执行还是执行放行后的逻辑?
执行放行后的逻辑
3.4 拦截路径
在快速入门中配置的是 /*,代表拦截所有请求
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {}
拦截路径 | urlPatterns | 含义 |
---|---|---|
拦截具体路径 | /login | 只访问/login路径时才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
3.5 过滤器链
- 一个web应用中,可以配置多个过滤器,多个过滤器形成了一个过滤器链。
过滤器链中的过滤器会一个一个的执行,第一个放行之后会执行第二个,依次推,最后一个过滤器执行完后会访问对应请求。
过滤器链中最后一个过滤器放行的话,会放行到web资源当中来访问web资源
访问完资源后,是倒着进行的,先执行最后一个过滤器,再倒数第二个…
- 注解配置Filter,优先级是按照过滤器类型(字符串)的自然排序
创建新的过滤器
@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Abc拦截方法执行,拦截到了请求 ...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Abc执行放行后逻辑 ...");
}
}
控制台输出:(我们之前创建了一个DemoFilter过滤器)
Abc拦截方法执行,拦截到了请求 …
Demo拦截方法执行,拦截到了请求 …Demo执行放行后逻辑 …
Abc执行放行后逻辑 …
3.6 登录校验过滤器
备注说明:
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为token,值为登录时下发的JWT令牌。
如果检测到用户未登录,则会返回如下固定错误信息。
- 所有的请求,拦截到之后,都需要校验令牌吗?
登录请求例外
- 拦截到请求后,什么情况下可以放行,执行业务操作?
JWT令牌存在且令牌校验通过(合法),否则都会返回未登录错误结果
3.6.1 实现思路
- 获取请求url
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行
- 获取请求头中的令牌(token)
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析token,如果解析失败,返回错误结果(未登录)
- 放行
3.6.2 代码实现
在过滤器当中为什么要把ServletRequest类型强转成HttpServletRequest类型?
在Java中,ServletRequest是一个接口,它是由Servlet容器提供的。HttpServletRequest接口则是ServletRequest接口的子接口,它包含了一些用于HTTP协议的方法和属性。在Java Web应用程序中,Servlet容器实现了ServletRequest和HttpServletRequest接口,并使用HttpServletRequest实现了HTTP协议相关的逻辑。
在开发Web应用程序时,Servlet容器将在每个客户端请求到达时创建一个ServletRequest对象并将其传递给请求处理器。由于具体的实现是由Servlet容器提供的并且通常是HttpServletRequest,因此在编写Servlet处理器时,我们通常将ServletRequest对象强制转换成HttpServletRequest对象,以便能够调用提供的HTTP协议相关方法。
因此,在过滤器中,如果我们需要使用HttpServletRequest接口中特定的HTTP协议相关方法,我们需要将ServletRequest对象强制转换成HttpServletRequest对象。这样我们才能够在处理ServletRequest对象时,使用HttpServletRequest中更多的方法和属性。
注意!!!将刚刚测试的Filter注释掉,只留这一个过滤器!!!!!!!!
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// ServletRequest、ServletResponse是父类,
// 请求对象与响应对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// TODO 1.获取请求url
String requestURL = request.getRequestURL().toString(); //不toString就是StringBuffer类型
log.info("请求的url:{}", requestURL);
// TODO 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (requestURL.contains("/login")){
log.info("登录操作,放行...");
filterChain.doFilter(request, response);
// 登录操作不需要执行下面的逻辑,直接结束此过滤器即可
return;
}
// TODO 3.获取请求头中的令牌(token)
String token = request.getHeader("token");
// TODO 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(token)) { //spring当中的工具类
// 说明字符串为null,返回错误结果(未登录)
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
// 手动转JSON
String errorJson = JSON.toJSONString(error);
// response.getWriter()获取输出流,write()直接将数据响应给浏览器
response.getWriter().write(errorJson);
return;
}
// TODO 5.解析token,如果解析失败,返回错误结果(未登录)
// 说明存在令牌,校验
try{
Claims claims = JwtUtils.parseJWT(token);
}catch (Exception e){ // 出现异常代表着解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转JSON
String errorJson = JSON.toJSONString(error);
// response.getWriter()获取输出流,write()直接将数据响应给浏览器
response.getWriter().write(errorJson);
return;
}
// 到这里说明令牌解析成功,直接放行
// TODO 6.放行
log.info("令牌合法,放行");
filterChain.doFilter(request, response);
}
}
很完美
四、Interceptor - 拦截器
之前做的笔记: Springboot——拦截器_springboot 拦截器_
概念:是一种动态拦截方法调用调用机制,类似过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行(拦截请求的)
作用: 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
4.1 入门
在测试之前可以把过滤器关掉,否则一直验证token比较麻烦
- 定义拦截器,实现HandlerInterceptor接口,并且重写其所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
// 目标资源方法执行前执行(Controller方法执行之前), true:放行, false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle......");
// return HandlerInterceptor.super.preHandle(request, response, handler);
return true;
}
// 目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle.....");
// HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
// 视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
System.out.println("afterCompletion......");
}
}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**");
}
}
4.2 详解
4.2.1 拦截路径
addPathPatterns 指定拦截哪些路径
excludePathPatterns 执行不拦截哪些不经
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配/depts/1 |
/** | 任意级路径 | 能匹配/depts/1,/depts,/depts/1/2 |
/depts/* | /depts下一级路径 | 能匹配/depts/1,不能匹配/depts,/depts/1/2,/depts |
/depts/** | /depts下的任意路径 | 能匹配/depts,/depts/1,/depts,/depts/1/2 |
4.2.2 执行流程
① 当浏览器向Web服务器发送请求时,我们所设定的过滤器会拦截到这一请求。
② 过滤器先执行放行前逻辑,在此处决定是否放行
③ 放行之后进入到Spring环境中,进入DispatcherServlet
在请求响应时说道,tomcat服务器并不识别我们所编写的controller程序,但是他是识别Servlet程序的,因为tomcat是一个Servlet程序。
而在SpringWeb当中提供了一个非常核心的Servlet,我们叫做DispatcherServlet前端核心控制器,所以请求会先进入到DispatcherServlet,请求由DispatcherServlet再传给Controller
如果设置了拦截器的话,DispatcherServlet在传给Controller之前需要先被拦截器拦截住
④ 拦截器拦截到,先进行preHandle,决定是否放行,如果放行便访问Controller层方法
⑤Controller层方法完成之后,再执行postHandler方法已经afterCompletion方法
⑥返回给DispatcherServlet
⑦最终执行放行后逻辑
⑧最终给浏览器响应数据
4.2.3 过滤器与拦截器的区别
-
接口规范不同:过滤器实现Filter接口,而拦截器需要实现HandlerInterceptor接口
-
拦截范围不同:过滤器Filter会拦截所有资源,而Interceptor只会拦截Spring环境中的资源,过滤器拦截范围更大
4.3 登录校验拦截器
4.3.1 实现思路
与过滤器思路一模一样
- 获取请求url
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行
- 获取请求头中的令牌(token)
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析token,如果解析失败,返回错误结果(未登录)
- 放行
4.3.2 代码实现
测试之前记得吧过滤器注解注释掉
// 目标资源方法执行前执行(Controller方法执行之前), true:放行, false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 1.获取请求url
String requestURL = request.getRequestURL().toString(); //不toString就是StringBuffer类型
log.info("请求的url:{}", requestURL);
// TODO 2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (requestURL.contains("/login")) {
log.info("登录操作,放行...");
return true;
}
// TODO 3.获取请求头中的令牌(token)
String token = request.getHeader("token");
// TODO 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if (!StringUtils.hasLength(token)) { //spring当中的工具类
// 说明字符串为null,返回错误结果(未登录)
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
// 手动转JSON
String errorJson = JSON.toJSONString(error);
// response.getWriter()获取输出流,write()直接将数据响应给浏览器
response.getWriter().write(errorJson);
return false;
}
// TODO 5.解析token,如果解析失败,返回错误结果(未登录)
// 说明存在令牌,校验
try {
Claims claims = JwtUtils.parseJWT(token);
} catch (Exception e) { // 出现异常代表着解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
// 手动转JSON
String errorJson = JSON.toJSONString(error);
// response.getWriter()获取输出流,write()直接将数据响应给浏览器
response.getWriter().write(errorJson);
return false;
}
// 到这里说明令牌解析成功,直接放行
// TODO 6.放行
log.info("令牌合法,放行");
// return HandlerInterceptor.super.preHandle(request, response, handler);
return true;
}