Торговый центр добавил модуль торгов в реальном времени, для которого требуется двусторонняя связь через веб-сокет или аналогичную технологию. Бэкэнд разработан Spring Boot, Поскольку у React Native есть некоторые проблемы с поддержкой stomp, мы решили использовать golang (кстати, просмотрите давно потерянный golang) для написания сервиса ставок на основе веб-сокетов.
Яма тирана: повторное использование jwt
Поскольку конечная точка входа находится на стороне весенней загрузки, jwt (на основе jsonwebtoken) также создается на этой стороне. Итак, golang использует dgrijalva/jwt-go для разбора jwt. Сторона загрузки spring генерирует и анализирует код 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
}
Я обычно использую SECRET, который соответствует стороне весенней загрузки. Но как только запускается, пишет "подпись недействительна". существуетвыпуск 272 jwt-goЗдесь нашел несколько подсказок: замените [] байт (ключ) на 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
}
После запуска он сообщит о «недопустимых данных base64 во входном байте 2». Видимо символ "пробел" не принимается. Но вдохновленный этой проблемой, посмотрите исходный код 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 );
}
Реализация Converter.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;
}
В совокупности символы, отличные от "A-Z", "a-z", "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 мультиплексирование
После успешного входа в систему на стороне весенней загрузки информация о пользователе будет сохранена в 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);
}
}
перейти к redis на основе gomodule/redigo, в основном обмен токенами для информации о пользователе:
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 отображается, что перед ключом есть несколько искаженных символов. Команда KEYS redis-cli показывает, что перед ключом есть значение, такое как «\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. Используйте аннотацию @Autowired вместо @Resource, где используется RedisTemplate. Java — открытый язык, но поведение RedisTemplate при сериализации по умолчанию неуместно, игнорируя применимость и удобство других языковых инструментов.