JWT&RSA實現單點登錄(詳細介紹)


今天給大家講一下基於JWT&RSA的單點登錄(Single Sign On,簡稱SSO)解決方案

概念

首先要了解幾個概念

  • 單點登錄(Single Sign On)
  • JWT
  • RSA

背景

為什么需要單點登錄?簡單的來說就是http請求是無狀態的,你的上一次請求和下一次請求都是沒有關聯的,那么怎么讓服務器知道你是誰?他是誰?

會話機制

說到會話機制你肯定會想到session和cookie

  • session將會話id作為每一個請求的參數,服務器接收請求自然能解析參數獲得會話id,判斷是否來自同一會話。
  • cookie和session的作用一致,最大的區別就是session保存在瀏覽器,session保存在服務器

隨着web系統的發展,現在的系統結構已經很復雜了,web系統早已從久遠的單系統發展成為如今由多系統組成的應用群,面對如此眾多的系統,用戶難道要一個一個登錄、然后一個一個注銷嗎?
因此,我們需要一種全新的登錄方式來實現多系統應用群的登錄,這就是單點登錄

單點登錄

單點登錄說白了就是,把你的登錄信息保存在token中,用戶發起http請求時把token帶上,服務器在接收到請求時去解析token,拿到你的登錄信息,就知道你是誰了。
更多關於單點登錄:傳送門

JWT&RSA

JWT 和 RSA 可以說是單點登錄是實現方式

JWT

JWT就是一個字符串也可以稱為token,它把你的信息加密后生成一個字符串,前端保存這個字符串,在發起請求的時候就帶上token,后端接收到token進行解析...
關於JWT:傳送門

RSA

RSA是一個加密方式,是一種非對稱的加密方式,為什么叫非對稱加密方式?
因為它的加密和解密用的不是同一個密鑰
加密使用公鑰加密,解密用過私鑰解密,私鑰保存到后端,通過RSA就大大的提高了信息的安全性
關於密鑰,私鑰,公鑰的區別:傳送門

總結

使用JWT&RSA實現單點登錄,通過RSA對公鑰和私鑰字符串進一步處理,JWT生成Token時,使用公鑰進行加密,解析時,JWT通過私鑰對Token進行解析(token可以設置過期時間,如果token過期,會自動解析失敗)

代碼

JwtUtils

package com.example.jwtras.utils;

import com.example.jwtras.entity.Employee;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.beanutils.BeanUtils;
import org.joda.time.DateTime;


import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

import java.security.PrivateKey;
import java.security.PublicKey;


/**
 * @author FENGZENG
 * @date 2021/8/23 17:30
 */
public class JwtUtils {

    private static final String FORMAT_STR = "yyyy-MM-dd HH:mm:ss";


    /**
     * 生成token
     *
     * @param data          加密的數據
     * @param expireMinutes token過期時間
     * @param privateKey    私鑰
     * @return token
     * @throws IntrospectionException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public static String generateToken(Object data, int expireMinutes, PrivateKey privateKey) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
        JwtBuilder jwtBuilder = Jwts.builder();
        if (data == null) {
            throw new RuntimeException("數據不能為空");
        }
        //拿到bean信息
        BeanInfo beanInfo = Introspector.getBeanInfo(Employee.class);
        //獲取bean的屬性
        PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
        //把bean的字符屬性放入token中
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            //獲取bean的屬性名
            String name = propertyDescriptor.getName();
            //獲取屬性對應的value
            Object value = propertyDescriptor.getReadMethod().invoke(data);
            if (value != null) {
                jwtBuilder.claim(name, value);
            }
        }
        //設置token過期時間
        jwtBuilder.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate());
        //使用私鑰進行加密
        jwtBuilder.signWith(SignatureAlgorithm.RS256, privateKey);
        //生成token
        return jwtBuilder.compact();
    }


    /**
     * 解析token,獲取token中存儲的對象
     * @param token token
     * @param publicKey 公鑰字符串
     * @param beanClass beanClass
     * @param <T> 泛型
     * @return
     */
    public static <T> T getObjectFromToken(String token, PublicKey publicKey, Class<T> beanClass) {

        try {
            //獲取token中存儲的信息
            Claims body = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).getBody();
            //獲取bean對象,實例化還為初始化
            T bean = beanClass.newInstance();
           //內省 Java bean 並了解其所有屬性、公開的方法和事件
            BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);

            //獲取bean的屬性和value,類似Map結構
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                //拿到屬性名
                String name = propertyDescriptor.getName();
                //拿到屬性名對應的值
                Object value = body.get(name);
                if (value != null) {
                    //實例化bean
                    BeanUtils.setProperty(bean, name, value);
                }
            }
            return bean;
        } catch (Exception e) { //如果token過期,會自動解析失敗
            throw new RuntimeException("token已失效");
        }
    }

}

RsaUtils

package com.example.jwtras.utils;


import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * @author FENGZENG
 * @date 2021/8/23 17:44
 */
public class RsaUtils {

    private static String algorithm = "RSA";

    /**
     * 獲取 私鑰對象
     * @param key 私鑰字符串
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PrivateKey getPrivateKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException {
        //對key進行base64加密
        byte[] decode = Base64.getDecoder().decode(key);
        //使用PKCS8進行加密
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decode);
        //加密方式RSA
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        //生成privateKey
        return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
    }

    /**
     * 獲取公鑰對象
     * @param key 公鑰字符串
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PublicKey getPublicKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException {
        // 對key進行base64加密
        byte[] decode = Base64.getDecoder().decode(key);
        //公鑰使用X509進行加密
        X509EncodedKeySpec spec = new X509EncodedKeySpec(decode);
        //加密方式RSA
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        //生成publicKey
        return keyFactory.generatePublic(spec);
    }


    /**
     * 從文件中讀取密鑰
     * @param filename 傳入文件名,相對於classpath
     * @return
     * @throws IOException
     */
    private static byte[] readFile(String filename) throws IOException {
        return Files.readAllBytes(new File(filename).toPath());
    }

}

tips

把個人信息放入到token中,使用的BeanInfoPropertyDescriptor其實是我學習別人的代碼的時候碰到的,我還沒有去細看,
另外一個方案就是序列化,可以直接把對象序列化成json串放到token中,拿到token時再反序列化得到序列化信息。
序列化可以使用Gson,fastJson,jackson


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM