登录校验和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;
}
}