SpringBootWeb案例_03

Web后端开发_06

SpringBootWeb案例_03

登录认证

智能学习辅助系统登录时需要身份验证

1.登录功能

先实现简单的登录功能,在进一步优化。

1.1需求

若账户或密码不存在/密码不正确,则登录失败。

账户密码正确,则登录成功

SpringBootWeb案例_03_第1张图片

1.2接口文档

1.2.1登录-基本信息

请求路径:/login

请求方式:POST

接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。

1.2.2请求参数

参数格式:application/json

参数说明:

名称 类型 是否必须 备注
username string 必须 用户名
password string 必须 密码

请求数据样例:

{
	"username": "jinyong",
    "password": "123456"
}
1.2.3响应数据

参数格式:application/json

参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 1 成功 ; 0 失败
msg string 非必须 提示信息
data string 必须 返回的数据 , jwt令牌

响应数据样例:

{
  "code": 1,
  "msg": "success",
  "data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo"
}
1.2.4备注说明

用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为 token ,值为 登录时下发的JWT令牌。

如果检测到用户未登录,则会返回如下固定错误信息:

{
	"code": 0,
	"msg": "NOT_LOGIN",
	"data": null
}

1.3思路

SpringBootWeb案例_03_第2张图片

1.4功能实现

LoginController.java登录的控制层

@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        log.info("员工登录:{}", emp);
        Emp e = empService.login(emp);
        return e != null ? Result.success() : Result.error("用户名或密码错误");
    }
}

EmpService.java员工管理的service的接口

public interface EmpService {
	Emp login(Emp emp);
}

EmpServiceImpl.java员工 接口的实现类

@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;
	@Override
    public Emp login(Emp emp) {
        return empMapper.getByUsernameAndPassword(emp);
    }
}

EmpMapper.java员工的Mapper层

@Mapper
public interface EmpMapper {
	/**
     * 根据用户名和密码查询员工
     *
     * @param emp
     * @return
     */
    @Select("select * from emp where username = #{username}  and password = #{password}")
    Emp getByUsernameAndPassword(Emp emp);
}

1.5API测试

登录失败:

SpringBootWeb案例_03_第3张图片

登录成功:

SpringBootWeb案例_03_第4张图片

1.6前端联调

登录失败示例:

SpringBootWeb案例_03_第5张图片

后端返回数据

image-20231130170602828

登陆成功示例:

SpringBootWeb案例_03_第6张图片

登陆成功

SpringBootWeb案例_03_第7张图片

后端返回数据

SpringBootWeb案例_03_第8张图片

问题

在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。

SpringBootWeb案例_03_第9张图片

2.登录校验

思路

SpringBootWeb案例_03_第10张图片

登录标记

会话技术:用户登录成功之后,每一次请求中,都可以获取到该标记

统一拦截器

  • 过滤器:Filter
  • 拦截器:Interceptor

2.1会话技术

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
  • 会话跟踪方案:
    • 客户端会话跟踪技术:Cookie
    • 服务端会话跟踪技术:Session
    • 令牌技术

SpringBootWeb案例_03_第11张图片

2.2会话跟踪方案对比
2.2.1Cookie

SpringBootWeb案例_03_第12张图片

  • 优点:HTTP协议中支持的技术
  • 缺点:
    • 移动端APP无法使用Cookie
    • 不安全,用户可以自己禁用Cookie
    • Cookie不能跨域

跨域区分的三个维度:协议、IP/域名、端口

SpringBootWeb案例_03_第13张图片

示例

cookie

@Slf4j
@RestController
public class SessionController {
    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response) {
        response.addCookie(new Cookie("login_username", "Bowen"));//设置Cookie/响应Cookie
        return Result.success();
    }

    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();//获取所有的Cookie
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("login_username")) {//输出name为login_username的cookie
                System.out.println("login_username:" + cookie.getValue());
            }
        }
        return Result.success();
    }
}

cookie1

SpringBootWeb案例_03_第14张图片

SpringBootWeb案例_03_第15张图片

cookie2

SpringBootWeb案例_03_第16张图片

2.2.2Session

SpringBootWeb案例_03_第17张图片

  • 优点:存储在服务端,安全
  • 缺点:
    • 服务集群环境下无法直接使用Session
    • Cookie的所有缺点

服务集群环境

SpringBootWeb案例_03_第18张图片

示例

session

@Slf4j
@RestController
public class SessionController {
    @GetMapping("/s1")
    public Result session1(HttpSession 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());

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        log.info("loginUser: {}", loginUser);
        return Result.success(loginUser);
    }
}

session1

访问http://localhost:8080/s1,可以看到响应头多了一行数据

Set-Cookie: JSESSIONID=E6162850B6461136FB3B6E47141BE345; Path=/; HttpOnly

SpringBootWeb案例_03_第19张图片

并且Application中存储了该cookie

JSESSIONID=E6162850B6461136FB3B6E47141BE345代表服务器端session对象的ID

SpringBootWeb案例_03_第20张图片

session2

访问http://localhost:8080/s2可以看到请求头中的

Cookie: Idea-28469084=333be614-a4f0-4420-bb00-7ef0ad6f47ab; sidebarStatus=0; login_username=Bowen; JSESSIONID=E6162850B6461136FB3B6E47141BE345

SpringBootWeb案例_03_第21张图片

控制台输出的日志中,两次请求拿到的session是同一个,都是295124489

SpringBootWeb案例_03_第22张图片

2.2.3令牌技术(主流方案)

SpringBootWeb案例_03_第23张图片

  • 优点:

    • 支持PC端、移动端

    • 解决集群环境下的认证问题

    • 减轻服务器端存储压力

  • 缺点:需要自己实现

2.3JWT令牌

2.3.1JWT(将原始的JSON数据格式进行了安全的封装)

简介

  • 全称:JSON Web Token(https://jwt.io)
  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
  • 组成:
    • 第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}
    • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}
    • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

image-20231130204742116

Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式

2.3.2场景:登录认证
  1. 登录成功后,生成令牌
  2. 后续每个请求,都要携带JWT令牌,系统在每次请求处理前,先校验令牌,通过后,再处理
2.3.3JWT-生成

pom.xml引入依赖


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

生成令牌

@SpringBootTest
class TliasWebManagementApplicationTests {
    /**
     * 生成JWT令牌
     */
    @Test
    public void testGenJwt() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 1);
        claims.put("name", "Tom");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "Bowen")//签名算法
                .setClaims(claims)//自定义内容(荷载)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期1h
                .compact();
        System.out.println(jwt);
    }
}

SpringBootWeb案例_03_第24张图片

生成的令牌

eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTcwMTM1Mzk2NH0.Bn4qfOumxnzuwkFrBxGw3MQa4fdYf8rOCRRUbL02f1M

可以将令牌粘贴到官网进行解析:https://jwt.io/

SpringBootWeb案例_03_第25张图片

2.3.4JWT-校验
@SpringBootTest
class TliasWebManagementApplicationTests {
    /**
     * 解析JWT
     */
    @Test
    public void testPareJwt() {
        Claims claims = Jwts.parser()
                .setSigningKey("Bowen")
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTcwMTM1MzA3OX0.8dK3aG5KhIjmPd9rxGZ_QW0BXMhWacycD9-jhqW4GKg")
                .getBody();
        System.out.println(claims);
    }
}

SpringBootWeb案例_03_第26张图片

注意事项

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的
  • 如果JWT令牌解校验时报错,则说明JWT令牌被篡改失效了,令牌非法
2.3.5案例

思路

  • 令牌生成:登录成功后,生成JWT令牌,并返回给前端
  • 令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验
2.3.5.1接口文档:见1.2接口文档

步骤

  • 引入JWT令牌操作工具类
  • 登录完成后,调用工具类生成JWT令牌,并返回
2.3.5.2JwtUtils.java工具类
public class JwtUtils {

    private static String signKey = "itheima";
    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.3.5.3LoginController.java登录的控制层
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp) {
        log.info("员工登录:{}", emp);
        Emp e = empService.login(emp);

        //登录成功,生成令牌,下发令牌
        if (e != null) {
            Map<String, Object> 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("用户名或密码错误");
    }
}
2.3.5.4API测试

用户密码正确时

SpringBootWeb案例_03_第27张图片

用户或密码错误时

SpringBootWeb案例_03_第28张图片

2.3.5.5前后端联调

登录成功可以抓取去tlias_token的值eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTM5NjQyOH0.N9-lpBDXMI_Bj_Bhy0Y6WBx1EcsTxBPA0IDnVqUhMm4

SpringBootWeb案例_03_第29张图片

进入员工管理页面,可以找到与tlias_token中一样的token

SpringBootWeb案例_03_第30张图片

2.4过滤器Filter

过滤器Filter

