当前位置: 首页 > 知识库问答 >
问题:

如何用JAX-RS和Jersey实现基于REST令牌的身份验证

常永怡
2023-03-14

还是不创建过滤器,并在每个请求中放置参数令牌?以便每个API首先检查令牌,然后执行一些内容以检索资源。

共有1个答案

邓威
2023-03-14

在基于令牌的身份验证中,客户端交换硬凭据(如用户名和密码)以获取称为令牌的数据。对于每个请求,客户端将向服务器发送令牌以执行身份验证和授权,而不是发送硬凭据。

简而言之,基于令牌的身份验证方案遵循以下步骤:

  • 客户端将其凭据(用户名和密码)发送到服务器。
  • 服务器对凭据进行身份验证,如果凭据有效,则为用户生成令牌。
  • 服务器将以前生成的令牌与用户标识符和过期日期一起存储在某个存储器中。
  • 服务器将生成的令牌发送到客户端。
  • 客户端在每个请求中向服务器发送令牌。
  • 服务器在每个请求中从传入请求中提取令牌。使用令牌,服务器查找用户详细信息以执行身份验证。
    • 如果令牌有效,服务器接受请求。
    • 如果令牌无效,服务器将拒绝请求。
    @Path("/authentication")
    public class AuthenticationEndpoint {
    
        @POST
        @Produces(MediaType.APPLICATION_JSON)
        @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
        public Response authenticateUser(@FormParam("username") String username, 
                                         @FormParam("password") String password) {
    
            try {
    
                // Authenticate the user using the credentials provided
                authenticate(username, password);
    
                // Issue a token for the user
                String token = issueToken(username);
    
                // Return the token on the response
                return Response.ok(token).build();
    
            } catch (Exception e) {
                return Response.status(Response.Status.FORBIDDEN).build();
            }      
        }
    
        private void authenticate(String username, String password) throws Exception {
            // Authenticate against a database, LDAP, file or whatever
            // Throw an Exception if the credentials are invalid
        }
    
        private String issueToken(String username) {
            // Issue a token (can be a random String persisted to a database or a JWT token)
            // The issued token must be associated to a user
            // Return the issued token
        }
    }
    

    如果成功验证了凭据,将返回状态为200(OK)的响应,并将发出的令牌发送到响应负载中的客户端。客户端必须在每个请求中向服务器发送令牌。

    当使用application/x-www-form-urlencoded时,客户端必须在请求有效载荷中以以下格式发送凭据:

    username=admin&password=123456
    

    可以将用户名和密码封装到一个类中,而不是表单params:

    public class Credentials implements Serializable {
    
        private String username;
        private String password;
        
        // Getters and setters omitted
    }
    
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response authenticateUser(Credentials credentials) {
    
        String username = credentials.getUsername();
        String password = credentials.getPassword();
        
        // Authenticate the user, issue a token and return a response
    }
    
    json prettyprint-override">{
      "username": "admin",
      "password": "123456"
    }
    

    客户端应该在请求的标准HTTPAuthorization标头中发送令牌。例如:

    Authorization: Bearer <token-goes-here>
    

    标准HTTP标头的名称是不幸的,因为它携带身份验证信息,而不是授权。但是,它是向服务器发送凭据的标准HTTP标头。

    JAX-RS提供@namebinding,这是一个元注释,用于创建其他注释,将过滤器和拦截器绑定到资源类和方法。定义@securite注释,如下所示:

    @NameBinding
    @Retention(RUNTIME)
    @Target({TYPE, METHOD})
    public @interface Secured { }
    
    @Secured
    @Provider
    @Priority(Priorities.AUTHENTICATION)
    public class AuthenticationFilter implements ContainerRequestFilter {
    
        private static final String REALM = "example";
        private static final String AUTHENTICATION_SCHEME = "Bearer";
    
        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
    
            // Get the Authorization header from the request
            String authorizationHeader =
                    requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
    
            // Validate the Authorization header
            if (!isTokenBasedAuthentication(authorizationHeader)) {
                abortWithUnauthorized(requestContext);
                return;
            }
    
            // Extract the token from the Authorization header
            String token = authorizationHeader
                                .substring(AUTHENTICATION_SCHEME.length()).trim();
    
            try {
    
                // Validate the token
                validateToken(token);
    
            } catch (Exception e) {
                abortWithUnauthorized(requestContext);
            }
        }
    
        private boolean isTokenBasedAuthentication(String authorizationHeader) {
    
            // Check if the Authorization header is valid
            // It must not be null and must be prefixed with "Bearer" plus a whitespace
            // The authentication scheme comparison must be case-insensitive
            return authorizationHeader != null && authorizationHeader.toLowerCase()
                        .startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
        }
    
        private void abortWithUnauthorized(ContainerRequestContext requestContext) {
    
            // Abort the filter chain with a 401 status code response
            // The WWW-Authenticate header is sent along with the response
            requestContext.abortWith(
                    Response.status(Response.Status.UNAUTHORIZED)
                            .header(HttpHeaders.WWW_AUTHENTICATE, 
                                    AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
                            .build());
        }
    
        private void validateToken(String token) throws Exception {
            // Check if the token was issued by the server and if it's not expired
            // Throw an Exception if the token is invalid
        }
    }
    

    如果某些方法或类不需要身份验证,只需不对其进行注释即可:

    @Path("/example")
    public class ExampleResource {
    
        @GET
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Response myUnsecuredMethod(@PathParam("id") Long id) {
            // This method is not annotated with @Secured
            // The authentication filter won't be executed before invoking this method
            ...
        }
    
        @DELETE
        @Secured
        @Path("{id}")
        @Produces(MediaType.APPLICATION_JSON)
        public Response mySecuredMethod(@PathParam("id") Long id) {
            // This method is annotated with @Secured
            // The authentication filter will be executed before invoking this method
            // The HTTP request must be performed with a valid token
            ...
        }
    }
    

    在上面显示的示例中,过滤器将只针对MySecuredMethod(Long)方法执行,因为它是用@securite注释的。

    您很可能需要知道是哪个用户在执行REST API的请求。可以使用以下方法来实现:

    final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
    requestContext.setSecurityContext(new SecurityContext() {
    
            @Override
            public Principal getUserPrincipal() {
                return () -> username;
            }
    
        @Override
        public boolean isUserInRole(String role) {
            return true;
        }
    
        @Override
        public boolean isSecure() {
            return currentSecurityContext.isSecure();
        }
    
        @Override
        public String getAuthenticationScheme() {
            return AUTHENTICATION_SCHEME;
        }
    });
    
    @Context
    SecurityContext securityContext;
    

    在JAX-RS资源方法中也可以这样做:

    @GET
    @Secured
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id, 
                             @Context SecurityContext securityContext) {
        ...
    }
    

    然后获取主体:

    Principal principal = securityContext.getUserPrincipal();
    String username = principal.getName();
    

    如果出于某种原因,您不想重写SecurityContext,可以使用CDI(上下文和依赖项注入),它提供了有用的特性,如事件和生产者。

    @Qualifier
    @Retention(RUNTIME)
    @Target({ METHOD, FIELD, PARAMETER })
    public @interface AuthenticatedUser { }
    
    @Inject
    @AuthenticatedUser
    Event<String> userAuthenticatedEvent;
    
    userAuthenticatedEvent.fire(username);
    

    创建一个CDI bean来处理身份验证事件,找到一个具有对应用户名的User实例,并将其分配给AuthenticatedUser生产者字段:

    @RequestScoped
    public class AuthenticatedUserProducer {
    
        @Produces
        @RequestScoped
        @AuthenticatedUser
        private User authenticatedUser;
        
        public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
            this.authenticatedUser = findUser(username);
        }
    
        private User findUser(String username) {
            // Hit the the database or a service to find a user by its username and return it
            // Return the User instance
        }
    }
    

    AuthenticatedUser字段生成一个User实例,该实例可以注入容器托管bean,如JAX-RS服务、CDI bean、servlet和EJB。使用下面的代码插入用户实例(实际上,它是一个CDI代理):

    @Inject
    @AuthenticatedUser
    User authenticatedUser;
    

    请注意,CDI@produces注释不同于JAX-RS@produces注释:

      null

    确保在AuthenticatedUserProducerbean中使用CDI@produces注释。

    这里的关键是用@requestscoped注释的bean,允许您在过滤器和bean之间共享数据。如果您不想不使用事件,可以修改过滤器,将经过身份验证的用户存储在请求范围bean中,然后从JAX-RS资源类中读取它。

    与重写SecurityContext的方法相比,CDI方法允许您从JAX-RS资源和提供者以外的bean获得经过身份验证的用户。

    • 不透明:除了值本身(如随机字符串)之外,不显示其他细节
    • 自包含:包含关于令牌本身的详细信息(如JWT)。

    详见下文:

    可以通过生成随机字符串并将其与用户标识符和过期日期一起持久化到数据库来发布令牌。在这里可以看到如何在Java中生成随机字符串的一个很好的示例。您还可以使用:

    Random random = new SecureRandom();
    String token = new BigInteger(130, random).toString(32);
    
      null

    客户端可以读取有效负载,通过在服务器上验证令牌的签名,可以轻松地检查令牌的完整性。签名可以防止令牌被篡改。

    如果不需要跟踪JWT令牌,就不需要持久化它们。但是,通过持久化令牌,您将有可能使它们的访问无效和撤消。为了跟踪JWT令牌,而不是在服务器上持久化整个令牌,您可以持久化令牌标识符(JTI声明)以及其他一些细节,如为其颁发令牌的用户、过期日期等。

    持久化令牌时,请始终考虑删除旧令牌,以防止数据库无限期增长。

      null

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

    应该使用JTI声明将令牌标识符存储在令牌上。验证令牌时,请根据服务器端的令牌标识符检查JTI声明的值,以确保它没有被撤消。

    出于安全目的,当用户更改密码时,请撤消其所有令牌。

      null

 类似资料:
  • 我正在寻找一种在泽西岛启用基于令牌的身份验证的方法。我试着不使用任何特定的框架。有可能吗? 我的计划是:一个用户注册我的web服务,我的web服务生成一个令牌,发送给客户端,客户端将保留它。则客户端将针对每个请求发送令牌,而不是用户名和密码。 我曾考虑为每个请求使用自定义筛选器和,但我只是认为这会导致大量请求到数据库以检查令牌是否有效。 或者不创建筛选器并在每个请求中放置一个param令牌?以便每

  • 问题内容: 我正在寻找一种在Jersey中启用基于令牌的身份验证的方法。我正在尝试不使用任何特定的框架。那可能吗? 我的计划是:用户注册我的Web服务,我的Web服务生成一个令牌,并将其发送给客户端,客户端将保留它。然后,对于每个请求,客户端将发送令牌,而不是用户名和密码。 我当时在考虑为每个请求使用自定义过滤器,但是我只是认为这会导致对数据库的大量请求检查令牌是否有效。 还是不创建过滤器,并在每

  • 为了测试,我尝试下面的代码过滤包含用户的url参数,但是它没有在未经授权的情况下中止请求。最重要的是,我需要以这样的方式来实现它,即只有更新和删除需要用各自的用户名和密码来授权。其他我只是不想过滤的东西。我有一个user类,它具有username和password(加密)属性。因此,如果url包含Users/{userID}的PUT或delete方法,我希望它使用特定用户的用户名和密码进行验证。我

  • null 我的自定义rest筛选器: 上面的内容实际上会导致应用程序启动时出现一个错误:有人能告诉我如何最好地执行此操作吗?pre_auth筛选器是执行此操作的最好方法吗? 编辑 使用Spring-security实现解决方案 希望它能帮助其他人…

  • 我正在使用C#实现一个REST web服务,它将作为云服务托管在Azure上。因为它是一个REST服务,所以它是无状态的,因此没有cookie或会话状态。 Web服务只能通过HTTPS(由StartSSL. com提供的证书)访问。 用户成功登录服务后,他们将获得一个安全令牌。该令牌将在未来的通信中提供身份验证。 令牌将包含客户端的时间戳、用户ID和ip地址。 所有通信都只能通过HTTPS进行,所

  • 我很少使用jersey实现创建rest服务。出于安全考虑,服务可以由任何一个调用。所以我决定使用基于令牌的认证系统。我在spring security中编写了一个过滤器,它在每个请求到达服务器之前处理它。 创建了一个登录服务,这样用户就可以通过传递有效凭据的用户名和密码来调用该服务,它将生成访问令牌和到期日期,并将其保存在Hashmap和DB中,并作为响应返回给用户。 对于剩余的服务,用户必须传递