微光集市-JWT和Token+RSA完善登录功能(版本5.0)

JWT和Token在本项目中的应用

1 JWT(Json Web Token)和Token介绍

1.1 Token介绍

Token:计算机身份认证中是(令牌)的意思

  • 当多服务器时,多个功能模块在不同服务器,此时登陆模块在其中一个服务器,其他模块在其他服务器,session只在登陆模块的服务器,此时需要session共享;

    ① session复制:将session复制给其他服务器:开销大,冗余大

    ②redis共享:将session存储到redis中,多个服务器可共享redis数据

    问题:① sessionId池存储在内存中,session过多,sessionId池压力大;

    ② 不是所有应用都会用到redis

    ③ 当我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。

    • CSRF:CSRF称为跨站请求伪造,用户很容易受到CSRF攻击

Token认证

在这里插入图片描述

1.2 JWT介绍

生成Token的一种方式

​ JWT(全称JSON Web Token)是一个开发标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象的形式安全的传输信息。此信息可以被验证和信任,因为它是数字签名的。jwt可以使用HMAC算法或使用RSA或ECDSA的公钥/私钥对进行签名。

简单说,JWT是通过JSON形式作为Web应用中的令牌,在各方之间安全地将信息作为JSON对象传输,在数据传输过程中还可以完成数据加密、签名等相关处理

1.2.1 JWT可以做什么?
  • 授权

​ 这是JWT最常见的使用方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小而且可以在不同的域中轻松使用。

  • 信息交换

​ JSON Web Token是在各方之间安全地传输信息的一个不错选择。因为可以对JWT进行签名,所以您可以确保发件人

1.2.2 JWT的组成部分

JWT生成的token字符串通过“.”分割为3段

在这里插入图片描述

  • 生成令牌的组成

    HEADER(标头)

    PAYLOAD(有效负载)

    VERIFY SIGNATURE(签名)

    • JWT通常的格式为:xxxx.yyyyy.zzzz (Header.Payload.Signature)
  • 标头

    • 标头通常由两部分组成:令牌类型(即JWT)和使用的签名算法,如:HMAC、HS256、RSA.它使用Base64编码
    • 注意:Base64是一种编码,也就是说,它可以被翻译回原来的样子,并不是一种加密算法。
  • 有效负载

    • Payload数据令牌的第二部分,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明
    • Payload中包含用户自身需要的信息(如ID、用户名、权限、角色、头像等信息,原本存入Session的信息)
    • 为了便于传输和解码,JWT使用Base64编码对Payload进行编码,让其编码为一个字符串,在使用时在对Base64进行解码,以还原数据本身

    注:用户敏感信息不要放入payload中,因为可以被解码还原数据本身

    • Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
    - iss (issuer):签发人
    - exp (expiration time):过期时间
    - sub (subject):主题
    - aud (audience):受众
    - nbf (Not Before):生效时间
    - iat (Issued At):签发时间
    - jti (JWT ID):编号
    
  • 签名

    • 前两部分都是使用Base64进行编码的,别人可以通过解码获得数据本身的信息,信息不安全。
    • Signature使用前两部分的编码在加上我们提供的一个密钥(盐),然后使用header中指定的签名算法生成签名,最终生成一个完整的token。
    • 服务端解签是通过header.payload和密钥生成一个新的签名与token中的签名匹配,如果header和payload任何一部分被篡改,生成的签名肯定和token中的签名不一致.以保证token的安全性
    • 签名的作用是保证JWT没有被篡改过。

    PS:签名的目的

    • 最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改。如果有人对头部以及负载的内容解码后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务端会判断出新的头部和负载形成的签名和JWT附带的签名不一致。
    • 如果要对新的头部和负载进行签名,在不知道服务端密钥的情况下,得出的签名肯定是不一致的
1.2.3 JWT认证流程

在这里插入图片描述

1.3 使用JWT签发和解签token

基于jjwt的实现

jjwt对各种加密算法的secretKey(盐)的长度进行的强制要求,如果长度不满足则会抛出异常

jjwt通过盐生成秘钥,然后在使用加密算法加密,最终生成签名

1.3.1 引入相关依赖

jjwt需要引入三个依赖包:jjwt-impl jjwt-api jjwt-jackson

jjwt-apijjwt-impl的必须依赖,所以引入jjwt-impl后自动将jjwt-api包引入,无需单独引入

<!-- jjwt-impl-->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.11.5</version>
 </dependency>

<!-- jjwt-jackson -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.11.5</version>
</dependency>
1.3.2 加密(签发Token)
  • 代码如下
    //签发:生成一个Token
    public static String createToken() {
        //标头map集合
        Map<String, Object> heardMap = new HashMap<String, Object>() {{
            put("typ", "JWT");
            put("alg", "HS256");//设置签名算法
        }};
        //负载信息map集合
        Map<String, Object> payloadMap = new HashMap<String, Object>() {{
            put("user_id", 93);
            put("user_name", "admin");
            //下方为jwt提供的标准自段
            put("iss", "huozhexiao");//签发人
            put("exp", new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24 * 7)));//过期时间
            put("iat", new Date());//签发时间
            put("sub", "JWT测试");//主题
            put("jti", "huozhexiao001");//编号
        }};
        String token = Jwts.builder()//创建一个JWT构建器对象
                .setHeader(heardMap)//设置标头,可以不设置,有默认值
                .setClaims(payloadMap)//设置负载信息
               .signWith(Keys.hmacShaKeyFor(secretKet.getBytes(StandardCharsets.UTF_8))//设置秘钥
                        , SignatureAlgorithm.HS256 //签名算法
                )
                .compact();//生成Token
        ;
        //生成Token
        return token;
    }

测试生成如下Token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJzdWIiOiJKV1TmtYvor5UiLCJ1c2VyX2lkIjo5MywidXNlcl9uYW1lIjoiYWRtaW4iLCJpc3MiOiJodW96aGV4aWFvIiwiZXhwIjoxNjgwNzk4MzAwLCJpYXQiOjE2ODAxOTM1MDAsImp0aSI6Imh1b3poZXhpYW8wMDEifQ
.53s4fwjG3ge0ZLLGI1CaNVCrJiAAMDxzDDbt5vWvAAE
1.3.2 解析(解签Token)
  • 代码如下
/**
     * 解析Token
     * 解析时,必须提供秘钥和token字符串
     * @param token
     */
    public static void parseToken(String token){
        //解析指定Token并返回数据
        Jws<Claims> claimsJws = 
            Jwts.parserBuilder()//创建一个解析构建器对象
         .setSigningKey(Keys.hmacShaKeyFor(secretKet.getBytes(StandardCharsets.UTF_8)))//设置解析器所使用的秘钥
                .build()//获得Token解析器对象
                .parseClaimsJws(token);
        ;
        System.out.println(claimsJws.getSignature());//获得签名
        JwsHeader header = claimsJws.getHeader();//获得标头对象
        System.out.println(header.getAlgorithm());//获得签名算法
        //获得负载数据对象
        Claims claims = claimsJws.getBody();
        System.out.println(claims.getSubject());//主题
        System.out.println(claims.getIssuer());//签发人
        System.out.println(claims.getExpiration());//过期时间
        System.out.println(claims.getIssuedAt());//签发时间
        System.out.println(claims.getId());//编号
        System.out.println(claims.get("user_id",Integer.class));
        System.out.println(claims.get("user_name",String.class));
    }

相应数据

HS256
JWT测试
huozhexiao
Fri Apr 07 00:49:45 CST 2023
Fri Mar 31 00:49:45 CST 2023
huozhexiao001
93
admin

1.4 使用RSA算法生成公钥和私钥进行签发和解签token

RSA加密是一种非对称加密算法。可以在不传递密钥清况下,完成解密。这能够确保信息的安全性,避免了直接传递秘钥被解密的风险。RSA由一对密钥进行加密解密,分别叫做公钥和私钥。

  • 公钥:一般用于加密,也可以解密,不对外公开
  • 私钥:用于解密,不能进行加密,对外公开
1.4.1 生成公钥和私钥
  • 导入工具类
import java.io.File;
import java.io.IOException;
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工具类 
 */
public class RsaUtils {
    private static final int DEFAULT_KEY_SIZE=2048;
    /**
     * 从文件中读取密钥
     * @param filename 公钥保存路径,
     * @return 公钥对象
     */
    public static PublicKey getPublicKey(String filename) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     * @param filename 私钥保存路径,
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     * @param bytes 字节形式的公钥
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PublicKey getPublicKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获得私钥
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    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
     * @param keySize
     */
    public static void generateKey(String publicKeyFileName,String privateKeyFileName,String secret,int keySize) throws NoSuchAlgorithmException, IOException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize,DEFAULT_KEY_SIZE));
        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 IOException {
        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);
    }
}
  • 测试
    public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
         //公钥文件名
        String publicKeyFileName ="D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public";
        //私钥文件吗
        String privateKeyFileName ="D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private";    
        /**
         *生成公钥和私钥
         * 参数1:公钥文件名
         * 参数2:私钥文件名
         * 参数3:盐
         * 参数4:尺寸
         */
        RsaUtils.generateKey(publicKeyFileName,privateKeyFileName,"huozhexiao",1024);
    }

