JavaWeb阶段学习知识点(二)

86 阅读18分钟

登录校验和JWT令牌实现

JWT使用方式

创建一个springboot项目,pom.xml引入jwt依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

       <!-- 针对jdk17
			或者报错内容为:java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter 的
			小伙伴加一下下面的依赖 -->
	   <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

在测试类中,定义一个测试方法,测试jwt令牌生成

package com.jwz;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class test {

    /**
     * 生成jwt
     */
    @Test
    void testSetJwt() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("name","xiaoji");
        /**
         * builder:用来构建jwt令牌
         * signWith: 生成jwt令牌使用的数字算法(jwt.io),官网有,参数一指定算法,参数二就是签名秘钥,这个随便写,但是切记不要少于5个字符,否则报错
         * setClaims:设置自定义数据(载荷)
         * setExpiration:设置令牌有效期,System.currentTimeMillis()+3600 当前时间+3600秒,也就是3600*1000毫秒后令牌过期,就是设置有效期为一小时
         * compact:调用compact可以拿到一个字符串类型的返回值
         */
        String jinweizhe = Jwts.builder().signWith(SignatureAlgorithm.HS256, "jinweizhe").setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 3600*1000)).compact();
        System.out.println("生成的jwt为: "+jinweizhe); // 这个打印的就是jwt令牌
        // 生成的jwt为: eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoieGlhb2ppIiwiaWQiOjEsImV4cCI6MTcxNjIxODU2OH0.4-yMVfNWyb87TFryq8FJTiH_AAXLsmYGOFVybyjK15g
    }


    /**
     * 解析jwt
     */
    @Test
    void testGetJwt(){
        /**
         * setSigningKey:指定签名秘钥
         * parseClaimsJws:传入jwt令牌
         * getBody:拿到自定义内容
         */
        Claims jinweizhe = Jwts.parser().setSigningKey("jinweizhe").parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoieGlhb2ppIiwiaWQiOjEsImV4cCI6MTcxNjIxODU2OH0.4-yMVfNWyb87TFryq8FJTiH_AAXLsmYGOFVybyjK15g").getBody();
        System.out.println("解析到的jwt为: "+jinweizhe);  // 解析到的jwt为: {name=xiaoji, id=1, exp=1716218568}
    }
}
  • JWT校验时使用的签名秘钥,必须和生成IWT令牌时使用的秘钥是配套的
  • 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改 或 失效了,令牌非法。

过滤器filter的使用操作

  • 定义Filter:定义一个类,实现 Filter 接口,并重写其所有方法。
  • 配置Filter:filter类上加 @WebFiter 注解,配置拦截资源的路径。引导类上加 @ServletComponentscan 开启Servlet组件支持,

登录校验流程

  • 获取请求url。
  • 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  • 获取请求头中的令牌(token)
  • 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
  • 解析token,如果解析失败,返回错误结果(未登录)
  • 放行。

在SpringBoot项目下,新建一个utils包和DemoFilter类

这里说明一下,下面用到的Result和JwtUtils都是工具包,下面有完整的项目代码,里面是有包含的,这里就不写出来了,可以往下翻找到完整项目代码

DemoFilter类内容如下

package com.jwz.login;


import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")  // 拦截所有的请求
// @WebFilter(urlPatterns = "/emps/*") // 访问emps下的所有资源的,都会被拦截
// @WebFilter(urlPatterns = "/login") // 拦截具体接口
public class DemoFilter implements Filter {
    // 还有init和destroy分别对应初始化方法和销毁方法,都只会调用一次,这两个不用重写,因为查看Filter源码会发现底层已经默认调用了,当然,想重写也可以
    // 这里只关注doFilter即可,他会在拦截到请求之后开始调用,会调用多次
    @Override // 拦截到请求之后调用,会调用多次,拦截到接口需要放行,否则接口不返回数据
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // System.out.println("拦截到请求了...放行之前的逻辑");
        // // 放行接口
        // // 参数1:请求对象  参数2:响应对象
        // // 放行后可以发现能正常返回数据了
        // filterChain.doFilter(servletRequest,servletResponse);
        // System.out.println("拦截到请求了...放行之后的逻辑");


