带有 Angular 8 和 Springboot 2 的 JWT:JWT 字符串必须恰好包含 2 个句点字符

Posted

技术标签:

【中文标题】带有 Angular 8 和 Springboot 2 的 JWT:JWT 字符串必须恰好包含 2 个句点字符【英文标题】:JWT with Angular 8 and Springboot 2 : JWT strings must contain exactly 2 period characters 【发布时间】: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',但没有成功。

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

【问题讨论】:

【参考方案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 参数发送,否则一旦在浏览器网络部分可以看到。你知道我该如何在前端处理它 标头也可以在浏览器网络部分读取,所以这并不能解决您的问题。我更新了我的答案以提出登录工作流程。

以上是关于带有 Angular 8 和 Springboot 2 的 JWT:JWT 字符串必须恰好包含 2 个句点字符的主要内容,如果未能解决你的问题,请参考以下文章

带有 Spring Boot 的 Angular CLI

带有Angular应用程序的tomcat 9服务器上的Spring Boot 404和CORS

寻找带有 Angular 4 和更多一个弹簧启动应用程序的示例 [关闭]

带有 Keycloak 的 Angular/Spring Boot 抛出 403

带有故事书 6 的 Angular 8

在带有 Angular 的 Spring Boot 中启用了 Cors,但仍然出现 cors 错误