公钥在key.public:

  • MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtlEdnUqLJb9kSP3NuIXS2mLRzyfsXFS7Kj4WxuH+lSckLoBMQiP3NcmueTml/G3d2X3Boc/6fGZss+XLZeun/zEtUuwCg4ZjX7H8bfiiJdynLtmpnaxRxDC7/PWAQZB1IbPcGdi041sdJLY2lXvx+NjtXcy+Kjr9dDj2L94/rEwemrlOsSkxxuxT9Jud3aEVx/97qwYvinDufHgWntCSVBtTzWCyAk6hET77fV5WCxAcOXORV9MwMErynCzUqgDUXQS5PSN1r0x+yBtaEYtWFn6Yyi6qPFzRHEoar+o1uPFbkPxG/7x/3zb8Foll9ddIb6nWVoPUKl07WBf9TQpuFQIDAQAB
    

私钥在key.private

  • MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2UR2dSoslv2RI/c24hdLaYtHPJ+xcVLsqPhbG4f6VJyQugExCI/c1ya55OaX8bd3ZfcGhz/p8Zmyz5ctl66f/MS1S7AKDhmNfsfxt+KIl3Kcu2amdrFHEMLv89YBBkHUhs9wZ2LTjWx0ktjaVe/H42O1dzL4qOv10OPYv3j+sTB6auU6xKTHG7FP0m53doRXH/3urBi+KcO58eBae0JJUG1PNYLICTqERPvt9XlYLEBw5c5FX0zAwSvKcLNSqANRdBLk9I3WvTH7IG1oRi1YWfpjKLqo8XNEcShqv6jW48VuQ/Eb/vH/fNvwWiWX110hvqdZWg9QqXTtYF/1NCm4VAgMBAAECggEAR7RkeNn6GykIuLp1oCal9LVb/mUdzXyXtjgAPk1hEul3jgBwvaymjFWblNsLANp3IBSZRNpnEmk4RJLS6e2Cv9foEw52uKLwz5DRjrD0mP6NFFyQHM7Kw+ZE8Wre/CpkHxK9tL6p+id6MVem5Sj/1JcA2Fzvx+02hPDoRpBbK5iElcVMhKUxJsdgM4QJut//8Ip/JdKV/E7Jrmt/eSSqe5LsSVLiKfIVUg3U093+jucyVTJ3swHmE9jlHB2O6PQy4IkbwD/FKFCjT3FzknbYijr57/Xf0v8bkrkVfgMshvjRHJNBNGHw6jgehfHWEyHErVu41xztdzOoqpj/9pMM+wKBgQDUyi5ikongGTnrSXg0kT6JMwx3fLXU63D3cPZCWN1rsorzBG1e/dwKVmL9x/U8/HgoI+Zo2pCmsqJrCz4p7qZnBVB+pUktR6tZk7wkniPbWNiMrJzf4JN98Nbqz7oQLjLKrFcLDXDgbe/LN0EfpkyRQgpjYLs+Nnr5pb/8zACq4wKBgQDbVs7xcZTeJA9pVwmYiREQirH5r0cGXWHI1vIYeq/XUUPxhnWpMDcQbygj49BvaTmGAUCnVwVlN5Gvw/SwvE48xKwRJM0apxEOjxt1RiuOIxfPjSQ5R4ftcZzFESaXfUX3h9VDGd4e236dEJnCxdUeDTxi3fgYIkjTJZ8u8ed8pwKBgQClMNPzqCkq9Mp28xFDVeJDZoLuG72ZLrIDFgnHFe/G1NNzt2Mk1FTHHas5ssqabrDlEIGlss+K6bCXAyJeMSuzXHfR6YS2hyXpo3vyvWW+uelaxAIA9vnpUle18E9UkljR6BqmtOeFAzOeAiYnaNWWCru/zG9v66FqPxedK8302wKBgGJINZZupJwdYGJ9Q6l70Y+t9i3BYnvxn/1Ug0qAvwYmPeGdtF9JYYMVq9DZJe6mIcZwDT5uedZu3fL6RUxkNFJ6dfeAm/8TWUtCyLT16lJYWzT/M3oPGVNGE08ibj53PcC6ts7IaoU9KTDL3XovF13N5H8Qozh9NFCYjQmGD4oFAoGAVFoj1l/pvaA2nGDRkcsBrVVkahWM53pIIaGzyHmTmqPrtPDMDbyoiukHWtJmcyVjeUIqUfz/InIOBnSKSET7rnWoTm+MXianXtWsYbKJoUVSuvpLvywgL42W8izr5pUKO2+aJMUNUjeV5Wn4wY73/5oStRyRgPuew+RnZayWt44=
    

1.4.2 私钥签发Token
  • 代码如下
 //签发:生成一个Token
    public static String createToken() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        //读取私钥
        PrivateKey privateKey = RsaUtils.getPrivateKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private");
        //标头map集合
        Map<String, Object> heardMap = new HashMap<String, Object>() {{
            put("typ", "JWT");
            put("alg", "HS256");//设置签名算法
        }};
        //负载信息map集合
        Map<String, Object> payloadMap = new HashMap<String, Object>() {{
            put("user_id", 93);
            put("user_name", "admin");
            //下方为jwt提供的标准自段
            put("iss", "huozhexiao");//签发人
            put("exp", new Date(System.currentTimeMillis() + (1000 * 60 *60 * 24 *7)));//过期时间
            put("iat", new Date());//签发时间
            put("sub", "JWT测试");//主题
            put("jti", "huozhexiao001");//编号
        }};
        String token = Jwts.builder()//创建一个JWT构建器对象
                .setHeader(heardMap)//设置标头,可以不设置,有默认值
                .setClaims(payloadMap)//设置负载信息
//                .signWith(privateKey//设置秘钥
//                        , SignatureAlgorithm.HS256 //签名算法
//                )
                .signWith(privateKey)//设置签名使用私钥生成
                .compact();//生成Token
        ;
        //生成Token
        return token;
    }

测试生成如下Token

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9
.eyJzdWIiOiJKV1TmtYvor5UiLCJ1c2VyX2lkIjo5MywidXNlcl9uYW1lIjoiYWRtaW4iLCJpc3MiOiJodW96aGV4aWFvIiwiZXhwIjoxNjgwODQ0Mzk4LCJpYXQiOjE2ODAyMzk1OTgsImp0aSI6Imh1b3poZXhpYW8wMDEifQ
.sQYka7mRHhIm4h7PRbWTWou8KvLCnwtb8ELzKPHhy4oFqh9_z7hMDXV_phadjiI8It9cpFYBnH8TQFuNwfpGF8l8vd64eX5n1e3joh24ocq5HkS9lr2nLFT4zkX3U01jtcS60mA089tXuwhnkd6dqr0T01PfYy1LGMiyUUDNZJgWIFGZEQpyQ3KwcVUMvplhvFL20ebDko3lBkdZOkullH0l0oEDBSDSKnLaNZJhaZ_iOpq3aJGalmWdEv8AyqrnfrUpOD_By136IYtdbtk0uChF6eonDi9avW-XH4KIb7js-HhRP4plwrRmzOH4Wrrc29wcYla_UY8Tk8jrEqA
1.4.3 解签Token
  • 代码如下
    /**
     * 解析Token
     * 解析时,必须提供秘钥和token字符串
     * @param token
     */
    public static void parseToken(String token) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        //读取公钥
        PublicKey publicKey = RsaUtils.getPublicKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public");
        //读取私钥
        PrivateKey privateKey = RsaUtils.getPrivateKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private");
        //解析指定Token并返回数据
        Jws<Claims> claimsJws = Jwts.parserBuilder()//创建一个解析构建器对象
//                .setSigningKey(Keys.hmacShaKeyFor(secretKet.getBytes(StandardCharsets.UTF_8)))//设置解析器所使用的秘钥
//                .setSigningKey(privateKey)//私钥来解签
                .setSigningKey(publicKey)//公钥来解签
                .build()//获得Token解析器对象
                .parseClaimsJws(token);
        ;
        System.out.println(claimsJws.getSignature());//获得签名
        JwsHeader header = claimsJws.getHeader();//获得标头对象
        System.out.println(header.getAlgorithm());//获得签名算法
        //获得负载数据对象
        Claims claims = claimsJws.getBody();
        System.out.println(claims.getSubject());//主题
        System.out.println(claims.getIssuer());//签发人
        System.out.println(claims.getExpiration());//过期时间
        System.out.println(claims.getIssuedAt());//签发时间
        System.out.println(claims.getId());//编号
        System.out.println(claims.get("user_id", Integer.class));
        System.out.println(claims.get("user_name", String.class));
    }

公钥私钥都可以生成相应数据

RS256
JWT测试
huozhexiao
Fri Apr 07 13:13:18 CST 2023
Fri Mar 31 13:13:18 CST 2023
huozhexiao001
93
admin
1.4.4 测试公钥签发Token
  • 代码如下
    //签发:生成一个Token
    public static String createToken() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        //读取公钥
        PublicKey publicKey = RsaUtils.getPublicKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public");
        //读取私钥
//        PrivateKey privateKey = RsaUtils.getPrivateKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private");
        //标头map集合
        Map<String, Object> heardMap = new HashMap<String, Object>() {{
            put("typ", "JWT");
            put("alg", "HS256");//设置签名算法
        }};
        //负载信息map集合
        Map<String, Object> payloadMap = new HashMap<String, Object>() {{
            put("user_id", 93);
            put("user_name", "admin");
            //下方为jwt提供的标准自段
            put("iss", "huozhexiao");//签发人
            put("exp", new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24 * 7)));//过期时间
            put("iat", new Date());//签发时间
            put("sub", "JWT测试");//主题
            put("jti", "huozhexiao001");//编号
        }};
        String token = Jwts.builder()//创建一个JWT构建器对象
                .setHeader(heardMap)//设置标头,可以不设置,有默认值
                .setClaims(payloadMap)//设置负载信息
//                .signWith(privateKey//设置秘钥
//                        , SignatureAlgorithm.HS256 //签名算法
//                )
                .signWith(publicKey)//设置签名使用私钥生成
                .compact();//生成Token
        ;
        //生成Token
        return token;
    }

测试

报如下异常InvalidKeyException:无效key异常

Exception in thread "main" io.jsonwebtoken.security.InvalidKeyException: 
JWT standard signing algorithms require either 
    1) a SecretKey for HMAC-SHA algorithms or 
    2) a private RSAKey for RSA algorithms or 
    3) a private ECKey for Elliptic Curve algorithms.  The specified key is of type sun.security.rsa.RSAPublicKeyImpl
  • 大致意思为

    HMAC-SHA和RSA算法生成的秘钥,只有私钥可以进行签发

1.5 本项目使用JWT登陆成功后签发Token

  • 在SpringSecurity中,登陆(认证)请求会进入UsernamePasswordAuthenticationFilter过滤器

    未登陆(非认证)请求进入BasicAuthenticationFilter

  • Handler处理器在Filter之后执行

我们可以在上面两个过滤器,进行认证签发,但是由于我们之前在Handler(用session)做登陆认证签发,所以为了不做过多改变,我们也可以处理器中进行认证签发

1.5.1 将JWT和RSA封装成工具类导入utils包
  • JWTUtils
/**
 * @Description:JWT工具类
 * 该工具类中包含以下三个方法:
 *  1.根据用户信息生成token
 *  2.从token中获取用户信息
 */
public class JWTUtils {
    private static final String JWT_PAYLOAD_USER_KEY = "user";
    /**
     * 使用私钥和用户信息生成token
     * @param userInfo  用户信息
     * @param privateKey 私钥对象
     * @param expire  过期时间(单位:秒)
     * @return  生成的token
     */
    public static <T> String generateToken(T userInfo, PrivateKey privateKey, int expire) throws JsonProcessingException {
        /**
         * 设置过期时间
         */
        //获取当前时间
        Calendar calendar = Calendar.getInstance();
        //在当前时间上加上指定的时间
        calendar.add(Calendar.SECOND,expire);
        //将userInfo对象转换为JSON字符串
        String userInfoJson = new ObjectMapper().writeValueAsString(userInfo);
        //生成token
        return Jwts.builder()//获得JWT的编译器对象
                .claim(JWT_PAYLOAD_USER_KEY,userInfoJson)//设置负载,负载内容为JSON字符串
                .setExpiration(calendar.getTime())//设置过期时间
                .signWith(privateKey, SignatureAlgorithm.RS256)//设置签名,使用私钥做为签名
                .compact();//生成token
    }

    /**
     * 使用公钥获取token中的payload信息(负载信息)
     * @param token     token令牌(该令牌使用私钥生成)
     * @param publicKey 公钥,用于解签token
     * @param claType   payload信息的类型
     * @return  payload数据
     */
    public static <T> T getPayLoadFromToken(String token, PublicKey publicKey, Class<T> claType) throws JsonProcessingException {
        Jws<Claims> claims = Jwts.parserBuilder()//获得JWT解析器的编译器对象
                                    .setSigningKey(publicKey)//设置签名(公钥加密签名)
                                    .build()//获得JWT编译器对象

                                    .parseClaimsJws(token);//解析token获得JWS对象

        Claims body = claims.getBody();//获得payload数据

        String jsonStr = body.get(JWT_PAYLOAD_USER_KEY).toString();
        return new ObjectMapper().readValue(jsonStr,claType);
        
    }

}
  • RSAUtils
/**
 * @Description:RSA加密解密工具类(用于生成公钥和密钥)
 */
public class RSAUtils {
    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中读取密钥
     *
     * @param filename 公钥保存路径,
     * @return 公钥对象
     */
    public static PublicKey getPublicKey(String filename) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 字节形式的公钥
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static PublicKey getPublicKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获得私钥
     *
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private 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             密钥
     * @param keySize            生成密钥的长度
     */
    public static void generateKey(String publicKeyFileName, String privateKeyFileName, String secret, int keySize) throws NoSuchAlgorithmException, IOException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE));
        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 IOException {
        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);
    }
}
1.5.2 生成公钥和私钥
     public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
         //公钥文件名
        String publicKeyFileName ="D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public";
        //私钥文件吗
        String privateKeyFileName ="D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private";
        /**
         *生成公钥和私钥
         *
         * 参数1:公钥文件名
         * 参数2:私钥文件名
         * 参数3:盐
         * 参数4:尺寸
         */
        RSAUtils.generateKey(publicKeyFileName,privateKeyFileName,"huozhexiao",1024);
    }
1.5.2 登陆成功在Handler签发Token
/**
 * 登陆成功处理器,该处理器实现AuthenticationSuccessHandler
 *  当用户登陆成功后会自动执行该处理器里的onAuthenticationSuccess方法
 */
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //设置响应类型及编码集
        response.setContentType("application/json;charset=utf-8");
        try {
            //获得认证对象获得认证主体,主体式User对象
            User user= (User) authentication.getPrincipal();

            //将用户名转化为UserInfo对象
            //user.getUsername存放有用户信息,将该信息转换为UserInfo对象
            UserInfo userInfo = JSONObject.parseObject(user.getUsername(), UserInfo.class);
            //将userInfo中的数据转存到CurUserInfo中
            CurUserInfo curUserInfo=new CurUserInfo();
            curUserInfo.setUser_id(userInfo.getUser_id());
            curUserInfo.setUser_name(userInfo.getUser_name());
            
            //读取私钥
            PrivateKey privateKey = RSAUtils.getPrivateKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.private");
            Map<String,Object> payloadMap =new HashMap<>();
            payloadMap.put("curUserInfo",curUserInfo);
            payloadMap.put("authorities",user.getAuthorities());

            //登陆成功签发
            String token = JWTUtils.generateToken(payloadMap, privateKey, 60*60*24*7);
            Map<String,Object>  map =new HashMap<>();
            map.put("token",token);
            map.put("curUserInfo",curUserInfo);


            Result result =Result.success("登陆成功",map);
            response.getWriter().println(JSONObject.toJSONString(result));
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}

测试

成功

在这里插入图片描述

1.5.3 Shopping项目使用过滤器解析token
  • 登陆(认证)请求会进入UsernamePasswordAuthenticationFilter过滤器

  • UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter里的successfulAuthenticationHttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) 方法

  • 该方法每次都会检测SecurityContextHolder是否是空,如过是空说明未认证

    在这里插入图片描述

  • 配置注册过滤器

//注入AuthenticationConfiguration对象
@Resource
private AuthenticationConfiguration authenticationConfiguration;

//securityFilterChain方法里配置
httpSecurity.addFilter(new ShoppingBasicAuthenticationFilter(authenticationConfiguration.getAuthenticationManager()));
  • 在登录成功Handler清空SecurityContextHolder
//删除SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
  • 过滤器ShoppingBasicAuthenticationFilter继承BasicAuthenticationFilter处理访问请求
/**
 * 自定义过滤器
 */