概述

  • 概念:Filter过滤器是JavaWeb三大组件(Servlet、Filter、Listener)之一。
  • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
  • 过滤器一般完成一些通用的操作,比如:登录校验统一编码处理敏感字符处理等。

SpringBootWeb案例_03_第31张图片

2.4.1快速入门
  1. 定义Filter:定义一个类,实现Filter接口,并重写其所有方法。
  2. 配置Filter:Filter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@ServletComponentScan开启Servlet组件支持。

SpringBootWeb案例_03_第32张图片

DemoFilter.javaFilter类

@WebFilter(urlPatterns = "/*")//urlPatterns = "/*" 拦截所有请求
public class DemoFilter implements Filter {
    @Override//初始化方法只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("init 初始化方法执行了");
    }

    @Override//拦截到请求之后调用,调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("拦截到了请求");
        //放行
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override//销毁方法,只调用一次
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("destroy 销毁方法执行了");
    }
}

TliasWebManagementApplication.java启动类

@ServletComponentScan//开启了对servlet组件的支持
@SpringBootApplication
public class TliasWebManagementApplication {

    public static void main(String[] args) {
        SpringApplication.run(TliasWebManagementApplication.class, args);
    }

}

SpringBootWeb案例_03_第33张图片

SpringBootWeb案例_03_第34张图片

2.4.2详情(执行流程、拦截路径、过滤器链)
2.4.2.1执行流程

SpringBootWeb案例_03_第35张图片

  • 放行后访问对应资源,资源访问完成后,会回到Filter中
  • 如果回到Filter中,只执行放行后的逻辑
2.4.2.2拦截路径

执行流程搞清楚之后,接下来再来介绍一下过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

示例

拦截了/depts下的一级路径,(调试的时候,在放行处打断点)

@WebFilter(urlPatterns = "/depts/*")//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("拦截到了请求...放行前的逻辑");
        //放行
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("拦截到了请求...放行后的逻辑");
    }

    @Override//销毁方法,只调用一次
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}
2.4.2.3过滤器链

介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链

顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然顺序排列

SpringBootWeb案例_03_第36张图片

2.4.2.4示例
@WebFilter("/*")
public class AbcFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Abc拦截到了请求...放行前的逻辑");
        //放行
        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("Abc拦截到了请求...放行后的逻辑");
    }
}

API测试

SpringBootWeb案例_03_第37张图片

控制台输出日志

SpringBootWeb案例_03_第38张图片

2.4.3小结
  1. 执行流程
    • 请求==》放行前逻辑==》放行==》资源==》放行后逻辑
  2. 拦截路径
    • /login
    • /depts/*
    • /*
  3. 过滤器链
    • 一个web应用中,配置了多个过滤器,就形成了一个过滤器链
2.4.4登录校验-Filter
2.4.4.1需求分析

需要使用过滤器Filter来完成案例当中的登录校验功能。

SpringBootWeb案例_03_第39张图片

先来回顾下前面分析过的登录校验的基本流程:

  • 要进入到后台管理系统,必须先完成登录操作,此时就需要访问登录接口login。

  • 登录成功之后,会在服务端生成一个JWT令牌,并且把JWT令牌返回给前端,前端会将JWT令牌存储下来。

  • 在后续的每一次请求当中,都会将JWT令牌携带到服务端,请求到达服务端之后,要想去访问对应的业务功能,此时必须先要校验令牌的有效性。

  • 对于校验令牌的这一块操作,使用登录校验的过滤器,在过滤器当中来校验令牌的有效性。如果令牌是无效的,就响应一个错误的信息,也不会再去放行访问对应的资源了。如果令牌存在,并且它是有效的,此时就会放行去访问对应的web资源,执行相应的业务操作。

大概清楚了在Filter过滤器的实现步骤了,那在正式开发登录校验过滤器之前,思考两个问题:

  1. 所有的请求,拦截到了之后,都需要校验令牌吗?

    • 答案:登录请求例外
  2. 拦截到请求后,什么情况下才可以放行,执行业务操作?

    • 答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
2.4.4.2具体流程

Filter过滤器的流程步骤:

SpringBootWeb案例_03_第40张图片

基于上面的业务流程分析出具体的操作步骤:

  1. 获取请求url
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  3. 获取请求头中的令牌(token)
  4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  5. 解析token,如果解析失败,返回错误结果(未登录)
  6. 放行
2.4.4.3功能实现
引入阿里巴巴fastJSON依赖

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.83version>
dependency>
LoginCheckFilter.java登录过滤器
/**
 * @ClassName LoginCheckFilter
 * @Description 登录过滤器
 * @Author Bowen
 * @Date 2023/11/30 23:53
 * @Version 1.0
 **/