        // 下面是登录校验的实现思路
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;

        // 将请求头和响应头都设置utf-8的格式,避免请求和响应结果有中文造成了乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json; charset=UTF-8");

        //1.获取请求ur1.
        String url = req.getRequestURL().toString();
        log.info("请求的url:{}",url);
        //2.判断请求url中是否包含login,如果包含,说明是获录操作,放行。
        if(url.contains("login")){
            log.info("登录操作,直接放行");
            filterChain.doFilter(servletRequest,servletResponse);
            return; // 停止代码继续向下执行
        }
        //3.获取请求头中的令牌(token)。
        String token = req.getHeader("token");
        //4.判断令牌是否存在、如果不存在,返回错误结果(未发录)
        if(!StringUtils.hasLength(token)){ // 判断字符串是否有长度
            log.info("请求头token为空,返回未登录信息");
            Result error = Result.error("未登录"); // 这里的Result是一个工具类,笔记下面的完整项目里面有工具文件代码
            // 手动转换 对象 -- json -----> 阿里巴巴fastJSON工具包(https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2/2.0.50)
            // 去上面地址复制代码到pom.xml依赖下载一下
            String noLogin = JSONObject.toJSONString(error); // 获取到json字符串(对象转成了json字符串)
            resp.getWriter().write(noLogin); // 将结果响应给浏览器
            return;
        }
        //5.解析token,如果解析失败,返回误结果(未录)
        // 这里的JwtUtils也是个工具类,跟上面的Result一样,下翻完整代码里面有工具类代码提供
        try {
            JwtUtils.parseJWT(token);
        } catch (Exception e) {
            // e.printStackTrace();
            log.info("令牌解析失败,返回未登录的错误信息");
            Result error = Result.error("未登录");
            String noLogin = JSONObject.toJSONString(error); // 获取到json字符串(对象转成了json字符串)
            resp.getWriter().write(noLogin); // 将结果响应给浏览器
            return;
        }
        //6.放行。
        log.info("令牌合法,直接放行");
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

启动类新增一个注解

package com.jwz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

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

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

}

测试用的controller

package com.jwz.login;

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;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class loginController {
    @PostMapping("/login")
    public Result login(@RequestBody loginEntity login){
        // 登陆成功,生成jwt并下发jwt返回给前端
            Map<String, Object> claims = new HashMap<>();
            claims.put("id",1);
            claims.put("name","xiaoji";
            // 只需要将参数传递给工具类即可(返回的jwt令牌里面里面包含了当前登录的员工信息)
            String s = JwtUtils.generateJwt(claims);
            return Result.success(s);
    }
    @PostMapping("/a")
    public Result a(){
        return Result.success("获取a成功");
    }
    @PostMapping("/b")
    public Result b(){
        return Result.success("获取b成功");
    }
}

然后启动项目即可,整个项目都会过这个过滤器的逻辑代码,所有的请求都会被处理,这里判断了如果是登录接口直接放行,否则其他的接口根据请求头是否含有token以及token是否过期来判断,如果非登录接口的情况下,headers有token并且token未过期才会继续访问下面的接口,否则统一返回错误信息

多个过滤器类执行顺序

  • 介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
  • 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序

拦截器的使用操作

项目下放入如下代码

package com.jwz.login;

import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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;
@Slf4j
@Component // 需要交给ioc容器管理
public class LoginCheckInterceptor implements HandlerInterceptor {
    // 按下ctrl+o重写里面所有的方法

    @Override  // 目标资源方法运行前运行,返回true 放行  返回false 不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=UTF-8");


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

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

        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("未登录");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {//jwt解析失败
            // e.printStackTrace();
            log.info("解析令牌失败, 返回未登录错误信息");
            Result error = Result.error("未登录");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            response.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...");
    }
}

在当前项目里面再新建一个配置类,用于添加我们上面创建的拦截器

package com.jwz.login;

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 WebCongfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor; // 注入我们刚刚配置的拦截器
    @Override  // 重写方法用来注册拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器

        // addPathPatterns指定拦截器的配置规则
        /**
         * 1. /* 一级路径 能匹配/depts, /emps, /login 不能匹配/depts/1 等多级路径
         * 2. /** 代表拦截所有接口 能匹配/depts 也能匹配/depts/1 也能匹配depts/1/1
         * 3. /depts/*  depts下的一级路径  能匹配/depts/1  不能匹配/depts/1/2 也不能匹配/depts
         * 4. /depts/**  depts下的任意路径,可以匹配多个路径 能匹配/depts/1  也能匹配/depts/1/2
         */

        // excludePathPatterns 配置不拦截哪些接口
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

接口还是用上面的,直接启动项目,会发现我们的配置都生效了,login接口没有被拦截,被排除了,其他接口都经过了拦截器(通过打印语句得出)

项目里面进行jwt操作

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

步骤

  • 引入JWT令牌操作工具类JwtUitls。
  • 登录完成后,调用工具类生成JWT令牌,并返回

建表语句

-- 部门管理
create table dept(
    id int unsigned primary key auto_increment comment '主键ID',
    name varchar(10) not null unique comment '部门名称',
    create_time datetime not null comment '创建时间',
    update_time datetime not null comment '修改时间'
) comment '部门表';

insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()), (4,'就业部',now(),now()),(5,'人事部',now(),now());