public class ShoppingBasicAuthenticationFilter extends BasicAuthenticationFilter {
    public ShoppingBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //获得请求路径
            String requestUri = request.getRequestURI().substring(1);
            System.out.println(requestUri);
            //让无需认证请求直接通过
            if (requestUri.matches("^book/.+|register")) {
                chain.doFilter(request, response);//交给下一个过滤器
                return;
            }
            //获得请求头的Authorization的属性里面包含前端的Token
            String token = request.getHeader("Authorization");
            //是否携带Token
            if (token == null || token.equals("")) {
                response.setContentType("application/json;charset=utf-8");
                //401:未认证
                response.getWriter().println(JSONObject.toJSONString(Result.fail(401, "请登录后访问")));
                return;
            }
            //解析Token
            //获取公钥
            PublicKey publicKey = RSAUtils.getPublicKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public");
//        Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
            //解析负载数据
            Map<String, Object> payloadMap = JWTUtils.getPayLoadFromToken(token, publicKey, Map.class);//解析负载数据
//            System.out.println(curUserInfo);
            //负载信息获得curUserInfo,返回一个LinkedHashMap集合
            LinkedHashMap curUserInfoMap = (LinkedHashMap) payloadMap.get("curUserInfo");
            CurUserInfo curUserInfo = new CurUserInfo();
            curUserInfo.setUser_name((String) curUserInfoMap.get("user_name"));
            curUserInfo.setUser_id((Integer) curUserInfoMap.get("user_id"));
            request.getSession().setAttribute("curUserInfo", curUserInfo);
            //负载信息里的认证信息
            List<Map<String, Object>> authorities = (List<Map<String, Object>>) payloadMap.get("authorities");
            System.out.println(authorities);
            List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
            for (Map<String, Object> map : authorities) {
                String authorStr = (String) map.get("authority");
                SimpleGrantedAuthority authority= new SimpleGrantedAuthority(authorStr);
                //将authority添加到List集合
                authorityList.add(authority);
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(curUserInfo,  null, authorityList);
            //将认证信息添加到SecurityContextHolder
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(authentication);
            SecurityContextHolder.setContext(context);

            chain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(JSONObject.toJSONString(Result.fail(401,"请登录后访问")));
        }
    }
}
1.5.4 Token认证后前端的处理
  1. login的处理
//将当前用户登陆者信息存入sessionStoragex 
let resultMap=result.data;
let token=resultMap.token;
let payload=token.substring(token.indexOf(".")+1,token.lastIndexOf("."))
let payloadStr=atob(payload)
let payLoadObj=JSON.parse(payloadStr);
let curUserObj =JSON.parse(payLoadObj.user)
let curUserInfo = curUserObj.curUserInfo
console.log(curUserInfo ) ;   window.sessionStorage.setItem("curUserInfo", JSON.stringify(curUserInfo));
window.localStorage.setItem("Authentication",token)
this.$router.push("/");                 
  1. 在mian.js处理受限用户的请求和响应
Vue.prototype.$axios.interceptors.request.use(function (config) {
  //检测axios请求路径是否为受限路径
  //在sessionStorage获取当前登陆者信息
  //获得请求访问路径
  let url = config.url;
  //定义匹配放行路径url正则
  let regUrl = /^book\/|^login$|^register$|^validateToken$/;
  //处理受限路径
  if (!regUrl.test(url)) {
    //从localStorage中获得Token信息
    let authorization = window.localStorage.getItem("Authorization");
    if (authorization == null || authorization == "") {
      let cancel = null;
      //取消原有请求
      //向config对象中添加一个cancelToken(取消令牌属性)
      config.cancelToken = new axios.CancelToken((c) => {
        //参数c为一个取消请求的函数
        cancel = c;
      });
      //判断cancel是否为一个函数,若果是,他必定是一个取消请求的函数
      if (typeof cancel == "function") {
        cancel("请求已取消");
      }

      Swal.fire({
        icon: 'error',
        title: "请登录后访问",
        showConfirmButton: false,
        timer: 1000,
        didClose: () => {
          //跳转到登录页
          router.push("/login");
        }
      });

    } else {
      //token存在则向请求中加入token的请求头,名字为:Authentication,值为token字符串
      config.headers.Authorization = authorization;
      console.log(config)
    }
  }
  let regUrl_validateToken = /^book\/|^validateToken$/;
  if (regUrl_validateToken.test(url)) {
    let authorization = window.localStorage.getItem("Authorization");
    if (authorization != "") {
      config.headers.Authorization = authorization;
    }
  }

  // 在发送请求之前做些什么
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});
  • 处理响应

    // 添加响应拦截器
    Vue.prototype.$axios.interceptors.response.use(function (response) {
      let result = response.data;
      if (result.code) {
        if (result.code == 401) {//用户未认证
          Swal.fire({
            icon: 'error',
            title: "请登录后访问",
            showConfirmButton: false,
            timer: 1000,
            didClose: () => {
              //跳转到登录页
              router.push("/login");
            }
          });
        }
      }
      console.log(result, "响应拦截器...")
      // 对响应数据做点什么
      return response;
    }, function (error) {
      // 对响应错误做点什么
      return Promise.reject(error);
    });
    

