jwtを使用してgolangとjava(スプリングブート)の間でトークンを共有することを忘れないでください。

モールは、リアルタイム入札モジュールを追加しました。これには、WebSocketまたは類似のテクノロジーを介した双方向通信が必要です。バックエンドはスプリングブートによって開発されました。reactnativeはstompのサポートに問題があるため、(長い間失われていたgolangを確認するために)golangを使用して、websocketベースの入札サービスを作成することにしました。

タイラントピット:jwt再利用

ログインエンドポイントはスプリングブート側にあるため、jwt(jsonwebtokenに基づく)もこの側で生成されます。したがって、golangはdgrijalva / jwt-goを使用してjwt解析を実装します。Spring Boot側でjwtコードを生成して解析します。

public class JWTAuthentication {
    private static final String SECRET = "secret key";
    public static final String PREFIX = "Bearer ";
    public static final String HEADER = "Authorization";


    public static String generateToken(String name, Collection<? extends GrantedAuthority> authorities) {
        return PREFIX + Jwts.builder()
            .claim("authorities", authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")))
            .setSubject(name)
            .signWith(SignatureAlgorithm.HS512, SECRET)
            .compact();
    }

    public static Authentication parseToken(String token) {
        if (token == null) {
            return null;
        }
        Claims claims = Jwts.parser()
            .setSigningKey(SECRET)
            .parseClaimsJws(token.replace(PREFIX, ""))
            .getBody();
        String name = claims.getSubject();

        return name != null ? new UsernamePasswordAuthenticationToken(name, null,
            AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"))) : null;
    }
}
复制代码

当局は外出先では使用されないため、無視してください。JWTコードを解析する側に行きます:

type Claims struct {
	Authorities string `json:"authorities"`
	jwt.StandardClaims
}

func parseJwt(tokenString string) (Claims, error) {
	if tokenString == "" {
        return nil, fmt.Errorf("missing token string")
	}
	token, err := jwt.ParseWithClaims(strings.TrimPrefix(tokenStr, "Bearer "), &Claims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte("secret key"), nil
	})
	if err != nil {
		return nil, err
	}
	claims, ok := token.Claims.(*Claims)
	if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token")
	}
	return claims, nil
}
复制代码

スプリングブーツ側と同じシークレットを使用するのが通例です。しかし、実行するとすぐに、「署名が無効です」と報告しました。jwt-goの問題272いくつかの手がかり見つけてください:[]バイト(キー)をbase64.URLEncoding.DecodeString(キー)で置き換えます。上記のコードを書き直した後:

func parseJwt(tokenString string) (Claims, error) {
	if tokenString == "" {
        return nil, fmt.Errorf("missing token string")
	}
	token, err := jwt.ParseWithClaims(strings.TrimPrefix(tokenStr, "Bearer "), &Claims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		// return []byte("secret key"), nil
    return base64.URLEncoding.DecodeString("secret key")
	})
	if err != nil {
		return nil, err
	}
	claims, ok := token.Claims.(*Claims)
	if !ok || !token.Valid {
	    return nil, fmt.Errorf("invalid token")
	}
	return claims, nil
}
复制代码

実行後、「入力バイト2の不正なbase64データ」を報告します。明らかに、「スペース」文字は受け入れられません。しかし、この問題に触発されて、JavaソースコードとsignWithの実装を確認してください。

public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) {
    Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty.");
    Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures.  If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
    byte[] bytes = TextCodec.BASE64.decode(base64EncodedSecretKey);
    return signWith(alg, bytes);
}
复制代码

TextCodec.BASE64.decodeの実装:

public static byte[] parseBase64Binary( String lexicalXSDBase64Binary ) {
    if (theConverter == null) initConverter();
    return theConverter.parseBase64Binary( lexicalXSDBase64Binary );
}
复制代码

theConverter.parseBase64Binaryの実装:

public byte[] parseBase64Binary(String lexicalXSDBase64Binary) {
    return _parseBase64Binary(lexicalXSDBase64Binary);
}
复制代码

_parseBase64Binary実践现:

public static byte[] _parseBase64Binary(String text) {
    final int buflen = guessLength(text);
    final byte[] out = new byte[buflen];
    int o = 0;

    final int len = text.length();
    int i;

    final byte[] quadruplet = new byte[4];
    int q = 0;

        // convert each quadruplet to three bytes.
    for (i = 0; i < len; i++) 
        char ch = text.charAt(i);
        byte v = decodeMap[ch];

        if (v != -1) {
            quadruplet[q++] = v
        }

        if (q == 4) {
            // quadruplet is now filled.
            out[o++] = (byte) ((quadruplet[0] << 2) | (quadruplet[1] >> 4));
            if (quadruplet[2] != PADDING) {
                out[o++] = (byte) ((quadruplet[1] << 4) | (quadruplet[2] >> 2));
            }
            if (quadruplet[3] != PADDING) {
                out[o++] = (byte) ((quadruplet[2] << 6) | (quadruplet[3]));
            }
            q = 0;
        }
    }
    if (buflen == o) // speculation worked out to be OK
    {
        return out;
    }

    // we overestimated, so need to create a new buffer
    byte[] nb = new byte[o];
    System.arraycopy(out, 0, nb, 0, o);
    return nb;
}
复制代码

decodeMapで定義:

private static final byte[] decodeMap = initDecodeMap();
private static final byte PADDING = 127;

private static byte[] initDecodeMap() {
    byte[] map = new byte[128];
    int i;
    for (i = 0; i < 128; i++) {
        map[i] = -1;
    }

    for (i = 'A'; i <= 'Z'; i++) {
        map[i] = (byte) (i - 'A');
    }
    for (i = 'a'; i <= 'z'; i++) {
        map[i] = (byte) (i - 'a' + 26);
    }
    for (i = '0'; i <= '9'; i++) {
        map[i] = (byte) (i - '0' + 52);
    }
    map['+'] = 62;
    map['/'] = 63;
    map['='] = PADDING;

    return map;
}
复制代码

まとめると、「AZ」、「az」、「0-9」、「+ / = "以外のすべての文字が除外されます。問題は解決しました。SECRETのスペースを削除してください:

func parseJwt(tokenString string) (Claims, error) {
	if tokenString == "" {
        return nil, fmt.Errorf("missing token string")
	}
	token, err := jwt.ParseWithClaims(strings.TrimPrefix(tokenStr, "Bearer "), &Claims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		// return []byte("secret key"), nil
    return base64.StdEncoding.DecodeString("secretkey")
	})
	if err != nil {
		return nil, err
	}
	claims, ok := token.Claims.(*Claims)
	if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token")
	}
	return claims, nil
}
复制代码

jwtが言語とライブラリをクロスしない場合、そのようなピットはありません。ただし、セキュリティの観点から、スプリングブートソリューションは「!」、「@」などの一部の特殊文字を放棄するため、セキュリティとブルートフォースクラッキングのコストが削減されます。

ドミネーションピット:Redisの再利用

Spring Boot側でログインに成功すると、ユーザー情報がredisに保存されます。

@Component
public class TokenCache {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public void setUserInfo(String token, String name) {
        redisTemplate.opsForValue().set(token, name);
    }

    public String getUserInfo(String token) {
        if (token == null) {
            return null;
        }
        return (String) redisTemplate.opsForValue().get(token);
    }
}
复制代码

goは主にトークンをユーザー情報と交換するgomodule / redigoに基づいてredisを訪問します。

func getUserInfo(token string) (string, error) {
  if token == nil {
    return nil, fmt.Errorf("missing token")
  }
  conn := pool.Get() // pool 是 redis 池。为简化代码,此处不表
	defer func() {
		conn.Close()
	}()
  return redis.String(conn.Do("GET", token))
}
复制代码

redigo側は、トークンに対応する値を読み取ることができませんでした。redisグラフィカルクライアントを通じて、キーの前にいくつかの文字化けした記号があります。redis-cliのKEYSコマンドを使用して、同等の「\ xac \ xed \ x00 \ x05t」がキーの前に存在することが示されます。これは、スプリングブート側のRedisTemplateがデフォルトでJdkSerializationRedisSerializerを使用してキーと値をシリアル化するためです。この問題を解決するには、RedisTemplateシリアル化クラスをカスタマイズする必要があります。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(stringRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
复制代码

キー、値、HashKey、およびHashValueのシリアル化クラスをStringRedisSerializerとして指定します。RedisTemplateを使用する場合は、@ Resourceではなく@Autowiredアノテーションを使用してください。Javaはオープンな言語ですが、RedisTemplateのデフォルトのシリアル化動作は不適切であり、他の言語ツールの適合性と利便性、および不適切なレビューを無視しています。

おすすめ

転載: juejin.im/post/5e90314e6fb9a03c3176210b