-- 员工管理(带约束)
create table emp (
  id int unsigned primary key auto_increment comment 'ID',
  username varchar(20) not null unique comment '用户名',
  password varchar(32) default '123456' comment '密码',
  name varchar(10) not null comment '姓名',
  gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
  image varchar(300) comment '图像',
  job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
  entrydate date comment '入职时间',
  dept_id int unsigned comment '部门ID',
  create_time datetime not null comment '创建时间',
  update_time datetime not null comment '修改时间'
) comment '员工表';

INSERT INTO emp
	(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
	(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
	(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
	(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
	(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
	(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
	(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
	(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
	(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
	(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
	(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
	(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
	(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
	(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
	(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
	(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
	(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
	(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now()); 

创建项目和之前员工管理创建方式一样,因为本质上这属于一个项目,但是登录校验涉及技术有点多,单独抽出来做个demo,下面是目录结构

在这里插入图片描述

application.properties内容如下

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user
spring.datasource.username=root
spring.datasource.password=admin
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true

pom.xml引入下面两个依赖

		<!--JWT令牌-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!--fastJSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

JwtUitls工具包

package com.jwz.login;

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 = "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;
    }
}

LoginCheckInterceptor

package com.jwz.login;

import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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;
@Slf4j
@Component // 需要交给ioc容器管理
public class LoginCheckInterceptor implements HandlerInterceptor {
    // 按下ctrl+o重写里面所有的方法

    @Override  // 目标资源方法运行前运行,返回true 放行  返回false 不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=UTF-8");


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

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

        //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("未登录");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        //5.解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {//jwt解析失败
            // e.printStackTrace();
            log.info("解析令牌失败, 返回未登录错误信息");
            Result error = Result.error("未登录");
            //手动转换 对象--json --------> 阿里巴巴fastJSON
            String notLogin = JSONObject.toJSONString(error);
            response.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...");
    }

}

WebCongfig

package com.jwz.login;

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 WebCongfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor; // 注入我们刚刚配置的拦截器
    @Override  // 重写方法用来注册拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器

        // addPathPatterns指定拦截器的配置规则
        /**
         * 1. /* 一级路径 能匹配/depts, /emps, /login 不能匹配/depts/1 等多级路径
         * 2. /** 代表拦截所有接口 能匹配/depts 也能匹配/depts/1 也能匹配depts/1/1
         * 3. /depts/*  depts下的一级路径  能匹配/depts/1  不能匹配/depts/1/2 也不能匹配/depts
         * 4. /depts/**  depts下的任意路径,可以匹配多个路径 能匹配/depts/1  也能匹配/depts/1/2
         */

        // excludePathPatterns 配置不拦截哪些接口
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

loginEntity

package com.jwz.login;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class loginEntity {
    private Integer id;
    private String username; //用户名
    private String password; //密码
}

Result

package com.jwz.login;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应信息 描述字符串
    private Object data; //返回的数据

    //增删改 成功响应
    public static Result success(){
        return new Result(1,"success",null);
    }
    //查询 成功响应
    public static Result success(Object data){
        return new Result(1,"success",data);
    }
    //失败响应
    public static Result error(String msg){
        return new Result(0,msg,null);
    }
}

loginMapper

package com.jwz.login;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

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

loginMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jwz.login.loginMapper">

</mapper>

loginService

package com.jwz.login;

public interface loginService {
    loginEntity login(loginEntity login);
}

loginServiceImpl

package com.jwz.login;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class loginServiceImpl implements loginService{
    @Autowired
    private loginMapper loginMapper;

    /*
     * @param login 员工登录
     * @return
     */
    @Override
    public loginEntity login(loginEntity login) {
        return loginMapper.getByUsernameAndPassword(login);
    }
}

loginController

package com.jwz.login;

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;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class loginController {
    @Autowired
    private loginServiceImpl loginServiceimpl;
    @PostMapping("/login")
    public Result login(@RequestBody loginEntity login){
        // log.info("员工登录:{}",login);
        loginEntity l = loginServiceimpl.login(login);
        // 登陆成功,生成jwt并下发jwt返回给前端
        if(l!=null){
            Map<String, Object> claims = new HashMap<>();
            claims.put("id",l.getId());
            claims.put("name",l.getUsername());
            // 只需要将参数传递给工具类即可(返回的jwt令牌里面里面包含了当前登录的员工信息)
            String s = JwtUtils.generateJwt(claims);
            return Result.success(s);
        }else{
            return Result.error("用户名和密码错误");
        }
    }

    @PostMapping("/a")
    public Result a(){
        return Result.success("获取a成功");
    }

    @PostMapping("/b")
    public Result b(){
        return Result.success("获取b成功");
    }
}

全局异常处理器

package com.jwz.login;


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("对不起,操作失败,请联系管理员");
    }
}

直接放到项目里面就好,当项目报错500会返回这个统一管理的错误信息

事务管理

  • 注解:@Transactional
  • 位置:业务(service)层的方法上、类上、接口上
  • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务,成功执行完毕,提交事务;出现异常,回滚事务

在application.properties里面加入开启事务管理日志代码

logging.level.org.springframework.jdbc.support.JdbcTransactionManager:debug

这里以ServiceImpl业务操作层为例

package com.itheima.service.impl;

import com.itheima.mapper.DeptMapper;
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Dept;
import com.itheima.pojo.DeptLog;
import com.itheima.service.DeptLogService;
import com.itheima.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    @Autowired
    private EmpMapper empMapper;

    @Transactional // 加上这个注解,如果里面出错了,会进行事务回滚
    @Override
    public void delete(Integer id) throws Exception {
            deptMapper.deleteById(id); //根据ID删除部门数据
            int i = 1/0;
            //if(true){throw new Exception("出错啦...");}
            empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
    }
}

AOP进阶

项目的pom.xml加入以下AOP的依赖代码

        <!--AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

传统AOP的写法

package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect //AOP类
public class TimeAspect {
    // * com.jwz.service.*(..) 代表运行项目的com.jwz.service包下所有的接口或者类当中所有的方法时都会被匹配到
    // @Around("execution(* com.jwz.service.*(..))") //切入点表达式
    @Around("com.jwz.aop.MyAspect1.pt()")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1. 记录开始时间
        long begin = System.currentTimeMillis();

        //2. 调用原始方法运行
        Object result = joinPoint.proceed();

        //3. 记录结束时间, 计算方法执行耗时
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin);

        return result;
    }

}

AOP核心概念

在这里插入图片描述

通知类型

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行

项目新增如下类

package com.jwz.login;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
@Order(1) // 参数是数字,用于控制当有多个切面类时的切面类的执行顺序 ,数字越小越先执行
public class MyAspect1 {
 
    // * com.jwz.service.impl.DeptServiceImpl.*(..)) 代表com.jwz.service.impl.DeptServiceImpl包下面所有的方法在运行时都会被匹配到
    @Pointcut("execution(* com.jwz.service.impl.DeptServiceImpl.*(..))")
    public void pt(){}

    // 前置通知
    // @Before("pt()") 等同于 @Before("execution(* com.jwz.service.impl.DeptServiceImpl.*(..))")
    // 调用pt()相当于直接调用* com.jwz.service.impl.DeptServiceImpl.*(..))
    @Before("pt()")
    public void before(){
        log.info("before ...");
    }

    // 环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();

        log.info("around after ...");
        return result;
    }

    // 后置通知
    @After("pt()")
    public void after(){
        log.info("after ...");
    }

    // 目标方法正常返回后通知,出现异常不会执行
    @AfterReturning("pt()")
    public void afterReturning(){
        log.info("afterReturning ...");
    }

    // 异常后通知
    @AfterThrowing("pt()")
    public void afterThrowing(){
        log.info("afterThrowing ...");
    }
}

注意点:

  • @Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。

切入点表达式

切入点表达式:描述切入点方法的一种表达式 作用:主要用来决定项目中的哪些方法需要加入通知 常见形式:

  • execution(...):根据方法的签名来匹配
  • @annotation(...):根据注解匹配
@Before ("execution (public void com.jwz.service.impl.DeptServiceImpl.delete(java.lang.Integer))"
public void before(JoinPoint joinPoint){}
         
@Before("@annotation(com.jwz.anno.Log)")
public void before(){}

execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如:public、protected)
  • 包名.类名: 可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

@annotation的使用

新建MyLog.java注解

package com.jwz.login;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 描述注解运行时有效
@Retention(RetentionPolicy.RUNTIME)
// 标识注解只作用于方法上面
@Target(ElementType.METHOD)
public @interface MyLog {
}

在Impl的实现类方法上面加上注解

package com.jwz.login;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class loginServiceImpl implements loginService{
    @Autowired
    private loginMapper loginMapper;

    /*
     * @param login 员工登录
     * @return
     */
    @Override
    @MyLog
    public loginEntity login(loginEntity login) {
        return loginMapper.getByUsernameAndPassword(login);
    }
}

在切面类中实现

package com.jwz.login;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class MyAspect1 {

    /**
     * 参数是刚刚自定义注解的全类名
     */
    @Pointcut("@annotation(com.jwz.login.MyLog)")
    public void pt(){}

    @Before("pt()")
    public void before(){
        log.info("before ...");
    }
}

上述方法就不需要切入点表达式指定范围了,而是使用自定义注解标记方法然后再通过@annotation找到这个注解所标记的方法,后续如果想新增方法,只需要在对应方法上面加上@MyLog即可

连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等

  • 对于 @Around 通知,获取连接点信息只能使用ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型

切面类代码

package com.jwz.login;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Slf4j
@Component
@Aspect
public class MyAspect1 {

    /**
     * 参数是刚刚自定义注解的全类名
     */
    @Pointcut("@annotation(com.jwz.login.MyLog)")
    public void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");
        // 这个joinpoint和下面的使用是一致的
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around before...");

        // 获取目标对象类名
        String name = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}",name);
        // 获取目标方法的方法名
        String name1 = joinPoint.getSignature().getName();
        log.info("目标对象的方法名:{}",name1);
        // 获取目标方法运行时传入的参数
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数:{}", Arrays.toString(args));
        // 放行 目标方法执行
        Object proceed = joinPoint.proceed();
        // 获取 目标方法运行的返回值
        log.info("目标方法运行的返回值:{}",proceed);

        log.info("around after...");
        return null;
    }
}