回顾
在之前的系统中,我们利用UUID
配合Redis
以达到角色登录的功能。
当前整个系统存在一个问题:人为修改token值
后,用户仍然能在前端进行数据库操作,后台没有校验当前用户token
就允许一些请求,导致系统存在安全漏洞
。
解决方法:Jwt签名验证
。整合Jwt
后,前端发出的请求后端会先进行token验证
,然后执行操作。
整合Jwt
的效果如下:找到token
值,然后进行修改
在token
前加上值123,保存后进行一些操作
此时点击页面修改按钮,会弹出token错误
的信息
后端也会记录token错误
的信息
现在开始来实现这个功能
添加依赖
Jwt依赖
在pom文件中添加下述依赖
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Jwt配置
在common
文件夹下新建一个文件夹utils
,然后新建java
文件JwtUtil
写上下述代码,注释已标出
package com.ums.common.utils;
import com.alibaba.fastjson2.JSON;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
// 有效期
private static final long JWT_EXPIRE = 30*60*1000L; //半小时, 单位为毫秒
// 令牌秘钥
private static final String JWT_KEY = "123456";
// Object data 可放入User对象,给User中的信息加密后成为token
public String createToken(Object data){
// 当前时间
long currentTime = System.currentTimeMillis();
// token过期时间
long expTime = currentTime+JWT_EXPIRE;
// 构建jwt
JwtBuilder builder = Jwts.builder()
.setId(UUID.randomUUID()+"")
.setSubject(JSON.toJSONString(data)) // User对象序列化
.setIssuer("system")
.setIssuedAt(new Date(currentTime))
.signWith(SignatureAlgorithm.HS256, encodeSecret(JWT_KEY)) // 加密
.setExpiration(new Date(expTime));
return builder.compact();
}
// 加密算法
private SecretKey encodeSecret(String key){
byte[] encode = Base64.getEncoder().encode(key.getBytes());
SecretKeySpec aes = new SecretKeySpec(encode, 0, encode.length, "AES");
return aes;
}
// token 解密
public Claims parseToken(String token){
Claims body = Jwts.parser()
.setSigningKey(encodeSecret(JWT_KEY))
.parseClaimsJws(token)
.getBody();
return body;
}
// token 解密,并返回一个对象,可是User对象
public <T> T parseToken(String token,Class<T> clazz){
Claims body = Jwts.parser()
.setSigningKey(encodeSecret(JWT_KEY))
.parseClaimsJws(token)
.getBody();
return JSON.parseObject(body.getSubject(),clazz);
}
}
定义Jwt拦截器
在XAdminApplication
同级目录下新建文件夹interceptor
,再新建java文件JwtValidateInterceptor
文件中写入以下代码,注释已给出
package com.ums.interceptor;
import com.alibaba.fastjson2.JSON;
import com.ums.common.utils.JwtUtil;
import com.ums.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// HandlerInterceptor继承该接口,然后重写方法
@Component
@Slf4j
public class JwtValidateInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// X-Token 是前端定义的token标头,与前端保持一致
String token = request.getHeader("X-Token");
log.debug(request.getRequestURI() +"需要验证:"+ token); // 后台日志记录
if (token != null){
try {
jwtUtil.parseToken(token);
// 不要写System.out.println(); 此为垃圾代码
// 加上注解@Slf4j , 用log.debug()来打印
log.debug(request.getRequestURI() +"验证通过:");
return true;
}catch (Exception e) {
e.printStackTrace();
}
}
log.debug(request.getRequestURI() +"验证失败,禁止访问"); // 后台日志记录
// 创建一个返回对象,当token错误后反馈给前端
Result<Object> fail = Result.fail(20003, "token无效,请重新登录");
// 验证不成功,给前端返回数据
response.setContentType("application/json;charset=utf-8"); // 定义返回数据格式
response.getWriter().write(JSON.toJSONString(fail)); // 将对象序列化后以json格式反馈至前端
return false; // 拦截当前用户的操作
}
}
注册Jwt拦截器,配置需要验证token的URL
在config
目录下新建java文件MyInterceptorConfig
写入以下代码
package com.ums.config;
import com.ums.interceptor.JwtValidateInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Autowired
private JwtValidateInterceptor iwtValidateInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(iwtValidateInterceptor);
registration.addPathPatterns("/**") // 拦截所有URL请求
.excludePathPatterns( // 开放下述URL请求
"/user/login",
"/user/info",
"/user/logout"
);
}
}
自此,Jwt
就算配置完毕
总共新建下述三个文件
测试Jwt
新建一个测试类JwtUtilsTest
@Autowired
private JwtUtil jwtUtil;
@Test
public void testCreateJwt(){
User user = new User();
user.setUsername("anthony");
user.setPhone("14766665555");
String token = jwtUtil.createToken(user);
System.out.println(token);
}
运行testCreateJwt()
,系统会打印出一个加密后的字符串,此串会作为token
使用。
将这个字符串复制
新建一个解密的测试方法testParseJwt()
,下述代码中复制你自己的token
@Test
public void testParseJwt(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwNmRlOGJmOS1kYmM1LTQzNjUtYWRmYi0yYzBjMmVmM2FkOGYiLCJzdWIiOiJ7XCJwaG9uZVwiOlwiMTQ3NjY2NjU1NTVcIixcInVzZXJuYW1lXCI6XCJhbnRob255XCJ9IiwiaXNzIjoic3lzdGVtIiwiaWF0IjoxNjkwMjQ4MjY1LCJleHAiOjE2OTAyNTAwNjV9.iskJNmm6b6rDFs1oxsinrCdFmul0dd9-4_zswD6eGV0";
Claims claims = jwtUtil.parseToken(token);
System.out.println(claims);
}
运行后可得到加密的信息
因为我们是将一个对象整体进行加密,所以希望在解密的时候还原为一个对象
此时代码可这样写
@Test
public void testParseJw2t(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwNmRlOGJmOS1kYmM1LTQzNjUtYWRmYi0yYzBjMmVmM2FkOGYiLCJzdWIiOiJ7XCJwaG9uZVwiOlwiMTQ3NjY2NjU1NTVcIixcInVzZXJuYW1lXCI6XCJhbnRob255XCJ9IiwiaXNzIjoic3lzdGVtIiwiaWF0IjoxNjkwMjQ4MjY1LCJleHAiOjE2OTAyNTAwNjV9.iskJNmm6b6rDFs1oxsinrCdFmul0dd9-4_zswD6eGV0";
User user = jwtUtil.parseToken(token,User.class);
System.out.println(user);
}
运行后得到一个对象
修改登录等逻辑
现在有了Jwt
签名验证机制,可将之前的UUID + redis
登录逻辑进行修改
打开UserServiceImpl
文件
将之前写的login(User user)
、getUserInfo(String token)
、logout(String token)
这三段函数全部重写
login(User user)
@Autowired private JwtUtil jwtUtil; @Override public Map<String, Object> login(User user) { // 根据用户名查询 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, user.getUsername()); User loginUser = this.baseMapper.selectOne(wrapper); // 结果不为空,并且密码与数据库解密后的密码匹配,生成token,将用户信息存入redis if (loginUser != null && passwordEncoder.matches(user.getPassword(), loginUser.getPassword()) // 匹配加密密码 ) { loginUser.setPassword(null); // 设置密码为空,密码没必要放入 // 创建jwt String token = jwtUtil.createToken(loginUser); // 返回数据 Map<String, Object> data = new HashMap<>(); data.put("token",token); return data; } // 结果不为空,生成token,前后端分离,前端无法使用session,可以使用token return null; }
getUserInfo(String token)
@Override public Map<String, Object> getUserInfo(String token) { // 之前已将对象进行序列化处理存入redis,现在从redis中取出需要反序列化处理 // Object obj = redisTemplate.opsForValue().get(token); // 此对象是map类型,稍后需要序列化为Json字符串 // User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class); User loginUser = null; try { // 解析Token loginUser = jwtUtil.parseToken(token, User.class); }catch (Exception e) { e.printStackTrace(); } if (loginUser != null) { Map<String,Object> data = new HashMap<>(); data.put("name",loginUser.getUsername()); data.put("avatar",loginUser.getAvatar()); // 先在xml里写SQL语句 id=getRoleNameByUserId,然后去UserMapper里实现接口 List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId()); data.put("roles",roleList); return data; } return null; }
logout(String token)
, 注释以前的代码,可啥也不用写!@Override public void logout(String token) { // redisTemplate.delete(token); // 从redis中删除token }
自此完毕!