依据前几篇文章,我们已经开发了部门管理和员工管理的功能,那么我们想一下,项目如果上线了的话,是否安全呢?
答案是一定不安全的,所以今天,我们来优化此案例,开发登录认证的功能。那么什么是认证呢?所谓认证就是根据用户的用户名和密码来进行校验的过程。认证成功之后,我们才可以访问系统中的信息,否则拒绝访问。
首先第一步,我们要先完成基础登录的功能,就是来判断用户输入的用户名或者密码是否正确。我们先来看一下登录功能具体的需求,首先,登录时需要输入员工的用户名和密码,即为员工的信息,同时也说明登录时我们要操作的表即为emp。用户名即为username,密码为password这两个字段。下面我们来考虑一下这个sql语句怎么写?其实就是根据用户名和密码查询员工,如果根据用户名和密码我查询到了员工,就说明用户名和密码是正确的,如果根据用户名和密码没有查询到员工,就说明用户名或密码错误。现在,我们来编写一下sql语句
select * from emp where username = 'zhangwuji' and password = '123456';
因为我们针对username添加了unique唯一约束,所以在查询的时候不会重复。最终查询的数据只会有一条。现在,我们来看一下接口文档
根据接口文档,接下来我们分析一下登录具体的实现思路
我们需要在controller这个接口中定义一个方法,在这个方法上我们需要加一个注解,也就是@postmapping,且请求的参数是一个json格式的参数,最终在服务端,我们要将其封装到一个对象中,这时我们就需要使用注解@ReuquestBody,我们只需要创建一个logincontroller,而service和mapper我们直接写在员工里面即可,即empservice和empmapper,在empservice中调用mapper接口的方法,根据用户名和密码来查询员工信息,再将查询到的员工信息返回给service,service再返回给controller,controller需要根据返回回来的员工信息进行判断,判断员工是否存在,如果存在,controller再返回给前端一个成功的结果,代表成功,如果不存在,那么直接返回一个登录失败的信息。
接下来我们在idea来完成登录这个功能,依旧是三层代码实现操作
首先是controller层
package com.ittaotao.controller;
import com.ittaotao.pojo.Emp;
import com.ittaotao.pojo.Result;
import com.ittaotao.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){ //注解作用就是将json格式的数据封装到这个实体类中
log.info("员工登录:{}",emp);
Emp e = empService.login(emp);
return e != null?Result.success() :Result.error("用户名或密码错误");
}
}
然后是service以及对应的impl实现层
/**
* 员工登录
* @param emp
* @return
*/
Emp login(Emp emp);
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
最后是mapper层接口
/**
* 根据用户名和密码查询员工
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password};")
Emp getByUsernameAndPassword(Emp emp);
最后,我们进行postman测试
最后,我们进行前后端联调就完成了基础的登录功能。
第二步,我们就要完成登录校验的操作
上一小节,我们已经完成了登录的操作,那么我们现在复制一下登录进去的地址,重新进行访问看一看会是什么情况。
可以发现,我们依旧可以访问进去,这次我们并没有登录。即在未登录情况下,我们也可以直接访问员工管理、部门管理等功能。这是一个异常现象
正常情况下,我们退出之后,必须重新登录才能访问,这才是一个正常的流程。那么为什么会出现这个问题呢?那是因为我们目前开发的功能都是正常的来操作数据库的数据,完成数据的增删改查,并没有判断用户是否登录。即无论用户是否登录,都可以正常的访问员工管理、部门管理这些功能。
要想解决这一问题,就要完成我们的第二功能——登录校验。登录校验即为当我们发起一个请求之后,服务端要去判断这个用户是否登录,如果登录则执行正常的业务操作。如果没有登录,则需跳转登录页面,让其完成登录再来访问系统。登录校验也是我们的重点。
接下来,我们来分析一下登录校验大概的实现思路,首先,我们先来思考一下,http协议是无状态的。所谓无状态,即每一次请求都是独立的,下一次请求不会携带上一次请求的数据。而浏览器与服务器之间进行交互就是通过http协议,那也就是说登录之后我要进行别的功能,浏览器重新请求,服务器无法判断员工是否登录,那我们能想到的如何判断呢?加if条件去一个一个判断?这样太繁琐了。为了简化这一操作,我们就可以通过统一拦截来拦截浏览器发送过来的统一请求。我们拦截到了之后,取回之前存储的登录标记进行对比,如果没有问题,则表示已经登录,那么就放行执行其他功能。如果存在问题,那么我们可以响应一个错误信息给前端,让前端跳转到登录界面。
执行上面操作,我们要涉及到两个部分,一个是登录标记,登录标记要求登录成功之后,每一次请求中,都可以获取到该标记。这个操作将涉及到web开发的会话技术。另一方面,则是实行统一拦截,即有两种方式,第一种servlet中提供的过滤器filter,还有一种就是spring当中提供的拦截器interceptor。那么我们登录校验就分为四部分来讲,分别是传统的会话技术、当前项目主流的方案JWT令牌技术,最后两种统一拦截的技术,过滤器filter以及拦截器Interceptor
在web开发当中,会话就指浏览器和服务器之间的一次连接,这个会话是指用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
我们可以分析到,每一次会话都代表了一个浏览器与服务器进行连接,不过你发起了多少请求,只要是一个浏览器,那就只是一个会话。
接下来,我们来介绍一下会话跟踪,会话跟踪,即为一种维护浏览器状态的方法,服务器需要识别多次请求是否来自同一浏览器,以便在同一次会话的多次请求间共享数据。如果服务器发现多个请求来自同一浏览器,那么我们即可共享数据。
会话跟踪方案,我们会讲解三种
首先是cookie,cookie是客户端会话跟踪技术,他是存储在客户端(浏览器)当中的,我们使用cookie来跟踪会话,我们就可以存储相关的一些属性信息。首先,服务器会自动的将cookie响应给浏览器,浏览器接收到回来的响应数据之后,会自动的将cookie存储在浏览器本地,在后续的请求当中,浏览器会自动的将cookie携带到服务器端。
cookie:请求头是作为http请求报头包含存储先前通过与所述服务器发送的HTTP cookies,即给服务端传送cookie数据。
set-cookie:响应头:HTTP响应报头被用于从服务器向用户代理发送cookie
接下来我们用代码演示一下,首先,创建一个sessioncontroller
package com.ittaotao.controller;
import com.ittaotao.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@RestController
public class SessionController {
//设置cookie
@GetMapping("/c1")
public Result cookie(HttpServletResponse response){
response.addCookie(new Cookie("login_username","ittaotao"));
return Result.success();
}
//获取cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie:cookies){
if (cookie.getName().equals("login_username")){ //输出name为login_username的cookie
System.out.println("login_username:"+ cookie.getValue());
}
}
return Result.success();
}
}
运行此服务浏览器输入localhost:8080/c1,打开开发者工具,可以发现确实存在一个set-cookie
接下来我们再次打开一个窗口运行c2,c2只有一个请求的cookie:ittaotao,即访问c2时,通过请求头携带到服务端,同时我们看到控制台也把login_username输出出来了。
最后,我们说一下cookie这种会话技术的优缺点。cookie是http协议中支持的技术,是官方提供的,无序手动操作,但是cookie在移动端无法使用,也就是只有浏览器支持cookie,安卓端和ios端都不支持cookie,其次cookie不是很安全,所以我们不能存放敏感、隐私的数据,其用户可以自己禁用cookie。如果用户禁用了cookie,那么cookie也就无法使用了。最后,cookie不可以跨域,也就是说我们目前主流的前后端分离开发,前后端各占一个服务器,当我们的浏览器访问完前端之后再次访问后端,由于我们当前所处的地址以及我们要请求的地址,三个维度(协议、IP/域名、端口)当中有任何一个不同,那就是跨域操作。如果跨域请求的话,cookie就不能用了。
上面介绍了我们的cookie操作,接下来我们来介绍第二种会话跟踪方案——session,session是服务器端会话存储技术,即存储在服务器端。而session底层就是基于我们刚才学到的cookie来实现的,如果说我们现在要基于session进行会话跟踪,那浏览器第一次请求服务器,服务器就可以获取到了session,而每一个会话对象都有一个id,即sessionId,那接下来,服务器端响应会将这个sessionId以setcookie的方式响应给浏览器。浏览器接收到sessionId会自动存储到本地,接下来每次请求都会携带过去,服务器拿到之后,会根据sessionId来从众多的session对象中找到当前的请求对象session
接下来我们进行演示
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1:{}",session.hashCode());
session.setAttribute("loginUser","tom"); //往session中存储数据
return Result.success();
}
//从HttpSession中获取值
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2{}",session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser:{}",loginUser);
return Result.success(loginUser);
}
运行服务,我们先访问s1,可以看到一个响应头set-cookie,里面包含一个sessioniD,接着我们看一下application,可以发现里面多了一个值为JSESSIONID。紧接着,我们访问s2,可以发现,我们请求头里包含的cookie有JSESSIONID,那么我们就可以根据这个id找到对应session对象。
经过测试,我们可以发现它的底层就是基于cookie的,那么最后,我们来说一下这种方案的优缺点,首先,session的数据都存储在服务器端,所以十分安全,然后,我们来说一下它的缺点,在pc互联网以及移动互联网高速发展的今天,session已经不再适用,首先第一点,我们现在所开发的项目一般都不会部署到一台服务器上,因为会出现单点故障的问题(一旦服务器挂掉,整个应用则无法使用),所以现在开发的项目都是以集群部署,也就是说同一个项目会部署多份,让其负载均衡。同时造成的缺点也就是无法找到session。其实,剩余的缺点都是cookie的缺点。
因为上面两种方案在企业开发中都会存在一堆问题,因此在现在的企业开发中,都会采用令牌技术的方案来解决会话跟踪。其实令牌技术,其本质就是一个字符串,在浏览器请求登录的时候,如果登录成功,那就生成一个令牌,即此令牌就是该用户的合法身份凭证。接下来响应数据时,就可以把这个令牌响应给前端,在前端程序当中接收到该令牌就可以将该令牌存储起来。接下来,在后续的请求当中,都需要将这个令牌携带到服务端,携带过去之后,我们就需要来校验这个令牌的有效性,如果这个令牌有效,那就说明用户之前已经执行了登录操作,如果这个令牌无效,那就说明用户之前并没有执行登陆这个操作,如果几次请求之间我们想要共享数据,那么我们就可以把想要存储的数据存储到这个令牌当中。
令牌技术的好处,它既支持pc端,又支持移动端,同时,他可以解决集群环境下的认证问题,因为我在服务器端不需要存储任何数据,这样一来,这也减轻了服务器端的存储压力。令牌技术的缺点就是需要我们自己去实现。
刚才,我们介绍了通过令牌技术来追踪会话,令牌的形式有很多,我们要讲解的是功能强大的JWT令牌。
JWT令牌(JSON Web Token),定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息,由于数字签名的存在,这些信息是可靠的。其组成形式如下
当我们生成JWT令牌时,要对上面的三部分json格式的数据进行Base64编码(一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式),也就是说任何数据经过Base64编码都会变成对应的那64个字符,当然还有一个补位符"="。最后,我们来介绍一下JWT令牌的应用场景:登录认证
首先,登陆成功后,生成令牌,其次,后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理。
简单介绍了什么事JWT令牌以及JWT令牌的组成,接下来我们讲解如何通过java代码来生成和校验JWT令牌
首先,我们来讲解JWT令牌的生成,首先,我们要在pop.xml中引入JWT令牌的依赖,引入依赖完成之后,我们就可以调用这个依赖给我们的api,来完成JWT令牌的生成和校验,而无论生成还是校验,都需要利用他给我们生成的工具类jwts,好了,接下来我们进行代码演示
首先,在pop.xml中进行依赖配置
io.jsonwebtoken
jjwt
0.9.1
然后,我们在测试中进行jwt令牌的编写
/**
* 生成JWT
*/
@Test
public void testGenJWT(){
Map claims = new HashMap<>();
claims.put("id",1);
claims.put("name","tom");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "ittaotao") //签名算法
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置JWT令牌的有效期为一个小时
.compact();//拿到字符串类型的返回值(JWT令牌)
System.out.println(jwt);
}
启动该测试,可以发现,jwt令牌已经在控制台输出出来了
我们打开jwt的官网,将上面这段字符串复制上去,他会自动帮我们解析出来
接下来,我们在写一个解析JWT令牌的测试代码
**
* 解析JWT
*/
@Test
public void testParseJWT(){
Claims claims = Jwts.parser()
.setSigningKey("ittaotao") //给定签名秘钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcwNTA1NDY1OH0.BA5qQ_yMhxx1mwchyvEvbH1DRwrU-Djyxt-myhOWtAM")
//传递JWT令牌
.getBody();//获取自定义内容
System.out.println(claims);
}
接着,运行该测试,控制台已经把解析出来的json数据打印出来了
我们已经拿到了id,name,令牌的过期时间。
以上就是我们jwt令牌的生成与解析,也就是校验。那么我们在校验时需要注意:JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的,如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或者失效了,令牌非法。
接下来,我们要在案例当中利用JWT令牌技术来跟踪会话。其实思路我们前面都已经分析过了,主要就是两步:令牌生成和令牌校验。在登录成功后,生成JWT令牌,并返回给前端,在请求到达服务端后,对令牌进行统一拦截、校验。接下来我们就再看一下接口文档对于登录这一接口的描述
我们主要看一下响应数据,可以看到,data属性对应的就是一个jwt令牌,所以我们只需要把生成的JWT令牌封装在result中,然后返回给前端即可,此时前端就可以接收到这个jwt令牌。接下来,我们再看一下备注说明
用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为 token ,值为 登录时下发的JWT令牌。
如果检测到用户未登录,则会返回如下固定错误信息:
{
"code": 0,
"msg": "NOT_LOGIN",
"data": null
}
基于以上信息,我们直接打开idea,完成以上操作,我们直接在utils下面创建一个JWTutils.java
package com.ittaotao.utils;
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 = "ittaotao";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map 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;
}
}
接下来,我们对之前写的logincontroller进行一个更改,加上是否登录的判断,并对其添加JWT令牌
@PostMapping("/login")
public Result login(@RequestBody Emp emp){ //注解作用就是将json格式的数据封装到这个实体类中
log.info("员工登录:{}",emp);
Emp e = empService.login(emp);
//登陆成功,生成令牌,下发令牌
if (e != null){
Map claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
String jwt = JwtUtils.generateJwt(claims);//jwt当中包含了当前登录的员工信息
return Result.success(jwt);
}
//登陆失败,返回错误信息
return Result.error("用户名或密码错误");
}
当我们全部编写完成之后,我们就可以利用postman进行测试了
测试完成之后,我们可以发现data中已经返回给了我们JWT令牌了,我们将其复制到jwt官网上,可以发现这个jwt令牌解析出来就是我们员工登录的登录信息,包括id,name和username。最后我们进行前后端联调,即可完成这次的操作
刚刚我们讲解了JWT令牌,并且在员工登录后下发了JWT令牌,JWT令牌就是用户成功登陆后的标记,接下来我们将介绍如何进行统一校验JWT令牌,也就是登录成功的标记,接下来我们会讲解两种比较主流的方案,一种是过滤器filter,另一种是拦截器Interceptor
首先,过滤器filter,javaweb三大组件(Servlet、Filter、Listener)之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。同时,过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等等。
此时,当我们有了过滤器,我们就可以把登录校验的功能定义在过滤器filter当中。在filter中校验用户是否登录。如果用户已经登录则放行,让其去访问那些资源,如果没有登录则在filter当中响应错误信息,即不允许其访问对应的资源。
我们现在可以通过一个快速入门程序来掌握filter对应的开发步骤,主要分为两步:1.定义filter:定义一个类,实现filter接口,并重写其所有方法(init、doFilter、destroy);2.配置filter:filter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@SevletComponentScan开启Sevlet组件支持(因为servlet是javaweb三大组件之一,并不是springboot当中的,所以想要使用必须在启动类上加上上方注解)。
接下来我们直接进行演示,首先,连包带类一起创建DemoFilter.java
package com.ittaotao.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*") //表示当前过滤器拦截所有请求,/*表示所有请求
public class DemoFilter implements Filter {
//因为init和destroy都有默认实现,一般可以不写,这里进行演示所以三个方法都进行了重写。
@Override //初始化方法,只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init初始化方法执行");
}
@Override //拦截到请求之后调用,会调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("拦截到了请求");
}
@Override //销毁方法,只调用一次
public void destroy() {
System.out.println("destroy销毁方法执行");
}
}
同时,我们要在启动类上加一个注解表示可以支持servlet组件
我们运行该服务,可以发现init和destroy都只执行了一次,destroy是在服务结束时运行的,而filter每请求一次都会执行一次,在控制台输出拦截到了请求。但是,我们可以发现,当我们点开部门管理时候,发现里面没东西,其实是因为filter只把请求拦截到了,但没有进行其他的处理,也就是没有做任何事情让请求去访问对应的资源,所以才会导致那些资源不被访问到。此时,我们就需要对其进行放行的操作。可以发现,springboot为我们通过了一个api,我们可以调用chain的方法进行放行,下面我们更改代码
@Override //拦截到请求之后调用,会调用多次
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("拦截到了请求");
//放行
chain.doFilter(request,response);
}
此时,我们打印输出完之后就调用dofilter让其放行去访问请求的资源。此时我们重新启动服务,点击部门管理之后会发现数据已经显示出来,说明过滤器已经放行成功了。
刚才,我们已经了解了filter的快速入门,接下来我们要详细的介绍一下filter过滤器当中的一些细节,我们主要介绍三个过滤器方面的细节,执行流程、拦截路径、过滤器链。
首先是执行流程,过滤器在放行之前会执行放行前逻辑,也就是dofilter之前的代码,那在放行之后,请求会去访问对应的资源,之后会在回到过滤器中,这个时候我们还可以执行放行后的逻辑,也就是在dofilter之后编写对应的代码。对此,我们可以总结两点:放行后访问对应资源,资源访问完成后,还会回到filter中;其次,如果回到filter中,会直接执行放行后的逻辑而不是重新执行,以上便是过滤器的执行流程。
接着,我们可以了解一下拦截路径,即@WebFilter注解,Filter可以根据需求,配置不同的拦截资源路径:首先第一种,即拦截具体路径(/login:只有访问/login路径时,才会被拦截),第二种,拦截目录路径(/emps/*:访问/emps下的所有资源,都会被拦截),第三种,拦截所以(/*:访问所有资源,都会被拦截)
最后,我们来学习一下过滤器链,什么是过滤器链?所谓过滤器链,是指在web应用当中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。而处于链上的过滤器会一个一个执行,会先执行第一个,第一个放行之后才会执行第二个,以此类推……,同样访问完web资源之后会回来执行放行后逻辑,此时顺序就会反过来,从后往前依次执行,最后再给浏览器响应数据。而其顺序则是以注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序,类名排名越靠前,优先级越高。
我们上面已经把过滤器的入门以及使用细节学习完了,接下来最后一步,我们需要通过过滤器filter来完成案例中的登录校验功能。
接下来,我们要分析一下登录校验的具体流程,首先,我们来思考两个问题
第一个问题,会有一个例外,即登录操作,在执行登录操作的时候,用户并没有jwt令牌,所以不需要校验。第二个问题,只有有令牌,且令牌校验通过(合法)才可以放行,否则都返回未登录错误信息,即接口文档中的错误信息
{
"code": 0,
"msg": "NOT_LOGIN",
"data": null
}
接下来,我们根据流程图分析一下具体的实现步骤
具体的思路和步骤我们已经分析完毕,接下来我们打开idea来进行登录校验的具体实现
里面的代码我们有一步需要下载一个依赖来满足对象到json格式的手动转换,打开pop.xml注入下面依赖
com.alibaba
fastjson
2.0.19.graal
然后,我们编写loginCheckFilter来进行流程的处理
package com.ittaotao.filter;
import com.alibaba.fastjson.JSONObject;
import com.ittaotao.pojo.Result;
import com.ittaotao.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request; //将request进行强转
HttpServletResponse resp = (HttpServletResponse) response;
//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (url.contains("login")){
log.info("登录操作,放行...");
chain.doFilter(request,response);
return;
}
//3.获取请求头中的令牌
String jwt = req.getHeader("token");
//4.判断令牌是否存在(字符串为null或者为空字符串),如果不存在,返回错误信息(未登录)
//if (jwt == null || jwt == " ")
if (!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json ----------> 阿里巴巴fastJson
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//5.解析token,如果解析失败(即解析jwt令牌报错),返回错误信息(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { //出现异常,jwt令牌解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json ----------> 阿里巴巴fastJson
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行
log.info("令牌合法,放行");
chain.doFilter(request,response);
}
}
编写完代码之后我们可以通过postman进行测试,测试完成之后打开浏览器进行前后端联调测试
以上我们便可以进行登录filter的登录校验了,接下来我们将来讲解一下拦截器Interceptor
拦截器我们将分为三个部分来讲解(简介&快速入门、详解、登录校验-Interceptor)
拦截器是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法(controller)的执行,其作用即为拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。我们可以发现,拦截器和过滤器非常类似,接下来我们就通过一个快速入门来演示一下拦截器的基本使用。
拦截器的使用也是分为两部分,首先,定义拦截器,实现HandlerInterceptor接口,并重写其所有方法(preHandle:目标资源方法(controller)执行前执行,返回true:放行,返回false:不放行、postHandle:目标资源方法执行后执行、afterCompletion:视图渲染完毕后执行,最后执行);其次,注册配置拦截器。
接下来,我们在idea上完成这个操作,首先,在ittaotao下创建一个包interceptor,在创建一个类LoginCheckInterceptor
package com.ittaotao.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//重写HandlerInterceptor中的三个方法
@Override //在目标资源方法运行前运行,返回true:放行,返回false,不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle......");
return 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......");
}
}
以上即为定义拦截器
接着,注册配置拦截器,首先创建一个包config用于装入配置类,其次,新建一个类WebConfig开始注册配置拦截器
package com.ittaotao.config;
import com.ittaotao.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration //代表当前类是一个配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor; //通过依赖注入引入刚才创建好的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**"); //注册添加刚才创建好的拦截器,addPathPatterns表示要拦截的资源(/**表示全部资源)
}
}
综上所述,拦截器就配置好了,接下来我们进行测试,在这之前我们先把filter的@webfilter(urlpatterns="/*")注释掉,这样我们运行时候filter过滤器就不会启动了,然后我们启动服务,进行登录这个请求,请求完之后可以看到控制台的输出
首先执行prehandle,然后放行去执行controller中的方法(登录),登录回来会执行postHandle这个方法,最终执行afterComptetion。
以上便是我们拦截器的快速入门,接下来我们来介绍拦截器的使用细节
首先我们来说一下拦截路径,即可以根据需求,配置不同的拦截路径:
register.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")[代表需要拦截哪些路径].excludePathPatterns("/login")[代表不需要拦截哪些路径]
以上便是拦截器的拦截路径了,介绍完后接下来我们来说一下拦截器的执行流程
当我们打开浏览器来访问部署到web服务器的web应用时,此时我们所定义的过滤器会拦截到这次请求,当拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作,而由于我们当前是使用springboot开发的,所以放行之后进入到了spring的环境当中,那就要访问我们定义到controller当中的接口方法,但是tomacat服务器并不识别我们controller的程序的,但是他能识别servlet程序,因为tomacat是一个servlet容器,而在springweb环境当中,他提供了一个非常核心的servlet,我们称为前端控制器,这个servlet叫dispatcherservlet,所以请求会先进入到dispatcherservlet,由其将这个请求再转给controller,再去执行对应的接口方法,但是我们现在又定义了拦截器,所以在执行controller的方法之前,先要被拦截器拦截住,拦截器拦截了这次请求之后,接下来就要对这些请求进行处理。在执行controller方法之前,会先执行prehandle,返回true之后,执行放行操作,允许其访问控制器。控制器方法执行完毕之后,才允许其返回执行postHandle以及aftercompletion,然后再返回给diapatcherservlet,最终执行过滤器当中放行后的逻辑,逻辑执行完毕之后,最终给浏览器响应数据,以上便是拥有过滤器和拦截器同时存在的执行过程了。
最后,我们进行代码测试,可以发现其执行顺序就是如上所示
通过以上,我们可以发现filter和interceptor的区别
接口规范不同:过滤器需要实现filter接口,而拦截器需要实现HandleInterceptor接口。
拦截范围不同:过滤器filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
最后,我们将要完成拦截器的最后一步操作,实现案例的登录校验功能。登录校验的过程以及逻辑步骤和过滤器完全一样,我们不再分析。现在我们只需要将过滤器filter换成拦截器Interceptor即可,接下来我们直接进行代码演示,我们直接对LoginCheckInterceptor进行修改
package com.ittaotao.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.ittaotao.pojo.Result;
import com.ittaotao.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//重写HandlerInterceptor中的三个方法
@Override //在目标资源方法运行前运行,返回true:放行,返回false,不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (url.contains("login")){
log.info("登录操作,放行...");
return true;
}
//3.获取请求头中的令牌
String jwt = req.getHeader("token");
//4.判断令牌是否存在(字符串为null或者为空字符串),如果不存在,返回错误信息(未登录)
//if (jwt == null || jwt == " ")
if (!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json ----------> 阿里巴巴fastJson
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false; //不能放行
}
//5.解析token,如果解析失败(即解析jwt令牌报错),返回错误信息(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) { //出现异常,jwt令牌解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json ----------> 阿里巴巴fastJson
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行
log.info("令牌合法,放行");
return 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......");
}
}
我们启动服务,利用postman进行测试,首先,进行登录操作,可以在其返回信息上面拿到我们的令牌,接着,我们开始查询部门,可以发现,一开始如果没有携带令牌,会报错误,返回not_login,此时,我们把令牌附上,这时,我们便可以查询部门了,同时拦截器也将我们放行。
最后我们只需要进行前后端联调即可,这里我不再演示
前两个章节讲解完了基础登录和校验登录,接下来,我们就讲解我们的最后一张——异常处理
此时我们新增一个重复的部门,在界面中我们可以发现什么也没变化,我们打开开发者工具重新运行,可以发现它发送了一个请求,但是请求的响应结果是500,由此可知是服务器端出现的问题,打开代码发现,它会告诉你就业部(添加的部门)重复了,原因就是我们当初添加部门表的时候,为name添加了unique唯一约束。
我们可以发现其返回的数据为json格式,并不符合开发规范中的统一响应结果result,所以前端并不会解析此结果。那如果之后应用中出现这种异常我们该怎么处理呢?
方案一:在controller的方法中进行try...catch
可是controller的方法有很多,这样做就以为着我们需要再controller的每一个方法都进行try..catch来捕获异常,十分繁琐,代码臃肿
方案二:全局异常处理器
我们可以在整个项目中定义一个全局异常处理器,这样我们就可以来捕获整个项目中所有的异常,这种方式简单、优雅、且比较推荐这样做。有了这个全局异常处理器,mapper1遇到异常不用处理,直接抛给servlice,service遇到异常直接抛给controller,controller也不用处理,最终交给全局异常处理器,全局异常处理器,处理完成之后,再给前端响应标准的统一响应结果result即可。
接下来我们就打开idea来定义一个全局异常处理器,首先,连包带类创建Exception.GlobalExceptionHandler
package com.ittaotao.exception;
import com.ittaotao.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //捕获所有的异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
最后,我们再次新增部门,可以发现,前端已经给了我们错误提示,这也就证明了全局异常处理器已经捕获到了异常,我们也可以发现其返回响应的结果是一个标准的result
以上便是我们本篇博客的所有内容了(基础登录功能、登录校验功能以及异常处理),如有疑问还请评论区多度指教,记得多多点赞,感谢!!!