JWT方案实现代码
添加依赖
<!--jwttoken加解密工具类依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
一:生成公钥/私钥工具类
package cn.ybl.basic.JWT;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
//Rsa公钥,私钥工具类:
/**
* RSA工具类 负责对RSA密钥的创建、读取功能(公钥和私钥)
*/
public class RsaUtils {
private static final int DEFAULT_KEY_SIZE = 2048; // 生成的大小
/**
* 从文件中读取公钥
* @param filename 公钥保存路径
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取私钥
* @param filename 私钥保存路径
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) {
try{
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 获取私钥
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
*/
public static void generateKey(String publicKeyFilename,
String privateKeyFilename,
String secret, int keySize) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
//修改自己的工作空间地址:ybl为密钥,可以随便写
public static void main(String[] args) throws Exception{
//1 生成秘钥对:公钥文件xxx_rsa.pub,私钥文件xxx_rsa.pri
generateKey("D:\\JavaSoft\\IdeaWorkSpace\\pet-home\\src\\main\\resources\\auth_rsa.pub",
"D:\\JavaSoft\\IdeaWorkSpace\\pet-home\\src\\main\\resources\\auth_rsa.pri","ybl",2048);
}
}
二:JWT加密解密工具类
package cn.ybl.basic.JWT;//JWT密钥的解析和加密工具类:
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.UUID;
/**
* JWT 密钥的解析和加密 工具类
*/
public class JwtUtils {
private static final String JWT_PAYLOAD_USER_KEY = "user";
private static String createJTI() {
return new String(Base64.getEncoder()
.encode(UUID.randomUUID().toString().getBytes()));
}
/**
* 私钥加密token
*
* @param loginData 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位分钟
* @return JWT
*/
public static String generateTokenExpireInMinutes(Object loginData, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JSONObject.toJSONString(loginData))
.setId(createJTI())
//当前时间往后加多少分钟
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(SignatureAlgorithm.RS256,privateKey)
.compact();
}
/**
* 私钥加密token
*
* @param loginData 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateTokenExpireInSeconds(Object loginData, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JSONObject.toJSONString(loginData))
.setId(createJTI())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(SignatureAlgorithm.RS256,privateKey)
.compact();
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws<Claims>
*/
private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
T t = JSONObject.parseObject(body.get(JWT_PAYLOAD_USER_KEY).toString(),userType);
claims.setLoginData(t);
claims.setExpiration(body.getExpiration());
return claims;
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserToken(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
//==================================================以下全部都为测试内容==================================================
public static void main(String[] args) throws Exception {
//1 获取token
PrivateKey privateKey = RsaUtils.getPrivateKey(JwtUtils.class.getClassLoader().getResource("auth_rsa.pri").getFile());
System.out.println(privateKey);
//使用私钥加密
String token = generateTokenExpireInSeconds(new User(1L, "zs"), privateKey, 10);
System.out.println(token);
// 2 解析token里面内容
PublicKey publicKey = RsaUtils.getPublicKey(JwtUtils.class.getClassLoader().getResource("auth_rsa.pub").getFile());
//使用公钥解密
Payload<User> payload = getInfoFromToken(token, publicKey, User.class);
System.out.println(payload);
Thread.sleep(11000); //超时后继续解析
payload = getInfoFromToken(token, publicKey, User.class);
System.out.println(payload);
}
static class User{
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public User() {
}
public User(Long id, String name) {
this.id = id;
this.name = name;
}
}
}
三:载荷类
package cn.ybl.basic.JWT;//载荷数据:
import java.util.Date;
public class Payload<T> {
private String id; // jwt的id(token) - 参考JwtUtils
private T loginData; // 用户信息:用户数据,不确定,可以是任意类型
private Date expiration; // 过期时间 - 参考JwtUtils
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public T getLoginData() {
return loginData;
}
public void setLoginData(T loginData) {
this.loginData = loginData;
}
public Date getExpiration() {
return expiration;
}
public void setExpiration(Date expiration) {
this.expiration = expiration;
}
@Override
public String toString() {
return "Payload{" +
"id='" + id + '\'' +
", loginData=" + loginData +
", expiration=" + expiration +
'}';
}
}
四:创建用户信息对象,即需要传给前端的对象,对应载荷中的第二个字段
package cn.ybl.user.JwtData;
import cn.ybl.system.domain.Menu;
import cn.ybl.user.domain.LoginInfo;
import lombok.Data;
import java.util.List;
/**
* @Author Mr.Yang
* @createTime 2022/8/4 12:11
* @Describe 后端登录成功封装数据对象
*/
@Data
public class LoginData {
//登录用户信息
private LoginInfo loginInfo;
//权限资源集合
private List<String> permissions;
//菜单资源集合
private List<Menu> menus;
}
五:封装JWT加密方法
//封装一个JWT加密信息的方法
private Map<String , Object> loginSuccessJwtHandler(LoginInfo loginInfo){
//创建LoginData对象,此LoginData为上面创建的那个,用于私钥加密
LoginData loginData = new LoginData();
loginData.setLoginInfo(loginInfo);
loginInfo.setPassword(null);
loginInfo.setSalt(null);
HashMap<String, Object> map = new HashMap<>();
try {
//如果当前登录用户是管理员
if(loginInfo.getType()==0){
//获取当前登录用户的所有权限
List<String> permission = employeeMapper.loadPerAuthrizeByLogininfoId(loginInfo.getId());
//获取当前用户的所有菜单权限
List<Menu> menu = employeeMapper.loadMenusByLogininfoId(loginInfo.getId());
loginData.setMenus(menu);
loginData.setPermissions(permission);
//将当前登陆人的权限和菜单添加到map - 响应给前端
map.put("permissions",permission);
map.put("menus",menu);
}
//获取私钥
PrivateKey privateKey = RsaUtils.getPrivateKey
(RsaUtils.class.getClassLoader().getResource("auth_rsa.pri").getFile());
//将loginData登录对象信息加密,30分钟有效
String jwtToken = JwtUtils.generateTokenExpireInMinutes(loginData, privateKey, 30);
//封装进Map返回
map.put("token",jwtToken);
map.put("logininfo",loginInfo);
return map;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
六:替换代码【将原来创建随机字符串往Redis中存,创建map返回信息的代码替换成这个方法即可】
//map此时包含了jwtToken和logininfo、permissions、menus
Map<String, Object> map = loginSuccessJwtHandler(loginInfo);
七:我们之前的SpringMVC拦截器使用的是无状态token方案,每次请求都根据前端请求头传的token到Redis中取值,现在我们在登录业务中去除了Redis,所以拦截器的判断方式也需要改变,先获取公钥,然后调用JWT工具类方法传入token,公钥,以及签证【loginData的字节码】进行解密,返回一个Payload载荷对象,获取对象信息,为后端用户就要通过HandlerMethod对象获取自定义注解,查询当前用户的权限集,判断权限集中是否包含这个注解上的方法【sn】
package cn.ybl.basic.interceptor;
import cn.ybl.basic.JWT.JwtUtils;
import cn.ybl.basic.JWT.Payload;
import cn.ybl.basic.JWT.RsaUtils;
import cn.ybl.org.mapper.EmployeeMapper;
import cn.ybl.system.annotation.PreAuthorize;
import cn.ybl.user.JwtData.LoginData;
import cn.ybl.user.domain.LoginInfo;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.PublicKey;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Author Mr.Yang
* @createTime 2022/7/30 14:49
* @Describe 拦截器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private EmployeeMapper employeeMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的数据
String token = request.getHeader("token");
if(token!=null){
//获取公钥
PublicKey publicKey = RsaUtils.getPublicKey(RsaUtils.class.getClassLoader().getResource("auth_rsa.pub").getFile());
try {
//解密 因为如果token超过时限失效了解密将会报错,所以需要try
Payload<LoginData> payload = JwtUtils.getInfoFromToken(token, publicKey, LoginData.class);
if(payload!=null){ //说明没过期
//获取登录对象的信息
LoginInfo loginInfo = payload.getLoginData().getLoginInfo();
if(loginInfo.getType().intValue()==1){ //用户放行
return true;
}
//程序运行到这儿,说明是后端用户
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取当前请求/接口/controller中方法上的权限信息【判断否有自定义注解@PreAuthorize】
PreAuthorize annotation = handlerMethod.getMethodAnnotation(PreAuthorize.class);
if(annotation == null){ //说明没有自定义注解,也就是公共资源,直接放行
return true;
}else{ //说明是受限资源,进行校验
//获取注解上的sn值
String sn = annotation.sn();
//查询当前登录用户的权限集
List<String> list = employeeMapper.loadPerAuthrizeByLogininfoId(loginInfo.getId());
if(list.contains(sn)){ //如果集合中包含当前sn - 说明有权限访问 - 放行
return true;
}else{
//程序运行到这儿,说明当前用户无权限
response.setContentType("application/json;charset=utf-8");
response.getWriter().println("{\"success\":false,\"msg\":\"noPermission\"}");
return false;
}
}
}
}
//登录超时异常
catch(ExpiredJwtException e){
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println("{\"success\":false,\"msg\":\"timeout\"}");
return false;
}
}
//请求头中没有数据或者登录信息过期了==============进行拦截
//手动拼接一个json数据返回给前端
response.setContentType("application/json;charset=utf8");
response.getWriter().write("{\"success\":false,\"msg\":\"nologin\"}");
return false;
}
}
到此后端功能完成,后端向前端共传递了jwtToken、logininfo、权限集permission、菜单集menus,前端拿到数据通过动态路由技术处理数据