登录功能实现¶
实际上,登录在逻辑上应该是"查询"操作,但是我们通常用post方法. 我们登录的结果(后端给前端返回的)往往有以下信息,我们可以封装成一个类.
/**
* 封装登录的结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id;
private String username;
private String name;
private String token;
}
controller层
/*
登录Controller
*/
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
/**
* 登录方法
* @return 登录结果
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("登录请求,参数:{}", emp);
LoginInfo info = empService.login(emp);
if (info == null) {
return Result.error("用户名或密码错误");
}
log.info("登录成功,用户信息:{}", info);
return Result.success(info);
}
}
Service层
@Override
public LoginInfo login(Emp emp) {
//1.调用mapper查询员工信息
Emp e = empMapper.selectByUsernameAndPassword(emp);
//2.判断员工是否存在
if(e!=null){
log.info("登录成功,员工信息: {}", e);
return new LoginInfo(e.getId(),e.getUsername(),e.getName(),"");
}
return null;
}
mapper层
/*
* 根据用户名和密码查询员工信息
*/
@Select("select id, username,name from emp where username = #{username} and password = #{password}")
Emp selectByUsernameAndPassword(Emp emp);
登录校验¶
本质上,我们为了解决这个问题,就需要在用户访问后台主页面的时候进行登录校验,必须处于登录状态才能操作.
思路¶
后端存储一个登录标记,记录当前时刻有哪些用户处于登录状态.
在用户访问后端任何一个Controller服务之前,要先把请求发送给一个统一拦截的网关.网关会检验每个请求,是否有对应的登录标记.如果有,那么就放行.如果没有,那么就直接给前端返回信息要求重新登录.
- 登录标记可以通过"会话技术"来解决.
- 统一拦截可以通过过滤器Filter 或 拦截器Interceptor 解决.
会话技术¶
定义¶
用户打开浏览器,访问web服务器的资源,会话就建立了.有一方断开连接,会话就结束了.在一次会话中间,可以包括很多次的请求和相应.
会话跟踪¶
一种用来让服务器识别多次请求是否属于同一个浏览器的技术,这是用来在同一次会话的多次请求之间共享数据用的. 会话跟踪很重要.比如,如果需要在每次登录时使用验证码,这个验证码是需要后端传递给前端的.后端需要记住这次给前端传递了些什么,因为待会用户点击"登录"时会把这个验证码传给后端,后端需要把传来的验证码和之前记住的验证码进行比对.
解决方案¶
客户端会话跟踪技术:Cookie¶
服务端会话跟踪技术:Session¶
令牌技术¶
Cookie¶
三个自动:
比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
演示¶
访问/c1直接就能让浏览器得到这个cookie.
访问/c2就会把cookie发到服务器.
可以查看本地存的cookie:
前三个都是与idea有关的,第四个是我们设置的.
另外,访问同一个网址的时候,会把网址对应的所有的cookie都发过去

缺点:¶
- 缺点:
- 移动端APP(Android、IOS)中无法使用Cookie
- 不安全,用户可以自己禁用Cookie(设置禁止访问第三方cookie就行)
-
Cookie不能跨域
跨域:

