JWT认证概念

14

我目前正在开发一个Angular JS 应用与Node.js服务器的交互(作为API)并且使用基于JSON Web Token的身份验证。

但我有一个问题无法自己回答:当您在服务器端将用户作为负载编码JWT时,如何在客户端检索用户信息? 以下是一个小例子,以便理解我的问题:

我是一个普通用户,我发送我的凭据到API进行身份验证。作为交换,我收到了一个JWT令牌,但我没有任何关于用户的信息,因为只有服务器拥有能够解码JWT令牌的密钥。那么服务器是否需要向我发送例如用户ID之类的信息,以便我可以调用我的api user/id来检索有关已认证用户的信息?

4个回答

10
您可以通过解码每个请求中的令牌来检索用户信息。因此,在您的示例中,令牌返回给客户端后,客户端会向服务器发出请求,使用编码令牌中存储的数据获取用户的名字和姓氏,并将其与请求一起发送回服务器。在进行此GET请求时,您可以将令牌作为参数发送。我将使用非cookie存储的示例,以下是步骤:
1.用户使用密码和用户名登录 2.服务器使用secret_key对包含签入用户唯一标识符(即user_id)的json web token负载进行编码。一个示例函数调用可能如下所示。
payload = {user_id: 35}
user_token = JWT.encode(payload, "your_secret_key");

3.将user_token返回给客户端,并将该令牌存储在隐藏的HTML标记或localStorage变量中。使用Angular,我会将其存储在localStorage中。 4.现在用户已经登录并且令牌位于客户端,您可以提交一个GET请求,其中包含user_token作为参数。请记住,这个user_token负载包含user_id。 5.服务器获取参数并解码user_token以从负载中获取user_id。 6.您使用user_id查询数据库并将数据(名和姓)作为普通JSON而不是编码的JSON返回。
重要的是要记住,您示例中需要编码的唯一内容是唯一标识符(user_id)。在每个请求中,您解码令牌,令牌本身就是身份验证机制。

谢谢你的回答,但如果你这样做,如何在客户端使用信息呢?我将通过一个例子再次解释:如果您是“John Doe”用户,并且您的用户名为jdoe,密码为pwd,您提交jdoe和pwd并接收一个令牌。但是,如何检索存储在数据库中的用户信息,例如名字和姓氏,以便在应用程序中显示它? - ChrisV
1
感谢Micah的编辑,现在更清晰了。我之前也有这个想法,你的回答证实了我的想法。非常感谢。晚安。 - ChrisV
“sub”(主题)负载键是否由JWT规范定义为用于标识此令牌声明的主题,即通常足够将用户ID放在此处,而不是任意的“user_id”键。这是真的吗,还是我误解了?(https://tools.ietf.org/html/rfc7519#section-4.1.2) - Jonathan Hartley

9
你在客户端上有有效载荷,如果所需数据在有效载荷中,你可以轻松对有效载荷进行Base64解码以找到它!
为了理解这一点,以下是步骤:
1. 客户端向服务器发送用户名:user,密码:pass。 2. 服务器开始进行身份验证,并发现用户名和密码是有效的。 3. 服务器必须将这些信息返回给客户端。这就是JWT规则的一部分。服务器必须向客户端返回一个令牌。该令牌由三个部分Header、Payload和Signature组成。暂时先忘记Signature这一部分,因为它会引起一些混淆。
第一部分是Header,类似于:
{"typ":"JWT","alg":"HS256"}

经过Base64解码后,结果将会是eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9。请注意,这只是解码,没有加密!您可以访问https://www.base64decode.org/进行测试。

header之后,服务器需要向用户发送有效载荷。服务器可能决定发送以下json(我说“可能”,因为这里没有标准要求,您可以发送更多或更少的数据作为有效载荷,例如,您还可以设置用户权限,例如admin:true或用户的名字和姓氏,但请记住,JWT大小必须小,因为它将在每个请求中发送到服务器)。

{"username":"user","id":3,"iat":1465032622,"exp":1465050622}

根据JWT的规定,服务器需要进行Base64解码(不需要进行加密)。以上json将会变成`eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9`。
到目前为止,服务器已经创建了Header和Payload。现在是时候生成签名了!这非常容易:
var encodedString=base64UrlEncode(header) + "." + base64UrlEncode(payload);
//As our example base64UrlEncode(header) is eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
//and the base64UrlEncode(payload) is eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9

 var signature=HMACSHA256(encodedString, 'a secret string which is kept at server');

签名是使用一个秘密密钥生成的,而您在客户端没有这个密钥!!您也不需要它。所有令牌数据都在负载中,并且可以通过解码访问(再次强调,不是解密!)。

当您将令牌发送回服务器时,此签名用于在服务器上进行验证,以确保服务器可以信任令牌数据。

总结一下,看看下面的令牌:

//Header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//PayLoad
eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9.
//Signature
0K8TL1YS0XKnEIfI3lYs-bu2vbWHSNZsVJkN1mXtgWg

头部和有效负载需进行Base64解码,并且客户端可以对其进行编码。但是您无法对签名进行任何操作。

服务器仅使用签名。客户端使用其令牌发送每个请求,服务器必须确保客户端未更改令牌有效负载的任何部分(例如更改userid)。这就是签名字符串的重要性所在,服务器为每个请求使用其秘密密钥重新检查签名!

注意:

您是否仍然想知道为什么JWT需要进行编码和解码?!这是为了使整个令牌在URL中安全传输!


如果我截取了某人的令牌,那么我就可以冒充他。有什么机制可以用来防止这种情况发生? - variable
1
我认为JWT中没有机制,它就像会话ID一样。如果你有某个人的会话ID,你就可以冒充他。 - Alireza Fattahi

6
答案中的策略是可行的,但它忽略了客户端可以看到JWT有效载荷的事实。在JSON Web Token解剖中很好地解释了这一点。
JWT有3个部分。前两个部分,headerpayload,都是Base64编码的。客户端可以轻松解码它们。有效载荷包含有关用户的声明,客户端可以使用这些数据(用户ID、名称、角色、令牌过期时间)而无需向服务器发出另一个请求。
JWT的第三部分是signature。它是headerpayload和只有服务器知道的密钥的哈希。服务器将在每个请求上验证令牌和用户权限。
客户端永远不知道密钥,它只有一个声称是给定用户的令牌。

客户端如何不知道密钥?它需要密钥才能准确地组合令牌... - nobody
您的期望是服务器是唯一分配令牌有效载荷和标头的来源吗?客户端不应该对其自己的有效载荷进行编码吗? - nobody
1
@BenNelson 没错,服务器是唯一应该生成令牌的东西。如果你让客户端生成令牌,那么你就会面临来自恶意用户的攻击,因为他们可以生成任何类型的有效载荷令牌。 - Sunil D.
1
如果我截取了某人的令牌,那么我就可以冒充他。有什么机制可以用来防止这种情况发生? - variable
1
@variable 我认为没有一个绝对安全的方法,如果有人能够获取用户的cookie,同样的事情也可能发生。您应该始终使用HTTPS并在JWT上设置适当的过期日期。但是,如果有人可以访问用户的浏览器,他们可以随心所欲地处理cookie/JWT。 - Sunil D.

-1

JWT(JSON Web Token)在Web开发中变得越来越流行。它是一种开放标准,允许以安全且紧凑的方式在各方之间传输数据作为JSON对象。使用JWT在各方之间传输的数据是数字签名的,因此可以轻松验证和信任。

ASP.NET Core中的JWT

第一步是在我们的项目中配置基于JWT的身份验证。我们可以添加自定义的JWT身份验证中间件,该中间件会在每个请求中触发授权。

Startup.cs

    services.AddMvc(options => options.EnableEndpointRouting = false);
    var tokenValidationParams = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey("Jwt_Key"),
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                RequireExpirationTime = false,
                ValidIssuer = "Jwt_Issuer",
                ValidAudience = "Jwt_Audience",
                ClockSkew = TimeSpan.Zero
            };

            services.AddSingleton(tokenValidationParams);

            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(jwt => {
                jwt.SaveToken = true;
                jwt.TokenValidationParameters = tokenValidationParams;
            });

            services.AddMvc();
            
            
            
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
                    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
                    {
                        if (env.IsDevelopment())
                        {
                            app.UseDeveloperExceptionPage();
                        }
                        // custom jwt auth middleware
                        **app.UseMiddleware<JwtMiddleware>();**
                        app.UseAuthentication();
                        app.UseMvc();
                        app.Run(async (context) =>
                        {
                            await context.Response.WriteAsync("Welcome to DATA API");
                        });
            
                    }

        

