【问题标题】:JWT with Angular 8 and Springboot 2 : JWT strings must contain exactly 2 period characters带有 Angular 8 和 Springboot 2 的 JWT:JWT 字符串必须恰好包含 2 个句点字符
【发布时间】:2020-07-24 20:31:47
【问题描述】:

我在 github 上找到了这个 tutorial,用 Angular 8 和 springboot2 实现 JWT。但是在执行相同的操作时,我遇到了下面给出的异常。

io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 0

在进一步调试时,该异常来自 DefaultJwtParser.java,它是 JWT 库中的类之一

@Override
    public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {

        Assert.hasText(jwt, "JWT String argument cannot be null or empty.");

        String base64UrlEncodedHeader = null;
        String base64UrlEncodedPayload = null;
        String base64UrlEncodedDigest = null;

        int delimiterCount = 0;

        StringBuilder sb = new StringBuilder(128);

        for (char c : jwt.toCharArray()) {

            if (c == SEPARATOR_CHAR) {

                CharSequence tokenSeq = Strings.clean(sb);
                String token = tokenSeq!=null?tokenSeq.toString():null;

                if (delimiterCount == 0) {
                    base64UrlEncodedHeader = token;
                } else if (delimiterCount == 1) {
                    base64UrlEncodedPayload = token;
                }

                delimiterCount++;
                sb.setLength(0);
            } else {
                sb.append(c);
            }
        }

        if (delimiterCount != 2) {
            String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
            throw new MalformedJwtException(msg);
        }

这意味着令牌应该以“Bearer abc.def.ghi”的格式出现,但根据教程,当我登录时它会像“Bearer Y2xpZW50OmNsaWVudA==”一样出现。

角度代码

login(user: User): Observable<any> {
    const headers = new HttpHeaders(user ? {
      authorization:'Bearer '+ btoa(user.username + ':' + user.password)
    }:{});

    return this.http.get<any> (API_URL + "login", {headers:headers}).pipe(
      map(response => {
        console.log("map"+response);
        if(response) {
          localStorage.setItem('currentUser', JSON.stringify(response));
          this.currentUserSubject.next(response);
        }
        return response;
      })
    );
  }

Java 代码

@Component
public class JwtTokenProvider
{
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    @Value("${app.jwt.token.prefix}")
    private String jwtTokenPrefix;
    @Value("${app.jwt.header.string}")
    private String jwtHeaderString;
    @Value("${app.jwt.expiration-in-ms}")
    private String jwtExpirationInMs;

    public String generateToken(Authentication authentication)
    {
        String authororities = authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority).collect(Collectors.joining());
        return Jwts.builder().setSubject(authentication.getName()).claim("roles", authororities)
            .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationInMs))
            .signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
    }

    public Authentication getAuthentication(HttpServletRequest request)
    {
        String token = resolveToken(request);
        if (token == null)
        {
            return null;
        }
        Claims claim = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
        String username = claim.getSubject();
        List<GrantedAuthority> authorities = Arrays.stream(claim.get("roles").toString().split(","))
            .map(role -> role.startsWith("ROLE_") ? role : "ROLE_" + role)
            .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return username != null
                        ? new UsernamePasswordAuthenticationToken(username, null, authorities)
                        : null;
    }

    public boolean validateToken(HttpServletRequest request)
    {
        String token = resolveToken(request);
        if (token == null)
        {
            return false;
        }
        Claims claim = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
        if (claim.getExpiration().before(new Date()))
        {
            return false;
        }
        return true;
    }

    private String resolveToken(HttpServletRequest request)
    {
        String bearerToken = request.getHeader(jwtHeaderString);
        if (bearerToken != null && bearerToken.startsWith(jwtTokenPrefix))
        {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}

我已经尝试从授权标头中删除 'Bearer',但没有成功。

如果需要其他详细信息,请告诉我

【问题讨论】:

    标签: angular spring-boot spring-security jwt


    【解决方案1】:

    Jwt 定义为 xxx.yyy.zzz 其中

    • xxx是你token的头信息,比如哈希算法,
    • yyy 是包含用户信息的有效载荷和
    • zzz 是验证令牌的签名。更多信息在这里:

    https://jwt.io/introduction/

    您只是将 username:password 作为 base64 编码的 ascii 字符串发送。那将是有效载荷部分yyy。

    Spring 需要 xxx.yyy.zzz 并抛出异常,因为没有标头/签名,它不是 jwt。

    所以如果你想在你的前端创建一个 jwt,你需要重写你的代码。

    顺便说一句:令牌不应包含密码等敏感数据。

    更新

    我会建议这个登录工作流程:

    • 用户在登录表单中输入用户名/密码
    • 您的 Angular 前端使用凭据、用户名和密码作为参数执行 POST 请求。密码被加密,例如使用bcrypt 算法。这个算法非常适合这个用例,因为它很慢,因此更难暴力破解。您对浏览器网络部分的担忧是有效的,但Authorization 标头也可以在request header 中看到,就在查询参数部分的正上方。
    • spring 后端比较凭据并返回一个 jwt 令牌。令牌可以包含用户信息,例如用户名、角色或电子邮件地址。
    • 前端将 jwt 存储在浏览器的 localStorage 中。对于每个请求,都会添加令牌,其标头为 Authorization: Bearer + ${token}
    • 后端现在可以在每个请求上验证令牌。无需每次都传递凭据。

    这需要一个身份验证服务器,目前 Spring Security 5 不支持该服务器(但此决定在 reconsideration atm 下)。

    或者,您可以使用第三方提供商,例如 aws cognito,它可以处理用户注册或使用 google 或 facebook 等社交媒体帐户登录。

    或者您可以使用google oauth api 之类的东西进行登录。用户使用 google 登录,它会返回一个 jwt,您可以将其用于后端身份验证。

    或者如果您想控制用户管理但又不想实现整个身份验证服务,我可以推荐cas。该服务可以作为一个独立的应用程序运行,支持各种认证方式。

    【讨论】:

    • 那么我该如何处理这种情况,我们不能将其作为 post 参数发送,否则一旦在浏览器网络部分可以看到。你知道我该如何在前端处理它
    • 标头也可以在浏览器网络部分读取,所以这并不能解决您的问题。我更新了我的答案以提出登录工作流程。
    猜你喜欢
    • 2020-04-07
    • 2023-03-14
    • 2021-07-24
    • 1970-01-01
    • 1970-01-01
    • 2016-08-04
    • 2022-01-13
    相关资源
    最近更新 更多