JWT 发出相同的令牌

Posted

技术标签:

【中文标题】JWT 发出相同的令牌【英文标题】:JWT is issuing the same token 【发布时间】:2017-08-15 21:56:55 【问题描述】:

我正在使用 Jersey 制作一个休息 API。我正在使用java-jwt(https://github.com/auth0/java-jwt) 进行令牌生成工作。请检查以下代码。

UserJSONInfo - REST 方法类

@Path ("/user_info")
public class UserInfoJSONService 

    @POST
    @Path("/authenticateUser")
    @Produces(MediaType.APPLICATION_JSON)
    public Response authenticateUser(Credentials credentials)
    
        UserInfoService service = new UserInfoService();

        try
        UserInfo authenticateUser = service.authenticateUser(credentials.getUserName(), credentials.getPassword());
        String generateToken = service.generateToken(AuthKey.authorizationSecret);

        Authentication auth = new Authentication();
        auth.setIdUser(authenticateUser.getIduser());
        auth.setToken(generateToken);

        return Response.status(Response.Status.OK).entity(auth).build();
        //return authenticateUser;
        
        catch(IndexOutOfBoundsException e)
        
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
         catch (JWTCreationException ex) 
            throw new WebApplicationException(Response.Status.UNAUTHORIZED);
         catch (UnsupportedEncodingException ex) 
            throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
        
    

UserInfoService - 服务层

public class UserInfoService 

    private static UserInfoDAOInterface userDAOInterface;

    public UserInfoService() 
        userDAOInterface = new UserInfoDAOImpl();
    

    public Session getSession() 
        Session session = userDAOInterface.openCurrentSession();
        return session;
    

    public Transaction getTransaction(Session session) 
        Transaction transaction = userDAOInterface.openTransaction(session);
        return transaction;
    



    public UserInfo authenticateUser(String userName, String password)
    
        return authenticate(userName, password);
    

    private UserInfo authenticate(String userName, String password) throws IndexOutOfBoundsException
    
        Session session = userDAOInterface.openCurrentSession();
        Transaction transaction = null;
        UserInfo user = new UserInfo();

        try 
            transaction = userDAOInterface.openTransaction(session);
            user = userDAOInterface.authenticate(userName, password, session);
            transaction.commit();
//         catch (Exception ex) 
//            //ex.printStackTrace();
//            System.out.println("OK");
//        
         finally 
            session.close();
        

        return user;
    

    public String generateToken(String secret) throws JWTCreationException, UnsupportedEncodingException
    
        Token token = new Token();
        return token.issueTokenHMAC256(secret);
    


AuthKey - 只包含secret

public interface AuthKey 
    public static String authorizationSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>ExampleServlet</servlet-name>
        <servlet-class>test.ExampleServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>Jersey RESTful Application</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>rest</param-value>
        </init-param>
        <init-param>
            <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>ExampleServlet</servlet-name>
        <url-pattern>/ExampleServlet</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Jersey RESTful Application</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>    
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

我已将我的令牌生成类作为另一个 java 项目进行维护,并将其作为库导入此处(我使用的是 Netbeans)。下面是代码

package com.xyz.security;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;

/**
 *
 * @author Yohan
 */
public class Token 

    /**
     * Generate the HMAC256 Token
     * @param secret
     *          Secret to generate the token
     * @return 
     *      Token as a String
     * @throws UnsupportedEncodingException
     *      UTF-8 encoding not supported
     * @throws JWTVerificationException 
     *      Invalid Signing configuration / Couldn't convert Claims.
     */
    public String issueTokenHMAC256(String secret) throws UnsupportedEncodingException, JWTCreationException
    

        String token="";
        try 
            Algorithm algorithm = Algorithm.HMAC256(secret);
            token = JWT.create()
                    .withIssuer("auth0")
                    .sign(algorithm);


         catch (UnsupportedEncodingException exception) 
    //UTF-8 encoding not supported
            exception.printStackTrace();
         catch (JWTCreationException exception) 
    //Invalid Signing configuration / Couldn't convert Claims.
            exception.printStackTrace();
        

        return token;
    


    /**
     * Validate a HMAC256 Token
     * @param token
     *          Token you need to validate
     * @param secret
     *          Secret used to generate the token
     * @return
     *          Returns `true` if token is valid.
     * @throws UnsupportedEncodingException
     *          UTF-8 encoding not supported
     * @throws JWTVerificationException 
     *          Invalid Signing configuration / Couldn't convert Claims.
     */
    public boolean validateTokenHMAC256(String token, String secret) throws UnsupportedEncodingException, JWTVerificationException
           
        Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("auth0")
                .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);

        return true;
    

现在的问题是,每次我在用户登录时生成token,我都会得到相同的token。我正在使用POSTMAN 检查 REST 方法,我打开了 3 个选项卡并尝试登录 3 个不同的用户。问题是我得到了相同的令牌!这是正确的还是错误的?在这种情况下,我该如何解决?