生成 JWT
GenerateJSONWebToken(User userInfo)
                {
                    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Jwt_Key"));
                    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
    
                    var claims = new[] {
                        new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserID),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
                    };
        
                    var token = new JwtSecurityToken("Jwt_Issuer","Jwt:Audience",
                        claims,
                        expires: DateTime.Now.AddHours(24),
                        signingCredentials: credentials);
        
                    return new JwtSecurityTokenHandler().WriteToken(token);
                }
    

该方法返回 JWT Token:

Token:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKaWduZXNoIFRyaXZlZGkiLCJlbWFpbCI6InRlc3QuYnRlc3RAZ21haWwuY29tIiwiRG F0ZU9mSm9pbmciOiIwMDAxLTAxLTAxIiwianRpIjoiYzJkNTZjNzQtZTc3Yy00ZmU xLTgyYzAtMzlhYjhmNzFmYzUzIiwiZXhwIjoxNTMyMzU2NjY5LCJpc3MiOiJUZXN0 LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.8hwQ3H9V8mdNYrFZSjbCpWSyR1CNyDYHc Gf6GqqCGnY”

调用授权方法

 [Authorize]
   public ActionResult<IEnumerable<string>> Get()
   {
    return new string[] { "value1", "value2", "value3", "value4",
    "value5" };
   }
    

在 Jwt 中间件类中验证令牌

JwtMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly TokenValidationParameters _tokenValidationParams;
        public JwtMiddleware(RequestDelegate next, TokenValidationParameters 
        tokenValidationParams)
        {
            _next = next;
            _tokenValidationParams = tokenValidationParams;
        }

        public async Task Invoke(HttpContext context)
        {
            var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();

            var jwtTokenHandler = new JwtSecurityTokenHandler();
            // Validation 1 - Validation JWT token format
            var tokenInVerification = jwtTokenHandler.ValidateToken(token, _tokenValidationParams, out var validatedToken);

            if (validatedToken is JwtSecurityToken jwtSecurityToken)
            {
                var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);

                if (result == false)
                {
                    Error Invalid = new Error()
                    {
                        Success = false,
                        Errors = "Token is Invalid"
                    };

                    context.Items["Error"] = Invalid;
                }
            }

            await _next(context);
        }
    }

授权属性

public void OnAuthorization(AuthorizationFilterContext context)
 {
    var Error= (UserModel)context.HttpContext.Items["Error"];
    if (AuthResult != null)
    {
        // not logged in
        context.Result = new JsonResult(new { message = "Unauthorized Access" }) { 
     StatusCode = StatusCodes.Status401Unauthorized };
    }
 }

希望这对您有效。


请不要仅仅发布代码作为答案,还要提供您的代码解决问题的方式和原因的解释。带有解释的答案通常更有帮助和质量更好,并且更有可能吸引赞成票。 - Ran Marciano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接