JWT实现Token登录验证
案例-登录认证
在前面的课程中,我们已经实现了部门管理、员工管理的基本功能,但是大家会发现,我们并没有登录,就直接访问到了Tlias智能学习辅助系统的后台。 这是不安全的,所以我们今天的主题就是登录认证。 最终我们要实现的效果就是用户必须登录之后,才可以访问后台系统中的功能。
1. 登录功能
1.1 需求
在登录界面中,我们可以输入用户的用户名以及密码,然后点击 “登录” 按钮就要请求服务器,服务端判断用户输入的用户名或者密码是否正确。如果正确,则返回成功结果,前端跳转至系统首页面。
1.2 思路
登录服务端的核心逻辑就是:接收前端请求传递的 用户名 和 密码 ,然后再根据用户名和密码查询用户信息,如果用户信息存在,则说明用户输入的用户名和密码正确。如果查询到的用户不存在,则说明用户输入的用户名和密码错误。
具体的执行流程为:
接下来,我们就可以参照资料中提供的接口文档,来完成对应的功能开发。
1.3 实现
1). LoginController
@RestController
public class LoginController {@Autowiredprivate EmpService empService;@PostMapping("/login")public Result login(@RequestBody Emp emp){Emp e = empService.login(emp);return e != null ? Result.success():Result.error("用户名或密码错误");}
}
2). EmpService / EmpServiceImpl
EmpService:
/**
* 登录
* @param emp
*/
Emp login(Emp emp);
EmpServiceImpl:
@Override
public Emp login(Emp emp) {Emp e = empMapper.getByUsernameAndPassword(emp);return e;
}
3). EmpMapper
//根据用户名及密码查询员工信息
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
1.4 测试
功能开发完毕后,我们就可以启动服务,打开postman进行测试了。 发起POST请求,访问:http://localhost:8080/login
postman 测试通过了,那接下来,我们就可以结合着前端工程进行联调测试。
1.5 问题
在进行前后端联调测试时,我们发现,虽然我们开发的登录功能。但是当我在地址栏直接注入路由地址时,我们发现即使不登录,也是可以访问到后台系统的。
现在的登陆功能只是徒有其表,我们不登陆,直接访问http://localhost:9528/#/system/dept
,仍然可以访问。而真正的登陆功能应该是:登陆后才能访问主页,不登陆,则跳转登陆页面进行登陆。
那其实登录只是第一步操作。要想解决这个问题,更重要的是登录之后的校验操作。
2. 登录校验
2.1 问题分析
在未登录的情况下,我们可以直接在浏览器地址栏访问部门管理、员工管理等功能。
因为在我们现在实现的代码逻辑中,我们根据用户名 和 密码查询用户,查询到了用户信息,就判定登录成功,我们并没有在服务端或客户端记录任何用户登录成功的标识。 而HTTP协议又是无状态协议,那下一次在请求时,我们也无法判断员工是否已经登录。
而要想解决上述的问题呢,我们就需要做两件事情:
- 在员工登录成功后,需要将用户登录成功的信息,存起来,记录用户已经登录成功的标记。
- 在浏览器发起请求时,需要在服务端进行统一拦截,然后读取登录标记中的信息,如果有登录成功的信息,就说明用户登录成功,放行请求,如果发现登录标记中没有登录成功的标记,则给前端返回错误信息,跳转至登录页面。
其中:
- 统一拦截:可以使用两种技术实现,Filter过滤器 以及 Interceptor 拦截器。
- 登录标记:就需要用户登录成功之后,每一次请求中,都可以获取到该标记。
此标记需要在用户登录成功之后,每一个请求资源中,都可以获取到,也就是说可以在多次请求间共享。但是我们之前介绍过,HTTP是无状态的,不能在多次请求间共享数据,所以,此处需要使用会话跟踪技术来解决。
那接下来呢,我们就先来讲解会话跟踪技术,然后再讲解统一拦截技术:Filter、Interceptor。
2.2 会话技术
1 介绍
对于会话跟踪
这四个词,我们需要拆开来进行解释,首先要理解什么是会话
,然后再去理解什么是会话跟踪
:
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含
多次
请求和响应。
用实际场景来理解下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页后,浏览器和京东的服务器之间就建立了一次会话,后面的搜索商品,查看商品的详情,加入购物车等都是在这一次会话中完成。
思考:下图中总共建立了几个会话 ?
每个浏览器都会与服务端建立了一个会话,加起来总共是3
个会话。
- 从浏览器发出请求到服务端,响应数据给前端之后,一次会话(在浏览器和服务器之间)就建立了。
- 会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着。
- 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为`会话`。
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间
共享数据
。
那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能后能实现哪些功能呢?
- 服务器会收到多个请求,这多个请求可能来自多个浏览器,如上图中的5个请求来自3个浏览器。
- 服务器需要用来识别请求是否来自同一个浏览器。
- 服务器用来识别浏览器的过程,这个过程就是`会话跟踪`。
- 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据。
- 购物车: `加入购物车`和`去购物车结算`是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享。
- 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],比如百度、京东、码云等。
- 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。
那么该如何实现会话跟踪技术呢? 具体的实现方式有:
1). 客户端会话跟踪技术:Cookie
2). 服务端会话跟踪技术:Session
这两个技术都可以实现会话跟踪,它们之间最大的区别:Cookie是存储在浏览器端,而Session是存储在服务器端
。
2.cookie
@RestController
@RequestMapping("/cookie")
public class CookieController {//设置cookie@GetMapping("/c1")public Result cookie1(HttpServletResponse response) {response.addCookie(new Cookie("login_username", "test"));//设置cookie/响应cookiereturn Result.success();}//获取cookie@GetMapping("/c2")public Result cookie2(HttpServletRequest request) {Cookie[] cookies = request.getCookies();//获取所有的cookiefor (Cookie cookie : cookies) {if ("login_username".equals(cookie.getName())) {//输出name为login_username的cookieSystem.out.println("login_username:" + cookie.getValue());}}return Result.success();}}
使用浏览器测试:
3. Session
在SpringBoot的web环境中,我们要想获取Session会话对象,直接就可以在Controller方法的形参中声明,比如在登录的方法中声明:
@Slf4j
@RestController
@RequestMapping("/session")
public class SessionController {//向HttpSession中存储数据@GetMapping("/s1")public Result s1(HttpSession session){log.info("HttpSession-s1:{}",session.hashCode());session.setAttribute("loginUser", "tom");//向session中存储数据return Result.success();}//从HttpSession中获取数据@GetMapping("/s2")public Result s2(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);}}
这样前端浏览器访问该登录方法之后,与该方法就建立起了一个会话,那么此时服务器会给浏览器响应数据的同时,返回一个响应头 set-cookie,值呢就是服务器会话session的ID,然后浏览器就会将对应的值写入浏览器的Cookie 。如下图所示:
在当前浏览器中,如果再发起请求,浏览器会自动的将JSESSIONID这个Cookie携带到服务端,服务端接收到这个Cookie之后,就会自动根据cookie对应的值,找到对应的服务端会话对象Session。 比如,登录之后,我们发起请求,查询部门列表数据:
4. 问题分析
在现今前前后端分离开发模式下,Cookie、Session这种会话技术已很少使用,而且在服务器集群环境下 以及 客户端多样化的情况下,传统的Cookie、Session的会话方案就显得力不从心了,其主要问题,体现在两个方面:
- 服务端集群环境下Session的共享问题。
- 移动端APP端无法使用Cookie。
那在现在的企业开发中,如何来解决上述的会话问题呢? 此时,令牌技术就出现了。
使用令牌技术,具体流程如下:
- 在进行登录请求时,如果用户登录成功,可以给前端响应一个令牌(其实就是一个特殊的字符串,就是一个用户合法的身份凭证)。
- 前端需要将登录返回的令牌记录下来,保存在浏览器端(客户端)。
- 该浏览器在后续的请求中,每一次请求都会将该令牌携带到服务端,然后接下来在服务端,我们可以通过Filter或Interceptor对所有的请求进行拦截,然后进行校验,获取到请求中携带过来的令牌,进行判断,如果令牌正确合法,则放行,如果令牌不合法,则直接返回错误信息给前端,前端跳转到登录页面。
令牌技术优势:
- 解决了集群环境下的认证问题,减轻服务器端的存储压力
- 支持PC端、移动端
2.3 JWT令牌
2.3.1 介绍
- JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
- 官网: https://jwt.io/
- 标准: https://tools.ietf.org/html/rfc7519
- 优点:
- 使用 json 作为数据传输,有广泛的通用型,并且体积小,便于传输;
- 不需要在服务器端保存相关信息;
- jwt 载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等;
2.3.2 组成
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
- 第一部分:Header(头),作用:记录令牌类型、签名算法等。 比如
{"alg":"HS256","type":"JWT"
}
将上边的内容使用Base64编码,得到一个字符串就是JWT令牌的第一部分。
- 第二部分:Payload(有效载荷),作用:携带一些用户信息及过期时间等。 比如
{"id":"1","username":"Tom"
}
最后将第二部分负载使用Base64编码,得到一个字符串就是JWT令牌的第二部分。
- 第三部分:Signature(签名),作用:防止Token被篡改、确保安全性。比如:计算出来的签名,是一个字符串
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret
)
base64UrlEncode(header):jwt令牌的第一部分。base64UrlEncode(payload):jwt令牌的第二部分。secret:签名所使用的密钥

- 一个完整的JWT令牌内容如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2.3.3 生成
1). pom.xml 引入jwt的依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
2). 生成JWT代码实现
public class JwtDemo {@Testpublic void genJwt(){Map<String,Object> claims = new HashMap<>();claims.put("id",1);claims.put("username","Tom");String jwt = Jwts.builder().setClaims(claims) //执行第二部分负载, 存储的数据.signWith(SignatureAlgorithm.HS256, "itheima") //签名算法及秘钥.setExpiration(new Date(System.currentTimeMillis() + 12*3600*1000)) //设置令牌的有效期.compact();System.out.println(jwt);}}
2.3.4 校验
1). 代码实现
@Testpublic void parseJwt(){/*说明:只要jwt令牌解析失败下面的方法就会报异常*/Claims claims = Jwts.parser().setSigningKey("itheima")//itheima表示秘钥.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjgyODk4Njk5LCJ1c2VybmFtZSI6IlRvbSJ9.a0FyyeayUDpd3Nof-AgCqzh7oAvbZr_Mnwm4PEwi2x8").getBody();System.out.println(claims);}
注意事项:
- JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
- 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。
2.4 登录-生成令牌
2.4.1 代码实现
JWT令牌的介绍、组成、生成、校验等基础知识,我们已经讲解完了。接下来,我们就要来完善之前开发的登录功能。那第一步呢,就需要在登录成功之后,生成JWT令牌并返回给浏览器。
- 步骤:
- 引入JWT工具类
- 登录完成后,调用工具类生成JWT令牌并返回
1). 引入JWT工具类
public class JwtUtils {private static String signKey = "test";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). 登录成功,生成JWT令牌并返回
@RestController
public class LoginController {@Autowiredprivate EmpService empService;@PostMapping("/login")public Result login(@RequestBody Emp emp){Emp e = empService.login(emp);if(e != null){ //用户名密码正确Map<String,Object> claims = new HashMap<>();claims.put("id",e.getId());claims.put("username",e.getUsername());claims.put("name",e.getName());//生成JWT令牌String jwt = JwtUtils.generateJwt(claims);return Result.success(jwt);}return Result.error("用户名或密码错误");}
}
2.4.2 测试
登录功能完善后,我们就可以启动服务,打开postman来测试登录接口。
我们可以看到,登录成功后,服务端将生成好的令牌已经响应给我们了。 通过接口文档中的描述,我们也可以看出,登录成功之后,前端会在后面的每一次请求中将令牌携带过来,那接下来,我们需要做的就是需要在服务端统一拦截校验JWT令牌。
那统一拦截请求,在服务端,我们可以通过两种手段实现:过滤器Filter、拦截器Interceptor。
2.5 过滤器Filter
2.5.1 介绍
- 概念:Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
- 过滤器一般完成一些通用的操作,比如:登陆鉴权、统一编码处理、敏感字符处理等等…
2.5.2 快速入门
1). 定义类,实现 Filter接口,并重写doFilter
方法
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {}
}
2). 配置Filter拦截资源的路径:在类上定义 @WebFilter 注解
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {}
}
3). 在doFilter方法中输出一句话,并放行
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {System.out.println("拦截方法执行, 拦截到了请求 ...");System.out.println("执行放行前逻辑 ...");chain.doFilter(req, res);System.out.println("执行放行后逻辑 ...");}
}
4). 在引导类上使用@ServletComponentScan
开启 Servlet 组件扫描
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {public static void main(String[] args) {SpringApplication.run(TliasWebManagementApplication.class, args);}}
2.5.3 执行流程
疑问:
- 放行后访问对应资源,资源访问完成后,还会回到Filter中吗? 答案:会
- 如果回到Filter中,是重新执行还是执行放行后的逻辑呢?答案:执行放行后逻辑
2.5.4 Filter 拦截路径
拦截路径 | urlPattern值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
2.5.5 登录校验Filter
登录完成后,会把JWT令牌返回给前端,前端浏览器会将其存入本地存储。 在后面的请求中,前端会自动在请求头中将令牌token携带到服务端,接下来呢,我们就需要在服务端中通过过滤器来进行统一拦截校验。 过滤器中具体的校验流程如下:
1.获取请求url。
2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
3.获取请求头中的令牌(token)。
4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
5.解析token,如果解析失败,返回错误结果(未登录)。
6.放行。
代码实现:
1). pom.xml
引入json数据处理的工具 .
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.39</version></dependency>
2). 登录校验过滤器
package com.itheima.filter;import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;
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;@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;//1.获取请求url。String url = request.getRequestURL().toString();//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。//如果是login, 直接放行if(url.contains("login")){System.out.println("登录操作, 直接放行...");filterChain.doFilter(request, response);//执行过滤器之后不让往下执行return;}//3.获取请求头中的令牌(token)。//如果不是 login ,需要校验 tokenString token = request.getHeader("token");//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)if(!StringUtils.hasLength(token)){ //如果没有JWT令牌System.out.println("获取到token为空 , 返回错误信息...");//返回 未登录 提示信息//使用fastjson工具类转换为json数据String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));//解决响应乱码问题response.setContentType("application/json;charset=utf-8");//响应数据给前端response.getWriter().write(result);return ;}//5.解析token,如果解析失败,返回错误结果(未登录)。//解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息try {//解析失败会报异常JwtUtils.parseJWT(token);System.out.println("令牌解析成功, 直接放行 ...");} catch (Exception e) {e.printStackTrace();System.out.println("令牌解析失败 , 返回错误信息...");//返回 未登录 提示信息String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));response.setContentType("application/json;charset=utf-8");response.getWriter().write(result);return ;}//6.放行。//如果校验通过放行filterChain.doFilter(request, response);}}
2.5.6 测试
登录校验Filter功能开发完毕后,我们就可以启动服务,打开postman来测试。
我们会看到未登录情况下,服务端响应回来了错误信息 NOT_LOGIN。
接下来,我们再来调用登录接口,获取到JWT令牌。
然后,再请求其他接口时,需要在请求头 Header 中,添加token,将JWT的值放在请求头中,点击请求。
通过Web组件Filter可以完成请求的统一校验,我们也可以通过SpringMVC中提供的 Interceptor 来解决。
2.6 拦截器Interceptor
2.6.1 介绍
- 拦截器:(Interceptor)是一种动态拦截方法调用的机制,类似于过滤器。在SpringMVC中动态拦截控制器方法的执行
- 作用:在指定的方法调用前后执行预先设定的代码,完成功能增强
2.6.2 快速入门
- 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {//目标资源方法执行前执行 , true : 放行 ; false : 不放行,拦截 ;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle ....");//如果校验通过放行return false;}//目标资源方法执行后执行@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 ....");}}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginCheckInterceptor loginCheckInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");}
}
3.测试
2.6.3 执行流程
拦截路径 :
拦截路径 | urlPattern值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的下一级资源,如: /emps/1 ,但是 不会拦截 /emps/list/1,/emps/list/1/2 |
目录拦截 | /emps/** | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /** | 访问所有资源,都会被拦截 |
2.6.4 登录校验Interceptor
- 获取请求url。
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
- 获取请求头中的令牌(token)。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
- 解析token,如果解析失败,返回错误结果(未登录)。
- 放行。
代码实现:
//@Component
public class LoginCheckInterceptor implements HandlerInterceptor {//目标资源方法执行前执行 , true : 放行 ; false : 不放行,拦截 ;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String url = request.getRequestURL().toString();//如果是login, 直接放行if(url.contains("login")){System.out.println("登录操作, 直接放行...");return true;}//如果不是 login ,需要校验 tokenString token = request.getHeader("token");if(!StringUtils.hasLength(token)){ //如果没有JWT令牌System.out.println("获取到token为空 , 返回错误信息...");//返回 未登录 提示信息String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));response.setContentType("application/json;charset=utf-8");response.getWriter().write(result);return false;}//解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息try {JwtUtils.parseJWT(token);System.out.println("令牌解析成功, 直接放行 ...");} catch (Exception e) {e.printStackTrace();System.out.println("令牌解析失败 , 返回错误信息...");//返回 未登录 提示信息String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));response.setContentType("application/json;charset=utf-8");response.getWriter().write(result);return false;}//如果校验通过放行return 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 ....");}}
2.6.5 测试
登录校验Filter功能开发完毕后,我们就可以启动服务,打开postman来测试。
我们会看到未登录情况下,服务端响应回来了错误信息 NOT_LOGIN。
接下来,我们再来调用登录接口,获取到JWT令牌。
然后,再请求其他接口时,需要在请求头 Header 中,添加token,将JWT的值放在请求头中,点击请求。
前后端联调:
2.6.6 Filter 与 Interceptor 区别
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
3. 异常处理
3.1 现象
程序开发过程中不可避免的会遇到异常现象。比如,当我们进行修改员工数据时,如果员工的用户名重复,将会返回如下错误信息:
我们看法,返回的结果数据,默认框架返回的结果数据,并不是我们项目规范中定义的Result。从返回的信息中,我们可以看到状态码为 500,代表服务端错误,服务端出现了异常。此时,我们可以打开服务端控制台,查看错误信息。
在我们编写的程序中,可能由于传入参数问题,系统问题导致各种各样异常,那再SpringBoot 项目中异常该如何处理呢?
回忆一下以前学习的异常处理原则
- dao、service 层向上抛即可
- 但 controller 层不同,它如果再向上抛,此异常必然暴露给最终用户,这是不允许的。
3.2 思考
现在各层代码出现的异常,我们是如何处理的? 答案:未做处理
如果未做处理,也就意味着,Mapper层出现的异常,会自动往上抛,抛给service层。 service层出现的异常,也会自动往上抛,抛给controller。 而controller中我们也并未处理,此时再往上抛,就抛给框架了,框架会就会将错误信息响应给用户。
那么异常,我们该如何来处理呢?
- 方案一:在Controller的方法中进行try…catch处理 (代码过于臃肿)
- 方案二:全局异常处理器
3.3 全局异常处理器
由于将来项目中会有很多Controller,那么每个Controller都需要一个异常处理器,则比较麻烦。而且这些异常处理器的处理逻辑又比较相似,所以SpringMVC中提供了全局异常处理器接收所有Controller中产生的异常。一般定义在exception
包下:
@RestControllerAdvice
public class GlobalExceptionHandler {/*** Exception异常分类* - 运行时异常 : RuntimeException , 编译时无需处理 .* - 编译时异常 : 非 RuntimeException , 编译时处理 .*/@ExceptionHandler(Exception.class)public Result ex(Exception ex){ex.printStackTrace();return Result.error("系统繁忙, 请稍后重试 ... ");}}
流程:
@RestControllerAdvice = @ControllerAdvice + @ResponseBody