@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;
        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. 获取请求头中的令牌(token)
        String jwt = req.getHeader("token");

        //> 4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
        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,如果解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJWT(jwt);//选中代码块后ctrl+alt+t
        } catch (Exception e) {//解析失败
            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);
    }

}

注释掉DemoFilter.javaAbcFilter中的过滤器

2.4.4.4API测试

启动springboot,进行测试

将登录拿到的jwt令牌复制给查询部门的测试接口

令牌eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTQwNTY1M30.rT2wkW2OoY_UZz75A36ElRVvmQh5ivv214RzXZp3yHk

image-20231201004134781

添加请求头的token后,发送请求,获取到了数据

SpringBootWeb案例_03_第41张图片

若请求参数没有token,则返回JSON数据

SpringBootWeb案例_03_第42张图片

查看控制台日志

SpringBootWeb案例_03_第43张图片

进行前后端联调

登录后拿到 员工的urlhttp://localhost:90/#/system/emp,退出登录,复制该url到地址栏,直接转跳到登录页面。前后端联调成功~~~

SpringBootWeb案例_03_第44张图片

将拿到的url复制到地址栏

SpringBootWeb案例_03_第45张图片

直接跳转到登录页面,说明拦截器配置成功

SpringBootWeb案例_03_第46张图片

2.5拦截器Interceptor

2.5.1简介

概述

  • 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用于动态拦截控制器方法的执行。
  • 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

SpringBootWeb案例_03_第47张图片

2.5.2快速入门
  1. 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法
  2. 注册拦截器

image-20231201101127141

SpringBootWeb案例_03_第48张图片

2.5.2.1示例
定义拦截器

com.bowen包下创建interceptor.LoginCheckInterceptor.java

@Component
public class LoginCheckInterceptor implements 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....");
    }
}
配置拦截器

com.bowen包下创建config.WebConfig.java配置类

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
    }
}

注释掉之前的拦截器,启动该项目

API测试

使用登录接口测试

preHandle方法中返回值为true时放行。

SpringBootWeb案例_03_第49张图片

控制台日志

SpringBootWeb案例_03_第50张图片

preHandle方法中返回值为false时禁止放行。

SpringBootWeb案例_03_第51张图片

控制台日志

SpringBootWeb案例_03_第52张图片

2.5.3详解(拦截路径、执行流程)
2.5.3.1拦截路径
  • 拦截器可以根据需求,配置不同的拦截路径

image-20231201110845868

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /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/1

示例1

addPathPatterns("/**").excludePathPatterns("/login")

excludePathPatterns("/login")排除登录请求

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

API测试-登录接口

登录返回数据令牌成功,并拿到令牌:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTcwMTQ1MzM0OX0.EPBM2H_BPFi71Sl4cUs3rzYGuA9xCdsFTPUOxfgNgkY

SpringBootWeb案例_03_第53张图片

查看控制台日志,发现直接登录了没有被拦截

SpringBootWeb案例_03_第54张图片

API测试-查询部门接口

需要登录后拿到的令牌作为token

SpringBootWeb案例_03_第55张图片

查看控制台日志,发现查询部门时拦截后放行,拿到数据,再执行postHandle、afterCompletion方法

SpringBootWeb案例_03_第56张图片

API测试-删除部门接口

SpringBootWeb案例_03_第57张图片

查看控制台日志,发现删除部门时拦截后放行,更新数据,再执行postHandle、afterCompletion方法

SpringBootWeb案例_03_第58张图片

示例2

addPathPatterns("/*").excludePathPatterns("/login")

