文章目录
- 前言
- 一、会话技术
- 1. Cookie
- 2. Session
- 3. 令牌
- 二、JWT令牌
- 1. 简介
- 二、过滤器Filter
- 1. 简介
- 2. 快速入门
- 3. 执行流程
- 4. 使用示例
- 5. 为什么自定义的Filter类不需要使用@Component
- 四、拦截器Interceptor
- 1. 介绍
- 2. 入门程序
- 3. Interceptor详解
前言
该篇详细对SpringBoot项目的登录检验功能进行了详细分析,包含jwt令牌,token的生产和检验,过滤器,拦截器等技术的使用。
一、会话技术
1. Cookie
cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,当使用 cookie 来跟踪会话,就可以在浏览器第一次发起请求来请求服务器的时候,在服务器端来设置一个cookie。
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中, 都会将浏览器本地所存储的 cookie 自动地携带到服务端。
在服务端就可以对cookie进行判,如果不存在这个cookie值,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样就可以基于 cookie 在同一次会话的不同请求之间来共享数据。
cookie 是 HTP 协议所支持的技术,各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:
- 响应头 Set-Cookie :设置Cookie数据的
- 请求头 Cookie:携带Cookie数据的
- 所以浏览器服务器可以完成上面的三个自动
- 服务器会 自动 的将 cookie 响应给浏览器。
- 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。
- 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。
程序实现分析如下:
@Slf4j
@RestController
public class SessionController {//设置Cookie 响应@GetMapping("/c1")public Result cookie1(HttpServletResponse response){response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookiereturn Result.success();}//获取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();}
}
2. Session
前面介绍的时候,我们提到Session,它是服务器端会话跟踪技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。
3. 令牌
这里我们所提到的令牌,其实它就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串。
如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。
接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。
接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。
此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。
二、JWT令牌
1. 简介
前面我们介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识,其本质就是一个字符串。令牌的形式有很多,我们使用的是功能强大的 JWT令牌。
2. 生成和校验
首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验
工具类:Jwts
package com.itheima;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;/*** @Author: 风清* @CreateTime: 2024-08-06 09:21*/
public class testJwt {//生成JWT代码实现:@Testpublic void testGenJwt(){// claims 一个键值对集合,用于存储 JWT 的载荷部分中的数据。例如用户 ID、角色或其他任何想要存储的信息。Map<String,Object> claims = new HashMap<>();claims.put("id",1);claims.put("password",123456);String string = Jwts.builder().signWith(SignatureAlgorithm.HS256, "MTAwODY=").addClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)).compact();//public static JwtBuilder builder() --- 用于创建JWT的构建器/*public JwtBuilder signWith(加密算法, 密钥) 设置加密信息 SignatureAlgorithm ---算法,常用HS256public JwtBuilder signWith(SignatureAlgorithm var1, byte[] var2);public JwtBuilder signWith(SignatureAlgorithm var1, String var2);public JwtBuilder signWith(SignatureAlgorithm var1, Key var2); Key---base64编码 推荐*/// public JwtBuilder addClaims(Map<String, Object> var1) 添加数据体(以键值对的格式)// public JwtBuilder setExpiration(Date var1) 设置密钥时限// public String compact() 将加密后的JwtBuilder对象转为String密钥System.out.println(string);}@Test//校验JWT令牌(解析生成的令牌):public void testParseJwt(){//Claims接口继承了Map接口,也是一个键值对的集合Claims claims = Jwts.parser().setSigningKey("MTAwODY=").parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6MTIzNDU2LCJpZCI6MSwiZXhwIjoxNzIyOTE3MDEyfQ.fgWip-sWmzBX6DdpJW_9vfs0jQg5mrNcf8FIlJv9GNE").getBody();//public static JwtParser parser() 创建一个新的解析器/*public JwtParser setSigningKey(密钥); 设置用于验证 JWT 签名的密钥,需要与加密保持一致public JwtParser setSigningKey(byte[] var1);public JwtParser setSigningKey(String var1);public JwtParser setSigningKey(Key var1); Key---base64编码 推荐*///Jws<Claims> parseClaimsJws(String var1) 解析 JWT,并获取 ClaimsJws 对象// (Jwt<H extends Header, B>) B getBody() 获取载荷部分 (payload) 中的数据,即 Claims 对象。System.out.println(claims);}
}
二、过滤器Filter
1. 简介
Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一,属于JavaEE。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
2. 快速入门
-
第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
-
第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
- init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。
- doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。
- destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。
3. 执行流程
过滤器拦截到请求之后,如果希望继续访问后面的web资源,就要执行放行操作,调用 FilterChain对象当中的doFilter()方法,在调用doFilter()方法之前所编写的代码属于放行之前的逻辑。
在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑写在doFilter()这行代码之后。
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {@Override //初始化方法, 只调用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init 初始化方法执行了");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("DemoFilter 放行前逻辑.....");//放行请求filterChain.doFilter(servletRequest,servletResponse);//上面的请求和响应参数System.out.println("DemoFilter 放行后逻辑.....");}@Override //销毁方法, 只调用一次public void destroy() {System.out.println("destroy 销毁方法执行了");}
}
4. 使用示例
过滤器实现
/*** 令牌校验过滤器*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {/*方法中的ServletRequest、ServletResponse接口分别被HttpServletRequest、HttpServletResponse接口继承了,所以可以强转*/HttpServletRequest request = (HttpServletRequest) req;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);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);}}
5. 为什么自定义的Filter类不需要使用@Component
在 Java Web 应用中,自定义的过滤器(Filter)类通常不需要使用 @Component 注解,这是因为过滤器的生命周期和配置是由 Web 容器(如 Tomcat)管理的,而不是由 Spring 容器管理。下面是几个原因说明为什么自定义的 Filter 类不需要使用 @Component 注解:
- 生命周期管理:
- 过滤器的生命周期(如初始化、销毁)是由 Web 容器管理的,而不是 Spring 容器。
- 当 Web 容器启动时,它会根据配置(如 web.xml 或 @WebFilter 注解)自动创建过滤器实例,并调用 init 方法来初始化过滤器。
- 当 Web 容器关闭时,它会调用过滤器的 destroy 方法来释放资源。
- 配置方式:
- 过滤器可以通过在 web.xml 文件中配置 和 元素来注册。
- 从 Java 7 开始,可以通过使用 @WebFilter 注解直接在过滤器类上进行配置。
- 这两种配置方式都不涉及 Spring 容器的管理。
- Spring 与 Web 容器的关系: Spring 容器负责管理 Spring Beans 的生命周期,而 Web 容器负责管理 Web 组件(如 Servlet、Filter 和 Listener)的生命周期。
- 当使用 Spring MVC 或 Spring Boot 时,虽然 Spring 容器和 Web 容器紧密集成,但过滤器仍然由 Web 容器管理。
- 使用 @Component 的场景:
- @Component 注解通常用于将类标记为 Spring Bean,使其成为 Spring 管理的对象。
- 如果你的过滤器需要与 Spring 容器中的其他 Bean 进行交互,或者需要通过 Spring 进行依赖注入,那么你可以考虑使用 @Component 注解,并且还需要在配置类中使用 @ServletComponentScan 来让 Spring 发现这些组件。
四、拦截器Interceptor
1. 介绍
拦截器
- 是一种动态拦截方法调用的机制,类似于过滤器。
- 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
- 是JavaEE规范中的,所以不需要导包,只需要实现然后交给Bean容器管理即可
拦截器的作用:
- 拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
2. 入门程序
自定义拦截器
//实现HandlerInterceptor接口重写方法
@Component
public class DemoInterceptor implements HandlerInterceptor {//目标资源方法执行前执行。 返回true:放行 返回false:不放行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle .... ");return true; //true表示放行}//目标资源方法执行后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle ... ");}//视图渲染完毕后执行,最后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion .... ");}
}
注意:
- preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
- postHandle方法:目标资源方法执行后执行
- afterCompletion方法:视图渲染完毕后执行,最后执行
注册配置拦截器
/*
然后创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,
并重写 addInterceptors 方法(该接口提供了各种插件方法,根据需求
重写,这里需要添加拦截器,就重写了addInterceptors添加拦截器的
方法)
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate DemoInterceptor demoInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象,并指定拦截器路径registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)}
}
3. Interceptor详解
- 拦截路径