• 本文承接上文-微光集市-登陆后用户对购物车的操作(版本4.0)

文章目录

  • JWT和Token在本项目中的应用
    • 1 JWT(Json Web Token)和Token介绍
      • 1.1 Token介绍
      • 1.2 JWT介绍
        • 1.2.1 JWT可以做什么?
        • 1.2.2 JWT的组成部分
        • 1.2.3 JWT认证流程
      • 1.3 使用JWT签发和解签token
        • 1.3.1 引入相关依赖
        • 1.3.2 加密(签发Token)
        • 1.3.2 解析(解签Token)
      • 1.4 使用RSA算法生成公钥和私钥进行签发和解签token
        • 1.4.1 生成公钥和私钥
        • 1.4.2 私钥签发Token
        • 1.4.3 解签Token
        • 1.4.4 测试公钥签发Token
      • 1.5 本项目使用JWT登陆成功后签发Token
        • 1.5.1 将JWT和RSA封装成工具类导入utils包
        • 1.5.2 生成公钥和私钥
        • 1.5.2 登陆成功在Handler签发Token
        • 1.5.3 Shopping项目使用过滤器解析token
        • 1.5.4 Token认证后前端的处理
      • 1.6 关闭浏览器后token依然存在,但是用户不显示的处理
        • 1.6.1 在ShoppingBasicAuthenticationFilter中验证Token是否有效
        • 1.6.2 前端在首页getCurUserInfo方法中验证Token

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攻击
  • 之前,我们客户端(浏览器)用cookie存储sessionId,但是移动端app(客户端),没有cookie,不支持存储sessionId

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)

  • 代码如下
    //签发:生成一个Tokenpublic 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;//生成Tokenreturn 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

  • 代码如下
 //签发:生成一个Tokenpublic 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;//生成Tokenreturn 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

  • 代码如下
    //签发:生成一个Tokenpublic 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;//生成Tokenreturn 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);//生成tokenreturn 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 {@Overridepublic 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);}@Overrideprotected 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的属性里面包含前端的TokenString token = request.getHeader("Authorization");//是否携带Tokenif (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);//将认证信息添加到SecurityContextHolderSecurityContext 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);}@Overrideprotected 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的属性里面包含前端的TokenString token = request.getHeader("Authorization");//是否携带Tokenif (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);//将认证信息添加到SecurityContextHolderSecurityContext 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.curUserInfowindow.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;}
}

微光集市-JWT和Token在本项目中的应用(版本5.0)相关推荐

  1. 乐鑫esp8266学习rtos3.0笔记第9篇:整理分享那些我在项目中常用的esp8266 rtos3.0版本的常见驱动,Button按键长短按、PWM平滑调光等。(附带demo)

    本系列博客学习由非官方人员 半颗心脏 潜心所力所写,仅仅做个人技术交流分享,不做任何商业用途.如有不对之处,请留言,本人及时更改. 1. Esp8266之 搭建开发环境,开始一个"hello ...

  2. 开源协议:在项目中使用Apache License 2.0

    Apache License 2.0的使用限制有很多介绍,这篇文章说明一下在项目中使用Apache License 2.0的步骤和注意事项. 最常见的理解误区 在项目的根目录下,创建一个LISENCE ...

  3. vue项目中更换tinymce版本踩坑

    项目需求: vue项目中实现多图片批量上传功能 问题: tinymce富文本编辑器的多图片批量上传插件 支持版本:5.0.4+ 项目中现有的富文本编辑器版本:4.9.4 为实现这一功能选择更换tiny ...

  4. Pycharm项目中更改python版本以及opencv版本

    原来我使用的项目版本是anaconda下安装了python3.9和opencv4.6 最近在项目中需要用到openc3.4.2.16的版本,同时为了适配这个opencv的版本需要将python解释器的 ...

  5. 在现有项目中使用AspNet Identity 2.0 实战

    感谢@LAgess 的解答:http://q.cnblogs.com/q/68698/ 办法一: 1.用 NuGet 安装 Identity. 2.添加链接字符串: <add name=&quo ...

  6. Maven排除项目中同名不同版本的jar

    今天突然发现web项目打包后的exe居然有200M+了,心想不应该有这么大的啊,于是检查了一番发现引用的jar有130+个,仔细一瞅发现好多同名的但是不同版本的jar,比如说有commons-http ...

  7. taro不同的项目中使用不同版本的taro-cli

    这里写目录标题 preface 解决方案 1. 局部安装@tarojs/cli 2. 测试 局部是否可以使用 @tarojs/cli preface 我有两个 taro 项目,写的时间前后差距半年多了 ...

  8. vue项目中更新element-ui版本

    一.升级element-ui需要先卸载原先的版本,在cmd中输入 npm uninstall element-ui 二.然后重新安装element-ui npm i element-ui -S

  9. 微光集市-登陆后用户对购物车的操作(版本4.0)

    本文承接上文-微光集市-基于Security的用户登录功能及用户注册功能(版本3.0) 文章目录 登陆后用户对购物车的操作 1 获得登录用户购买商品数量,及将固定用户修改为当前用户 1.1 客户端 1 ...

最新文章

  1. Nginx服务器搭建和基本配置详解
  2. anaconda和vscode配置python_vscode配置anaconda3的方法步骤
  3. NYOJ_5743Distribution(第八届河南省程序设计大赛)
  4. 我们为什么要学Java?Java好在哪?
  5. Thinking in Java方法签名
  6. 【推荐】十位笔耕不辍的产品大拿
  7. centos 7 163 yum 源 python 2.7.5
  8. 深度Linux操作系统使用体验
  9. 史诗级Java入门教程,想成为初级程序员看他就够了
  10. Homography和warpPerspective
  11. arduino ps2摇杆程序_Arduino 双轴按键摇杆 PS2游戏摇杆 控制杆 JoyStick 电子积木
  12. Python保留两位小数,并做四舍五入处理
  13. java校园圈子论坛跳蚤市场小程序源码
  14. Linux 命令(204)—— ss 命令
  15. c语言课程设计会员卡计费系统,会员卡计费系统源代码.doc
  16. 问题 D: 清点人数
  17. #{}和${}的区别
  18. 一次性永远激活7290自带浏览器上网和彩信收发功能
  19. hp服务器性能测试,较新架构服务器用CPU性能对比测试
  20. 【JAVA】计算算式

热门文章

  1. 软硬整合:开发一款VR大作背后的技术支撑
  2. 内边距和外边距,清除默认的内外边距
  3. 外边距+内边距+边框详解
  4. 火焰图:全局视野的Linux性能剖析
  5. 系统设计与分析lesson5-关于微信小程序开发工具的学习
  6. 工作流Activiti集成构建自有平台方案
  7. Tracert与Traceroute[转]
  8. 清华人工智能研究院今日成立!张钹姚期智坐镇,Jeff Dean被聘顾问委员
  9. 互联网电视:OTT TV与IPTV的区别
  10. 设置PPT幻灯版自动翻页播放