Remember to use jwt to share tokens between golang and java (spring boot)

The mall has added a real-time bidding module, which requires two-way communication through websocket or similar technologies. The back end was developed by spring boot. Because react native has some problems with the support of stomp, I chose to use golang (by the way to review the long-lost golang) to write a websocket based bidding service.

Tyrant Pit: jwt reuse

Since the login endpoint is on the spring boot side, jwt (based on jsonwebtoken) is also generated on this side. So golang uses dgrijalva / jwt-go to implement jwt parsing. Generate and parse jwt code on the spring boot side:

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;
    }
}
复制代码

Because authorities are not used on the go side, please ignore it. Go side parsing jwt code:

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
}
复制代码

It is customary to use the same SECRET as the spring boot side. But as soon as I ran, I reported "signature is invalid". Find some clues in jwt-go's issue 272 : replace [] byte (key) with base64.URLEncoding.DecodeString (key). After rewriting the above code:

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
}
复制代码

After running, it will report "illegal base64 data at input byte 2". Obviously, "space" characters are not accepted. But inspired by this issue, go to view the java source code and the implementation of 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 implementation:

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

The implementation of 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;
}
复制代码

Defined by 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;
}
复制代码

Taken together, it filters out all characters except "AZ", "az", "0-9", "+ / =". The problem is solved, remove the spaces in 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
}
复制代码

If jwt does not cross languages ​​and libraries, there is no such pit. However, from a security point of view, the spring boot solution discards some special characters such as "!", "@", Etc., which reduces the cost of security and brute force cracking.

Domination pit: redis reuse

After successful login on the spring boot side, user information will be saved in 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 visits redis based on gomodule / redigo, which mainly exchanges tokens for user information:

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))
}
复制代码

The redigo side has been unable to read the value corresponding to the token. Through the redis graphical client, there are several garbled symbols in front of the key. Through the KEYS command of redis-cli, it is shown that the "\ xac \ xed \ x00 \ x05t" equivalent exists before the key. This is because the RedisTemplate on the spring boot side uses JdkSerializationRedisSerializer to serialize Key and Value by default. To fix this problem, you need to customize the RedisTemplate serialization class:

@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;
    }
}
复制代码

Specify the serialization class of key, value, HashKey and HashValue as StringRedisSerializer. Use @Autowired annotation instead of @Resource when using the RedisTemplate. Java is an open language, but the default serialization behavior of RedisTemplate is improper, ignoring the suitability and convenience of other language tools, and bad reviews.

Guess you like

Origin juejin.im/post/5e90314e6fb9a03c3176210b