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,前端拿到数据通过动态路由技术处理数据

posted @ 2022-08-05 15:38  yyybl  阅读(47)  评论(0)    收藏  举报