1.6 关闭浏览器后token依然存在,但是用户不显示的处理

加一个验证Token的方法

1.6.1 在ShoppingBasicAuthenticationFilter中验证Token是否有效
/**
 * 自定义过滤器
 */
public class ShoppingBasicAuthenticationFilter extends BasicAuthenticationFilter {
    public ShoppingBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获得请求路径
        String requestUri = request.getRequestURI().substring(1);
        System.out.println(requestUri);
        try {
            //让无需认证请求直接通过
            if (requestUri.matches("^book/.+|register")) {
//                System.out.println("匹配成功");
                chain.doFilter(request, response);//交给下一个过滤器
                return;
            }
            //获得请求头的Authorization的属性里面包含前端的Token
            String token = request.getHeader("Authorization");
            //是否携带Token
            if (token == null || token.equals("")) {
                response.setContentType("application/json;charset=utf-8");
                //401:未认证
                response.getWriter().println(JSONObject.toJSONString(Result.fail(401, "请登录后访问")));
                return;
            }
            //解析Token
            //获取公钥
            PublicKey publicKey = RSAUtils.getPublicKey("D:\\Java源码\\demo\\shopping\\myshopping-project-server\\src\\main\\resources\\Keys\\key.public");
//        Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
            //解析负载数据
            Map<String, Object> payloadMap = JWTUtils.getPayLoadFromToken(token, publicKey, Map.class);//解析负载数据
//            System.out.println(curUserInfo);
            //负载信息获得curUserInfo,返回一个LinkedHashMap集合
            LinkedHashMap curUserInfoMap = (LinkedHashMap) payloadMap.get("curUserInfo");
            CurUserInfo curUserInfo = new CurUserInfo();
            curUserInfo.setUser_name((String) curUserInfoMap.get("user_name"));
            curUserInfo.setUser_id((Integer) curUserInfoMap.get("user_id"));
            request.getSession().setAttribute("curUserInfo", curUserInfo);
            //负载信息里的认证信息
            List<Map<String, Object>> authorities = (List<Map<String, Object>>) payloadMap.get("authorities");
            System.out.println(authorities);
            List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
            for (Map<String, Object> map : authorities) {
                String authorStr = (String) map.get("authority");
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(authorStr);
                //将authority添加到List集合
                authorityList.add(authority);
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(curUserInfo, null, authorityList);
            //将认证信息添加到SecurityContextHolder
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            context.setAuthentication(authentication);
            SecurityContextHolder.setContext(context);

            //验证Token是否有效,如果有效直接返回成功即可
            if (requestUri.equals("validateToken")) {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().println(JSONObject.toJSONString(Result.success()));
                return;
            }

            chain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
            //验证Token是否有效,如果有效直接返回成功即可
            if (requestUri.equals("validateToken")) {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().println(JSONObject.toJSONString(Result.fail(520)));
                return;
            }
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().println(JSONObject.toJSONString(Result.fail(401, "请登录后访问")));
        }
    }
}
1.6.2 前端在首页getCurUserInfo方法中验证Token
getCurUserInfo(){
 this.$axios.get("validateToken")
      .then(response=>{
        let result =response.data;
          if (result.code==200){//如果有效
              //解析token中的负载信息
              let token = window.localStorage.getItem("Authorization")
              let payload=token.substring(token.indexOf(".")+1,token.lastIndexOf("."))
              let payloadStr=atob(payload)
              let payLoadObj=JSON.parse(payloadStr);
              let curUserObj =JSON.parse(payLoadObj.user)
              let curUserInfo = curUserObj.curUserInfo
              window.sessionStorage.setItem("curUserInfo", JSON.stringify(curUserInfo));           }else if(result.code =520){
            this.curUserInfo = null;
            window.localStorage.removeItem("Authorization");
            window.sessionStorage.removeItem("curUserInfo");
          }                 
      }).catch(error=>{
        console.log(error);
      })
	 this.curUserInfo = JSON.parse(window.sessionStorage.getItem("curUserInfo"))         
 	 if (this.curUserInfo != null) {
 		//获得当前用户商品数量
  		 this.getCarCount();
      }
}

此时验证方法会被拦截住在过滤器中设置当Token为空时,且不为该路径,才返回错误

if (token == null || token.equals("")) {
  if (!requestUri.equals("validateToken")) {
      response.setContentType("application/json;charset=utf-8");
      //401:未认证
      response.getWriter().println(JSONObject.toJSONString(Result.fail(401, "请登录后访问")));
      return;
  }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@活着笑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值