addPathPatterns("/*")拦截一级路径

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/*").excludePathPatterns("/login");
    }
}

API测试-查询部门

查询部门是一级路径,应该被拦截

SpringBootWeb案例_03_第59张图片

查看日志,查询部门请求被拦截

SpringBootWeb案例_03_第60张图片

API测试-删除部门

删除部门有二级路径,应该没有拦截

SpringBootWeb案例_03_第61张图片

查看控制台生成的日志

SpringBootWeb案例_03_第62张图片

2.5.3.2执行流程

SpringBootWeb案例_03_第63张图片

FilterInterceptor

接口规范不同:过滤器需要实现Fileter接口,而拦截器需要实现HandlerInterceptor接口。

拦截范围不同:过滤器Fileter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

2.5.3.3示例

打开DemoFilter.java中的过滤器,打开WebConfig.java中的拦截器,重新启动服务进行测试

API测试-查询部门

SpringBootWeb案例_03_第64张图片

查看控制台日志

SpringBootWeb案例_03_第65张图片

2.5.4登录校验-Interceptor
2.5.4.1流程图

流程图与Filter过滤器的流程完全一致

SpringBootWeb案例_03_第66张图片

基于上面的业务流程分析出具体的操作步骤:

  1. 获取请求url
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  3. 获取请求头中的令牌(token)
  4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  5. 解析token,如果解析失败,返回错误结果(未登录)
  6. 放行
2.5.4.2功能实现

WebConfig.java配置类,在config包下,其中的.excludePathPatterns("/login")可省略,因为在LoginCheckInterceptor.java//> 2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行,所以造成了重复判断,可删除该拦截器。

/**
 * @ClassName WebConfig
 * @Description 配置类
 * @Author Bowen
 * @Date 2023/12/1 10:20
 * @Version 1.0
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

LoginCheckInterceptor.java实现登录校验的拦截器

/**
 * @ClassName LoginCheckInterceptor
 * @Description 拦截器`Interceptor`演示
 * @Author Bowen
 * @Date 2023/12/1 10:13
 * @Version 1.0
 **/
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override//目标资源方法运行前,返回true:放行,返回false,不放行
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        System.out.println("preHandle....");

        //> 1. 获取请求url
        String url = req.getRequestURL().toString();
        log.info("请求的url:{}", url);

        //> 2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if (url.contains("login")) {
            log.info("登录操作,放行。。。");
            return true;
        }

        //> 3. 获取请求头中的令牌(token)
        String jwt = req.getHeader("token");

        //> 4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
        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,如果解析失败,返回错误结果(未登录)
        try {
            JwtUtils.parseJWT(jwt);//选中代码块后ctrl+alt+t
        } catch (Exception e) {//解析失败
            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....");
    }
}

注释掉filter包下的所有过滤器注解

启动服务进行测试

2.5.4.3API测试
API测试-登录

SpringBootWeb案例_03_第67张图片

API测试-查询部门(未登录)

SpringBootWeb案例_03_第68张图片

API测试-查询部门(已登录)

SpringBootWeb案例_03_第69张图片

查看日志

SpringBootWeb案例_03_第70张图片

前后端联调

启动nginx,不再具体演示,具体操作可参考2.4.4.4API测试

SpringBootWeb案例_03_第71张图片

3.异常处理

3.1问题引出

打开浏览器,访问系统中的新增部门操作,系统中已经有了 “就业部” 这个部门,再来增加一个就业部,看看会发生什么现象。

SpringBootWeb案例_03_第72张图片

点击确定之后,窗口关闭了,页面没有任何反应,就业部也没有添加上。 而此时会发现,网络请求报错了。

SpringBootWeb案例_03_第73张图片

响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据。

状态码为500,表示服务器端异常,打开idea,查看服务器端出了什么问题。在项目案例中没有进行异常处理

SpringBootWeb案例_03_第74张图片

上述错误信息的含义是,dept部门表的name字段的值 就业部 重复了,因为在数据库表dept中已经有了就业部,之前设计这张表时,为name字段建议了唯一约束,所以该字段的值是不能重复的。

而当再添加就业部,这个部门时,就违反了唯一约束,此时就会报错。

3.2全局异常处理器

SpringBootWeb案例_03_第75张图片

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

新建一个包exception,创建GlobalExceptionHandler.java全局异常处理器

/**
 * @ClassName GlobalExceptionHandler
 * @Description 全局异常处理器
 * @Author Bowen
 * @Date 2023/12/1 16:10
 * @Version 1.0
 **/
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)//捕获所有异常
    public Result ex(Exception ex) {
        ex.printStackTrace();
        return Result.error("对不起操作失败请联系管理员~~~");
    }
}

添加断点后进行调试,

SpringBootWeb案例_03_第76张图片

重新添加重复的部门

SpringBootWeb案例_03_第77张图片

点击确定后,进入控制层的断点,恢复程序进入下一个断点

SpringBootWeb案例_03_第78张图片

可以看到抛出异常

SpringBootWeb案例_03_第79张图片

前端返回异常弹窗

SpringBootWeb案例_03_第80张图片

3.3总结

全局异常处理器

  • @RestControllerAdvice
  • @ExceptionHandler

你可能感兴趣的:(JavaWeb,网络,开发语言,java,spring,boot)