- 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
- 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
- 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
- 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
- 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域
-
区分跨域的维度(三个维度有任何一个维度不同,那就是跨域操作):
- 协议
- IP/协议
- 端口
举例: - http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域] - http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]
Session¶
服务器端会话技术. Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的
登录之后,如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。
接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
代码示例:
前端结果:@Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1(HttpSession session){//声明session就行 log.info("HttpSession-s1: {}", session.hashCode()); session.setAttribute("loginUser", "tom"); //往session中存储数据,存储的值的名字是什么,值是什么. return Result.success(); } @GetMapping("/s2") public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); log.info("HttpSession-s2: {}", session.hashCode());//前后的hashCode应该是一样的. Object loginUser = session.getAttribute("loginUser"); //从session中获取数据 log.info("loginUser: {}", loginUser); return Result.success(loginUser);//告诉前端username是什么 } }
测试之后,显示结果:
两次请求,获取到的Session会话对象的hashcode是一样的,就说明是同一个会话对象。而且,第一次请求时,往Session会话对象中存储的值,第二次请求时,也获取到了。 那这样,我们就可以通过Session会话对象,在同一个会话的多次请求之间来进行数据共享了。
- 缺点:
- 服务器集群环境下无法直接使用Session
- 移动端APP(Android、IOS)中无法使用Cookie
- 用户可以自己禁用Cookie
- Cookie不能跨域
- 为什么集群不能使用session?
这是微服务集群架构图:
最终部署的时候都是以集群的形式来进行部署,也就是同一个项目它会部署多份。比如这个项目我们现在就部署了 3 份。
登录请求到达负载均衡服务器,将这个请求转给了第一台 Tomcat 服务器。此时假如又执行了一次查询操作,要查询部门的数据。这次请求到达负载均衡服务器之后,负载均衡服务器将这次请求转给了第二台 Tomcat 服务器.这样就出现不了问题的.
令牌技术(最常用)¶
就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串. 如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。 接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。 接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。 此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。 - 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
JWT令牌技术¶
- JWT全称 JSON Web Token (官网:https://jwt.io/),定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
- 简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
- 自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
- 简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。 jwt令牌是login之后产生的. #### JWT的组成:
- (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

JWT是如何将原始的JSON格式数据,转变为字符串的呢?¶
- 其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
- Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号(结尾的= 是一个补位符号,这个很容易在base64产生.)
- 需要注意的是Base64是编码方式,而不是加密方式。
- 我们可以看到,这是header和payload都是明文的.
- 签名肯定是有个密钥的.
生成和校验¶
要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 10);
claims.put("username", "itheima");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")//指定使用 HS256 签名算法,并传入签名密钥aXRjYXN0(这个密钥其实是ithema的base64编码)
.addClaims(claims)//将前面定义的claims中的自定义信息添加到 JWT 中。
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))//设置 JWT 的过期时间,也就是说这里是12个小时候过期.这个最后会在payload里添加一个expjson字段,标识过期时间
.compact();//完成 JWT 的构建,并将其转换为紧凑的字符串形式。
System.out.println(jwt);
}
也就是说,只是签名了,并没有加密.
第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。
第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。
由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。
进行校验:
@Test
public void testParseJwt() {
Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")
.getBody();
System.out.println(claims);
}
{id=10, username=itheima, exp=1701909015}
实际上,如果设置的时间过期,那么就
代码:¶
- 引入JWT工具类:在项目工程下创建 com.itheima.util 包,并把提供JWT工具类复制到该包下
package com.itheima.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.Map; 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; } } - 完善 EmpServiceImpl中的 login 方法逻辑, 登录成功,生成JWT令牌并返回
测试的结果:
@Override public LoginInfo login(Emp emp) { Emp empLogin = empMapper.getUsernameAndPassword(emp); if(empLogin != null){ //1. 生成JWT令牌 Map<String,Object> dataMap = new HashMap<>(); dataMap.put("id", empLogin.getId()); dataMap.put("username", empLogin.getUsername()); String jwt = JwtUtils.generateJwt(dataMap); LoginInfo loginInfo = new LoginInfo(emjpLogin.getId(), empLogin.getUsername(), empLogin.getName(), wt); return loginInfo;//这里的loginInfo就表示login成功之后,同时把令牌返回过去 } return null; }
我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),后续的每一次请求当中,都会将这个令牌携带到服务端。(当然,这是前端写的)

过滤器 Filter¶
我们可以看到在后续的请求当中,都会在请求头中携带JWT令牌(token字段)到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。 - Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。(除了Filter之外的两个都不常用了.) - 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能 - 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。 - 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
快速入门¶
- 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
public class DemoFilter implements Filter { //滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。 public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init ..."); } //这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { System.out.println("拦截到了请求..."); } //- destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。 public void destroy() { System.out.println("destroy ... "); } } - 第2步,配置过滤器
在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求
当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。(Springboot中使用这种传统开发的组件,必须这么做.)
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 ) public class DemoFilter implements Filter { //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次,很不常用 public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init ..."); } //拦截到请求时,调用该方法,可以调用多次 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { System.out.println("拦截到了请求..."); } //销毁方法, web服务器关闭时调用, 只调用一次.很不常用 public void destroy() { System.out.println("destroy ... "); } }注意事项:在过滤器Filter中,如果不执行放行操作,将无法访问后面的资源。 放行操作:chain.doFilter(request, response);@ServletComponentScan //开启对Servlet组件的支持 @SpringBootApplication public class TliasManagementApplication { public static void main(String[] args) { SpringApplication.run(TliasManagementApplication.class, args); } }
注意专门创建了一个包;注意导入的位置.
放行操作:

具体实现¶
大概清楚了在Filter过滤器的实现步骤了,那在正式开发登录校验过滤器之前,我们思考两个问题:
1. 所有的请求,拦截到了之后,都需要校验令牌吗 ?
- 答案:登录请求例外,注册也是(不过我们这个项目没有注册,都是后台管理员手动加的.)
1. 拦截到请求后,什么情况下才可以放行,执行业务操作 ?
- 答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
401是前后端约定好的,表示没有登录,前端收到了之后,立刻就跳转到登录页面.
在 com.itheima.filter 包下创建TokenFilter,具体代码如下:
package com.itheima.filter;
import com.itheima.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.io.IOException;
/**
* 令牌校验过滤器
*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;//因为我们知道这是httprequest
HttpServletResponse response = (HttpServletResponse) resp;
//1. 获取请求url。
String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
log.info("登录请求 , 直接放行");
chain.doFilter(request, response);
return;
}
//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);//其实就是401.
return;
}
//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//6. 放行。
log.info("令牌合法, 放行");
chain.doFilter(request , response);
}
}
细节¶
- 在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。
效果:
@WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter { @Override //初始化方法, 只调用一次 public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init 初始化方法执行了"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("DemoFilter 放行前逻辑....."); //放行请求 filterChain.doFilter(servletRequest,servletResponse); //资源访问结束后(也就是Controller都返回了之后),就会回到这里, System.out.println("DemoFilter 放行后逻辑....."); } @Override //销毁方法, 只调用一次 public void destroy() { System.out.println("destroy 销毁方法执行了"); } }
- 拦截路径

- 过滤器链
过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。
比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。
访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。
先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。 - 过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:
- AbcFilter
- DemoFilter 这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。
拦截器Interceptor¶
什么是拦截器?
- 是一种动态拦截方法调用的机制,类似于过滤器。
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
- 拦截器的作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
基本使用¶
- 自定义拦截器
实现HandlerInterceptor接口,并重写其所有方法
//自定义拦截器 @Component public class DemoInterceptor implements HandlerInterceptor { //目标资源方法执行前执行。 返回true:放行 返回false:不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle .... "); return true; //true表示放行 } //目标资源方法执行后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ... "); } //视图渲染完毕后执行,最后执行.视图渲染只在前后端不分离的时候常用.这里了解一下即可. @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion .... "); } } - 注册配置拦截器
在 com.itheima下创建一个包,然后创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法
测试结果:
@Configuration //表示这是一个配置类.但是实际上,底层就是compenent的封装. public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired private DemoInterceptor demoInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求) } }

令牌校验拦截器.¶
在 com.itheima.interceptor 包下创建 TokenInterceptor.逻辑上只需要把filter的改一下就行.
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 获取请求url。
String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
log.info("登录请求 , 直接放行");
return true;
}
//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//6. 放行。
log.info("令牌合法, 放行");
return true;
}
}
@Configuration //打开这个,会发现里面也是@compoent
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
细节¶
在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(demoInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
.excludePathPatterns("/login");//设置不拦截的请求路径
}
}
执行流程¶
- 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
- Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
- 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。
- 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
对比¶
以上就是拦截器的执行流程。通过执行流程分析,大家应该已经清楚了过滤器和拦截器之间的区别,其实它们之间的区别主要是两点: - 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。 - 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。 另外,从代码上看,拦截器和过滤器都是在三层架构之外的.