go语言与java语言jwt加签和验签的协同问题

450 阅读2分钟

某项目1.0版本拿go写的,2.0版本是java写的。但是因为项目业务的原因,1.0版本和2.0版本要并行很长时间,而且,用户数据基本上都在1.0版本服务器上。故需要在2.0版本中解析1.0版本中go语言的jwt,并对jwt完成验签。

go中的核心代码,已删除业务逻辑,只保留问题相关代码

#conf.go 程序内容
package conf
//go:embed ec-512-private.pem  
var ES256PrivateKeyPEM string  #读取pem文件

#init.go 程序内容
package service
var initOnce sync.Once
var privateKey *ecdsa.PrivateKey
var PublicKey *ecdsa.PublicKey
func init() {
	initOnce.Do(func() {
		// jwt key pair init
		tmpPrivateKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(conf.ES256PrivateKeyPEM))  #读取pem文件获取私钥
		if err != nil {
			glog.Fatalf("ParseECPrivateKeyFromPEM err: %s", err.Error())
		}
		privateKey = tmpPrivateKey
		glog.Info("ES512 private key init success")
		tmpPublicKey := privateKey.PublicKey
		PublicKey = &tmpPublicKey #获取公钥
		glog.Info("ES512 public key init success")
		// wechat access token handler
		glog.Flush()
	})
}

#base.go 程序内容
package service
import (
	"strconv"
	"time"
	"github.com/golang-jwt/jwt"
	"github.com/golang/glog"
)
#定义载体结构
type customClaims struct {
	UserID string `json:"userid"`
	jwt.StandardClaims
}
#生成token
func createToken(userID int) string {
	IDStr := strconv.Itoa(userID)
	claims := &customClaims{
		IDStr,
		jwt.StandardClaims{
			IssuedAt:  time.Now().Unix(),
			ExpiresAt: time.Now().Add(time.Hour * 8640).Unix(),
			Issuer:    "author",
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodES512, claims)
	result, err := token.SignedString(privateKey)
	if err != nil {
		glog.Errorf("signedString err: %s", err.Error())
	}
	return result
}

# jwt.go 程序内容
package handler
import (
	"strings"
	"git.wisetv.com.cn/fovecifer/wise-yinfa/service"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	"github.com/golang/glog"
)
#获取token内容,token验签
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := c.GetHeader("authorization")
		if len(token) == 0 {
			c.AbortWithStatusJSON(200, gin.H{
				"code": 401,
				"msg":  "bad token",
			})
			return
		}
		jwtToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
			return service.PublicKey, nil
		})
		if err != nil {
			glog.Errorf("jwt token parse err: %s", err.Error())
			c.AbortWithStatusJSON(200, gin.H{
				"code": 401,
				"msg":  "bad token",
			})
			return
		}
		if !jwtToken.Valid {
			glog.Errorf("jwt token not valid")
			c.AbortWithStatusJSON(200, gin.H{
				"code": 401,
				"msg":  "bad token",
			})
			return
		}
		if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok {
			if userID, ok := claims["userid"]; ok {
				if IDStr, ok := userID.(string); ok {
					glog.V(5).Infof("set userId: %s", IDStr)
					c.Set("userId", IDStr)
					return
				}
			}
		}
		glog.Errorf("jwt token has no userid")
		c.AbortWithStatusJSON(200, gin.H{
			"code": 401,
			"msg":  "bad token, need userid",
		})
	}
}
#从header中获取jwtToken
func getBearerToken(auth string) string {
	if auth == "" {
		return ""
	}
	if !strings.HasPrefix(auth, "Bearer") && !strings.HasPrefix(auth, "bearer") {
		if auth != "" {
			return auth
		} else {
			return ""
		}
	}
	if len(auth) < 8 {
		return ""
	}
	// space
	if auth[6] != 32 {
		return ""
	}
	token := strings.Trim(auth[7:], " ")
	return token
}

go项目中的业务逻辑分析

首先go语言不会,不过逻辑能读懂,通过读代码,go的加签逻辑如下:

  • 通过ec-512-private.pem生成ECPrivateKey作为私钥;
  • 根据ECPrivateKey获取ECPublicKey作为公钥;
  • 用户鉴权成功,则生成jwtToken,生成过程中用ECPublicKey签名;
  • 再次请求,携带jwtToken,获取token中的载体信息,并对token内容验签,确保没有被篡改。

java要实现的业务逻辑

  • 截获用户再次请求,获取token中的载体信息,并对token内容验签,然后进行2.0版本的业务。

需要解决的问题

  • 证书有源文件,私钥与公钥需要重新生成,并且与go的生成方式保持一致。
  • 生成的公钥要能正常验签go生成的jwtToken。

问题解决方案

解决过程中查询大量文档,为了能与go实现同样的证书生成逻辑,并能成功对go生成的jwtToken进行校验,测试了n中方法。最终测试通过的解决方案如下:

# pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.sunchen.asc</groupId>
    <artifactId>jwtTest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>2.3.2.RELEASE</spring-boot.version>
        <lombok.version>1.18.12</lombok.version>
        <druid.version>1.1.22</druid.version>
        <mysql.version>5.1.47</mysql.version>
        <fastjson.version>1.2.68</fastjson.version>
        <swagger.version>3.0.0</swagger.version>
        <nimbus-jose-jwt.version>8.16</nimbus-jose-jwt.version>
        <hutool-version>5.3.5</hutool-version>
        <knife4j-version>2.0.3</knife4j-version>
        <!--设置源代码、编译JDK版本-->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
        <!--<dubbo.version>2.7.3</dubbo.version>-->
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>


        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>${nimbus-jose-jwt.version}</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.68</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.2.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

#testVerify.java
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;

import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;


public class TestParse {
    public static void main(String[] args) throws Exception {
        Security.addProvider(BouncyCastleProviderSingleton.getInstance());
        PEMParser pemParser = new PEMParser(new InputStreamReader(new FileInputStream("{你自己的pem文件路径}")));
        PEMKeyPair pemKeyPair = null;
        try {
            pemKeyPair = (PEMKeyPair)pemParser.readObject();
            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            KeyPair keyPair = converter.getKeyPair(pemKeyPair);
            pemParser.close();
            Security.removeProvider("BC");
            ECPrivateKey privateKey = (ECPrivateKey)keyPair.getPrivate();
            privateKey = fixAlg(privateKey); // BC parses key alg as ECDSA instead of EC
            System.out.println(privateKey.getS());
            ECPublicKey publicKey = (ECPublicKey)keyPair.getPublic();
            publicKey = fixAlg(publicKey); // BC parses key alg as ECDSA instead of EC
            System.out.println("key init finished");
            String token = "{从go中测试拿到的jwtToken字符串}";
            //解析token,拿token中的传输数据
            JWSObject jwsObject = JWSObject.parse(token);
            //用公钥验签
            Boolean verify = jwsObject.verify(new ECDSAVerifier(publicKey));
            System.out.println(verify);
        } catch (IOException e) {
            e.printStackTrace();

        }
    }
    private static ECPrivateKey fixAlg(final ECPrivateKey key)
            throws Exception {

        KeyFactory keyFactory = KeyFactory.getInstance("EC");

        return (ECPrivateKey)keyFactory.generatePrivate(
                new ECPrivateKeySpec(key.getS(), key.getParams()));
    }

    private static ECPublicKey fixAlg(final ECPublicKey key)
            throws Exception {

        KeyFactory keyFactory = KeyFactory.getInstance("EC");

        return (ECPublicKey)keyFactory.generatePublic(
                new ECPublicKeySpec(key.getW(), key.getParams()));
    }
}