JWT令牌
员工登录生成令牌
加载jwtProperties类(该类从配置文件中读取sky.jwt的值),然后调用类中的变量作为
JwtUtil.createJWT()方法的参数,返回一个token作为令牌,后续请求需要携带令牌进行身份验证才能执行业务逻辑。
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
//配置属性类
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
配置文件:
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
jwt令牌校验的拦截器
一般执行过程为
- 客户端请求: 客户端向服务端发送请求,该请求可能包含 JWT 令牌,通常是在请求的头部(HTTP Header)中或作为请求参数。
- 拦截器调用阶段: 拦截器通常在请求到达控制器(Controller)之前被调用。在 Spring 框架中,你可以使用HandlerInterceptor 或 Spring Security 中的相关拦截器来实现 JWT 令牌校验。本项目使用的是HandlerInterceptor拦截器
- JWT 令牌校验: 在拦截器中,你可以编写代码来提取请求中的 JWT令牌,然后对其进行校验。校验的过程包括验证令牌的签名是否有效、是否过期,以及其他自定义的校验规则。
- 通过或拒绝请求: 如果 JWT 令牌校验通过,拦截器可以选择允许请求继续到达控制器;否则,它可以拒绝请求,返回相应的错误信息。
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//当前线程的id
System.out.println("当前线程的id:" + Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//存储登录用户的令牌
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
拦截器会调用JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);使用配置文件中的秘钥进行解密和身份核实。
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
使用ThreadLocal从令牌中储存当前登录用户的id,用于后续操作。
BaseContext.setCurrentId(empId);
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外 则不能访问。
ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量
客户端发送的每次请求,后端的Tomcat服务器都会分配一个单独的线程来处理请求
PageHelper插件
PageHelper是一个用于MyBatis的分页插件,它简化了在数据库查询中进行分页操作的过程。
使用PageHelper,你可以很容易地在MyBatis查询中加入分页功能,而不需要手动编写复杂的分页逻辑。
注:分页查询插件Pagehelper 基于Mybatis拦截器实现 动态拼接sql
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10; 从第一条开始查我一共查十条
//使用分页查询插件pagehelper 基于Mybatis拦截器实现 动态拼接sql
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total , records);
}
xml文件
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test= " name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
从图中可以看到当我发起员工分页查询请求,就会先进行jwt校验,然后PageHelper会自动在sql语句后面拼接分页查询条件。
密码处理
当进行根据id查询员工信息时(信息回显),需要加密密码,提高安全性
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
//对密码进行处理
employee.setPassword("****");
return employee;
}
日期格式
如果不设置日期格式,则在传递给前端日期数据时会出现以下情况:
解决方法:
方式一:在属性上加入注解,对日期进行格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
使用@JsonFormat注解需要引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.2</version>
</dependency>
方式二:在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
//消息转换器
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转化器可以Java对象序列为JSON序列
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转换器加入到容器中
converters.add(0, converter);
//需要设置优先使用我们的转换器
}
第一个方法需要在用到日期属性的地方都加入这个注解,第二个方法只需要设置一次就好