JavaWeb学习(十二)
JavaWeb学习(十二):Web后端开发 —— 登录校验技术
本文为个人学习记录,内容学习自 黑马程序员
概述
- 功能:在浏览器发送请求后,首先对该请求进行校验,判断其是否登录,如果已登录则继续执行该请求的业务功能,否则返回错误结果
- 由于 HTTP 协议是无状态的,仅仅根据 HTTP 协议无法判断用户是否登录了,因此需要在用户登录后存储一个登录标记
- 考虑到如果每个请求都要书写登录校验功能,会导致代码较为繁琐,因此提出了统一拦截技术,对浏览器发送来的请求统一进行登录校验
会话技术
-
会话:当用户使用浏览器访问 Web 服务器的资源时会话建立,直至有一方断开连接(关闭浏览器/服务器)时会话结束,一次会话中可以包含多次请求和响应
-
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自同一浏览器,以便在同一次会话的多次请求间共享数据
-
会话跟踪方案:
-
客户端会话跟踪技术:Cookie
-
过程:当浏览器第一次发送请求到服务器端时,可以在服务器设置 Cookie 并存储相关信息(例如用户名、id 等),服务器向浏览器响应数据时会自动将 Cookie 作为响应头的一部分传递,浏览器接收到 Cookie 后自动将其存储到本地(存储到本地的 Cookie 信息的清理不取决于会话的通断,而是取决于浏览器设置,例如可以设置为关闭浏览器时清理),之后浏览器每次向该服务器发送请求时都会自动将这一 Cookie 作为请求头的一部分传递
-
示例:
// 设置Cookie // 将前端控制器中用于封装响应数据的HttpServletResponse对象作为方法的参数,设置用于响应的Cookie // new Cookie()的第一个参数为该Cookie的名字,第二个参数为该Cookie的值,这一步在响应中添加了一条Cookie // 设置后在响应头的Set-cookie一项中将记录有该Cookie,即Set-Cookie: login_username=victoria @GetMapping("/c1") public Result cookie1(HttpServletResponse response){ response.addCookie(new Cookie("login_username","victoria")); return Result.success(); } // 获取Cookie // 将前端控制器中用于封装请求数据的HttpServletRequest对象作为方法的参数,获取传递来的Cookie // 在请求头的Cookie一项中将记录有本次会话的所有Cookie,即Cookie: login_username=victoria @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()); } } return Result.success(); } -
优点:Cookie 是 HTTP 协议中支持的技术,使用方便
缺点:移动端 APP 无法直接使用 Cookie;Cookie直接存储在浏览器中,不安全;用户可以自己禁用 Cookie,导致相关功能失效;Cookie 不能跨域
跨域:只要两个地址的协议、IP 地址、端口号有任何一个不同,就称为跨域。例如当前前端页面的地址为 http://192.168.150.200:80/login.html,执行登录操作时需要向地址为 http://192.168.150.100:8080/login 的后端服务器发送请求,此时的请求就称为跨域请求,是不能使用 Cookie 的
-
-
服务端会话跟踪技术:Session
-
Session 的底层是基于 Cookie 实现的
Session 仅仅只是一条 Cookie,例如 Cookie: JSESSIONID=355E015D1EA6DE7FF4F106F00286B641,并且一个会话只有一个 Session
-
过程:当服务端第一次使用 Session 对象时,会自动创建一个会话对象 Session,每个会话对象 Session 都有一个唯一的 id,当服务端响应数据时会自动将该 Session 的 id 通过 Cookie 响应给浏览器,浏览器接收到后会自动将 Session id 存储在本地(Session 虽然也会存储在本地,但当会话重新建立时会创建新的 Session,因此会话断开后存储在本地的 Session id 就失去意义了,重新建立会话后原本 Cookie 中的 Session id 也会被覆盖成新的),在后续的每次请求时都会将该 Session 作为 Cookie 的一部分传递
-
示例:
// 设置Session中的数据 // 将HttpSession对象作为方法的参数,如果当前对话的Session不存在,则会创建一个新的Session;如果存在,则会获取到当前会话对应的Session // session.setAttribute()的第一个参数为存储数据的名字,第二个参数为该数据的值 @GetMapping("/s1") public Result session1(HttpSession session){ session.setAttribute("loginUser", "victoria"); return Result.success(); } // 从Session中获取数据 // 将前端控制器中用于封装请求数据的HttpServletRequest对象作为方法的参数,获取传递来的Session id // 根据浏览器传递来的Session id找到当前会话对应的Session对象,对象内可以存储需要在多次请求间共享的数据 @GetMapping("/s2") public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); return Result.success(loginUser); } -
优点:存储在服务器端,安全
缺点:服务器集群环境下无法直接使用 Session;Session 的底层基于 Cookie,因此包含 Cookie 的其他缺点
-
-
令牌技术
-
过程:在用户登陆完成后在服务端生成一个令牌,将令牌响应回客户端,客户端将令牌存储在本地,之后的每次请求都需要携带令牌,并在服务端进行统一拦截后校验令牌的有效性。如果需要在同一次会话的多次请求间共享数据,只需要将数据存储在令牌中即可
-
优点:支持 PC 端、移动端;解决了集群环境下的认证问题;减轻服务器端存储压力
缺点:需要自己实现
-
-
JWT 令牌
-
JWT:JSON Web Token
-
简介:定义了一种简洁的、自包含的格式,用于在通信双方以 json 数据格式安全地传输信息,由于数字签名的存在,这些信息是可靠的
-
组成:
- 第一部分:Header(头),记录令牌类型、签名算法等。例如
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如 {"id": "1", "username": "tom", "exp": 1670467224},最后一个为过期时间
- 第三部分:Signature(签名),防止令牌被篡改,确保安全性,是通过 Header、Payload 和指定密钥根据签名算法计算而来
-
实际 JWT 令牌类似于下面形式,三个部分之间通过小数点分割,前两部分是 JSON 数据根据 Base64 编码得到的,第三部分是根据签名算法计算得到的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 -
使用:
-
在 pom.xml 文件中引入依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> -
生成 JWT 令牌:
@Test public void testGenJwt() { // 自定义信息 Map<String, Object> claims = new HashMap<>(); claims.put("id", 1); claims.put("username", "tom"); String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "victoria") // 签名算法和密钥 .setClaims(claims) // 设置自定义内容,需要为Map集合或Claims对象 .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置JWT令牌的有效期为1h,需要为Date对象 .compact(); System.out.println(jwt); } -
解析 JWT 令牌:
@Test public void testParseJwt() { Map<String, Object> claims = Jwts.parser() .setSigningKey("victoria") // 提供密钥,必须和生成令牌时使用的密钥一致 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9...") // 指定需要解码的JWT令牌(字符串) .getBody(); // 获取载荷内容,返回值为Map集合或Claim对象 System.out.println(claims); }注意事项:解析令牌时出错通常有两种情况:1.令牌被篡改了 2.令牌有效期结束了
-
Filter
-
概念:Filter,过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一,可以把对资源的请求拦截下来,从而实现一些特殊的功能,例如登录校验
-
快速入门
- 定义 Filter:定义一个类,实现 Filter 接口,并重写方法
- 配置 Filter:Filter 实现类上加 @WebFilter 注解,配置拦截资源的路径
- 引导类(启动类)上加 @ServletComponentScan 开启 Servlet 组件支持,这是因为 Filter 是 JavaWeb 提供的组件,并不是 SpringBoot 提供的功能
import javax.servlet.Filter; // @WebFilter注解中的参数配置了拦截资源的路径,此处"/*"表明了拦截所有请求 @WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter { // 在Web服务器启动时,会自动创建Filter对象,创建后会自动调用其中的init方法来初始化,只会调用一次 // 为接口中的默认方法,可以不用重写 @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } // 拦截到请求时调用该方法 // 一定要重写 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { // 拦截到请求后,通过该语句放行 chain.doFilter(request, response); } // 销毁方法,服务器关闭时调用,只会调用一次 // 为接口中的默认方法,可以不用重写 @Override public void destroy() { Filter.super.destroy(); } } -
详解
-
执行流程:当浏览器发送请求后,该请求首先被 Filter 拦截,可以在放行前先执行特定业务逻辑,然后通过 chain.doFilter 方法放行,放行后正常访问 Web 资源,访问完后回到 Filter 中继续执行放行后的代码,最后向浏览器响应数据
-
拦截路径:
拦截路径 urlPatterns值示例 含义 拦截具体路径 /login 只有访问 /login 路径时,才会被拦截 目录拦截 /emps/* 访问 /emps 下的所有资源时,都会被拦截,包括 /emps 本身 拦截所有 /* 访问所有资源时,都会被拦截 -
过滤器链:
-
介绍:一个 Web 应用中,可以配置多个过滤器,这些过滤器就形成了一个过滤器链
-
实现:只要过滤器的拦截路径相同,或者存在包含关系,就会形成过滤器链
-
执行顺序:以注解方式配置的 Filter,优先级是按照过滤器类名的自然排序,例如存在过滤器 Abc 和 Demo,执行顺序为:
Abc放行前 -> Demo放行前 -> 访问Web资源 -> Demo放行后 -> Abc放行后 -
关键:chain.doFilter 本质上是放行到下一个过滤器,只有过滤器全部执行完了,才会放行到 Web 资源
-
-
-
使用 Filter 实现登录校验
@WebFilter(urlPatterns = "/*") public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1.对形参强转:request其实类型为HttpServletRequest(多态),为了调用HttpServletRequest的特有方法需要强转,response同理 HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 2.获取请求的url:考虑到并非所有的请求都要进行登录校验,例如登陆操作就不需要登录校验,因此需要获取请求的url再进行是否要登录校验的判断 String url = req.getRequestURI().toString(); // 3.剔除个例:判断请求url中是否包含login,如果包含则直接放行,需要注意放行后不需要再执行放行后逻辑,因此需要用return直接返回 if (url.contains("login")) { chain.doFilter(request, response); return; } // 4.获取请求头中的令牌:此处假设前端将令牌信息存储在了请求头的"token"属性中 String jwt = req.getHeader("token"); // 5.判断令牌是否存在:如果令牌不存在,返回错误结果,需要根据具体的业务逻辑进行响应,此处假设需要响应的数据格式为json格式 // 需要注意的是,在Controller层返回响应时,使用@RestController注解会自动将返回的实体对象转换成json格式,再经由HttpServletResponse返回 // 而此处需要直接对HttpServletResponse的对象进行操作,因此要先将实体类对象手动转换成json字符串,此处使用了JSONObject工具类,需要导入依赖 if (jwt == null || jwt.equals("")) { Result error = Result.error("NOT_LOGIN"); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; } // 6.解析令牌:考虑到如果解析失败会直接报错,因此需要使用try-catch语句 try { JwtUtils.parseJWT(jwt); } catch (Exception e) { Result error = Result.error("NOT_LOGIN"); String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; } // 7.放行:如果上述校验都通过则放行 chain.doFilter(request, response); } }
Interceptor
-
概念:Interceptor 是一种动态拦截方法调用的机制,是 Spring 框架提供的
-
快速入门
-
定义拦截器,实现 HandlerInterceptor 接口,并重写方法(接口内有三个方法,都存在默认实现)
@Component public class LoginCheckInterceptor implements HandlerInterceptor { // 目标资源方法执行前执行,用于拦截请求时会在Controller中的方法执行前执行,返回值为true时放行,否则不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } // 目标资源方法执行后执行,如果不放行就不会执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //... } // 视图渲染完毕后执行,是最后执行的,如果不放行就不会执行 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //... } } -
注册/配置拦截器
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; // 使用addInterceptor方法注册拦截器,再使用addPathPatterns方法配置该拦截器拦截的资源,如果要拦截所有资源需要使用"/**" @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**"); } }
-
-
详解
-
拦截路径:拦截器可以配置需要拦截的资源和不需要拦截的资源
// 示例:拦截除了"/login"以外的所有资源 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); }拦截路径 含义 举例 /* 一级路径 能匹配 /depts,/emps,不能匹配 /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 -
执行流程:
-
Filter 和 Interceptor 的不同:
- 接口规范不同:过滤器需要实现 Filter 接口,拦截器需要实现 HandlerInterceptor 接口
- 拦截范围不同:Filter 会拦截所有的资源,Interceptor 只会拦截 Spring 环境中的资源
-
浙公网安备 33010602011771号