【问题讨论】:

你是如何在“AuthKey”中生成这个“authorizationSecret”的? 【参考方案1】:

基于令牌的认证方案是什么

在基于令牌的身份验证方案中,令牌成为用户的凭证。用户名和密码等硬凭证被交换为必须在每个请求中发送的令牌,然后服务器可以执行身份验证/授权。令牌可以在短时间内有效,可以撤销,可以携带范围详细信息(可以使用令牌请求的内容)等。

使用令牌,您必须能够识别以您的 API 为目标的用户。因此,为所有经过身份验证的用户使用一个令牌是没有意义的。

解决您的问题

使用 JWT 后,您可以使用用户名进行声明。还可以考虑为您的令牌添加到期日期(exp 声明)。您不希望您的令牌永远有效,是吗?

对于java-jwt,使用以下内容:

try 

    Algorithm algorithm = Algorithm.HMAC256("secret");
    Date expirationDate = Date.from(ZonedDateTime.now().plusMinutes(60).toInstant());
    String token = JWT.create()
                      .withExpiresAt(expirationDate)
                      .withClaim("username", username)
                      .sign(algorithm);

 catch (UnsupportedEncodingException e)
    // UTF-8 encoding not supported
 catch (JWTCreationException e)
    // Invalid signing configuration / Couldn't convert claims

验证令牌时,您将能够获得username 声明并知道您为谁颁发令牌:

try 

    Algorithm algorithm = Algorithm.HMAC256("secret");
    JWTVerifier verifier = JWT.require(algorithm).build();
    DecodedJWT jwt = verifier.verify(token);

    Claim usernameClaim = jwt.getClaim("username");
    String username = usernameClaim.asString();

 catch (UnsupportedEncodingException e)
    // UTF-8 encoding not supported
 catch (JWTVerificationException e)
    // Invalid signature/claims

使用 JWT 处理令牌刷新

接受有效(未过期)令牌以进行更新。在 exp 声明中指定的到期日期之前刷新令牌是客户的责任。

为避免令牌无限期刷新,您可以通过向令牌添加两个声明来跟踪令牌刷新(声明名称由您决定):

refreshLimit:表示令牌可以刷新多少次。 refreshCount:表示令牌刷新了多少次。

所以只有在满足以下条件时才刷新令牌:

令牌未过期 (exp &gt;= now)。 令牌已刷新的次数小于令牌可刷新的次数 (refreshCount &lt; refreshLimit)。

刷新令牌时:

更新到期日期 (exp = now + some-amount-of-time)。 增加令牌刷新的次数 (refreshCount++)。

一旦令牌被签名并在服务器端验证签名,令牌的内容就不能被客户端篡改。

除了跟踪茶点的数量之外,您还可以声明绝对过期日期。在此日期之前,可以接受任意数量的茶点。

另一种方法涉及发布一个单独的长寿命刷新令牌,用于发布短寿命 JWT 令牌。

最佳方法取决于您的要求。

使用 JWT 处理令牌撤销

如果您想撤销令牌,您必须跟踪它们。您不需要将整个令牌存储在服务器端,只存储令牌标识符(必须是唯一的)和一些元数据(如果需要)。对于令牌标识符,您可以使用 UUID。

jti 声明应用于将令牌标识符存储在令牌本身上。验证令牌时,请根据您在服务器端拥有的令牌标识符检查 jti 声明的值,确保它没有被撤销。

出于安全考虑,请在用户更改密码时撤销所有令牌。


有关 JAX-RS 中基于令牌的身份验证的更多详细信息,请参阅此answer。

【讨论】:

谢谢。验证时,我的validateTokenHMAC256() 也可以输入同样的内容,对吗? 谢谢。我还想问,如果我们得到一个JWT 对象并继续,而不是像JWT.create().withClaim........ 等那样做,那将是非常可定制的,对吗?然后就像jwtobject.withClaim.... 。我在打电话所以无法检查,但这有可能不是吗? @PeakGen 对不起,我不明白你的意思。上述方法有什么问题吗?这就是您使用的 API 的工作原理。 谢谢。一切正常。另一个问题,当令牌过期时,我该如何处理?用户必须登录返回登录屏幕并每隔一小时登录一次? @PeakGen 如果使用无效令牌执行请求,则服务器应以403 状态代码响应,并且客户端应处理该状态代码(例如,显示登录页面)。您还可以允许您的客户刷新他们的令牌。对此没有标准,但我用我通常做的事情更新了我的答案。

以上是关于JWT 发出相同的令牌的主要内容,如果未能解决你的问题,请参考以下文章

黑名单/验证/生成 JWT 刷新令牌

从 Azure AD 发出 JWT 令牌中的角色

在永久存储中跟踪 JWT

text 发出JWT令牌

使用过期令牌发出同时 API 请求时如何避免多个令牌刷新请求

向 Google 进行身份验证时,从 Spring OAuth2 授权服务器发出 JWT 令牌