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

使用JAX-RS和Jersey进行基于REST令牌的身份验证的最佳实践

郗亦
2023-03-14

我正在寻找一种在泽西岛启用基于令牌的身份验证的方法。我试着不使用任何特定的框架。有可能吗?

我的计划是:一个用户注册我的web服务,我的web服务生成一个令牌,发送给客户端,客户端将保留它。则客户端将针对每个请求发送令牌,而不是用户名和密码。

我曾考虑为每个请求使用自定义筛选器和@preauthorize(“hasrole('role')”),但我只是认为这会导致大量请求到数据库以检查令牌是否有效。

或者不创建筛选器并在每个请求中放置一个param令牌?以便每个API首先检查令牌,然后执行一些内容来检索资源。

共有2个答案

通迪
2023-03-14

这个答案是关于授权的,它是我前面关于身份验证的答案的补充

为什么是另一个答案?我试图通过添加关于如何支持JSR-250注释的细节来扩展前面的回答。然而,最初的答案变得太长,超过了30,000个字符的最大长度。因此,我将整个授权细节移到了这个答案上,而让另一个答案专注于执行身份验证和发行令牌。

除了另一个答案中所示的身份验证流之外,在REST端点中还可以支持基于角色的授权。

创建枚举并根据需要定义角色:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

更改以前创建的@securited名称绑定批注以支持角色:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
    Role[] value() default {};
}

然后用@securited注释资源类和方法以执行授权。方法注释将覆盖类注释:

@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // But it's declared within a class annotated with @Secured({Role.ROLE_1})
        // So it only can be executed by the users who have the ROLE_1 role
        ...
    }

    @DELETE
    @Path("{id}")    
    @Produces(MediaType.APPLICATION_JSON)
    @Secured({Role.ROLE_1, Role.ROLE_2})
    public Response myOtherMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
        // The method annotation overrides the class annotation
        // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
        ...
    }
}

创建具有授权优先级的筛选器,该优先级在前面定义的身份验证优先级筛选器之后执行。

ResourceInfo可用于获取将处理请求的资源方法和资源,然后从中提取@securited注释:

@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        // Get the resource class which matches with the requested URL
        // Extract the roles declared by it
        Class<?> resourceClass = resourceInfo.getResourceClass();
        List<Role> classRoles = extractRoles(resourceClass);

        // Get the resource method which matches with the requested URL
        // Extract the roles declared by it
        Method resourceMethod = resourceInfo.getResourceMethod();
        List<Role> methodRoles = extractRoles(resourceMethod);

        try {

            // Check if the user is allowed to execute the method
            // The method annotations override the class annotations
            if (methodRoles.isEmpty()) {
                checkPermissions(classRoles);
            } else {
                checkPermissions(methodRoles);
            }

        } catch (Exception e) {
            requestContext.abortWith(
                Response.status(Response.Status.FORBIDDEN).build());
        }
    }

    // Extract the roles from the annotated element
    private List<Role> extractRoles(AnnotatedElement annotatedElement) {
        if (annotatedElement == null) {
            return new ArrayList<Role>();
        } else {
            Secured secured = annotatedElement.getAnnotation(Secured.class);
            if (secured == null) {
                return new ArrayList<Role>();
            } else {
                Role[] allowedRoles = secured.value();
                return Arrays.asList(allowedRoles);
            }
        }
    }

    private void checkPermissions(List<Role> allowedRoles) throws Exception {
        // Check if the user contains one of the allowed roles
        // Throw an Exception if the user has not permission to execute the method
    }
}

如果用户没有执行操作的权限,则请求将以403(禁止)中止。

要了解正在执行请求的用户,请参阅我前面的回答。您可以从SecurityContext(应该已经在ContainerRequestContext中设置)获取它,也可以使用CDI注入它,这取决于您选择的方法。

如果@securited批注没有声明任何角色,则可以假定所有经过身份验证的用户都可以访问该端点,而不考虑用户所具有的角色。

除了在@securite批注中定义如上所示的角色之外,您还可以考虑JSR-250批注,如@rolesalward@permitall@denyall

JAX-RS不支持这种现成的注释,但它可以通过过滤器来实现。如果您想要支持所有这些,请记住以下几点注意事项:

    方法上的
  • @DenyAll优先于类上的@RolesAllow@PermitAll
  • 方法上的
  • @RolesAllowed优先于类上的@PermitAll
  • 方法上的
  • @PermitAll优先于类上的@RolesAllowed
  • @DenyAll不能附加到类。
  • 类上的
  • @RolesAllowed优先于类上的@PermitAll

因此,检查JSR-250注释的授权筛选器可以如下所示:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        Method method = resourceInfo.getResourceMethod();

        // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
        if (method.isAnnotationPresent(DenyAll.class)) {
            refuseRequest();
        }

        // @RolesAllowed on the method takes precedence over @PermitAll
        RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
            return;
        }

        // @PermitAll on the method takes precedence over @RolesAllowed on the class
        if (method.isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // @DenyAll can't be attached to classes

        // @RolesAllowed on the class takes precedence over @PermitAll on the class
        rolesAllowed = 
            resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
        }

        // @PermitAll on the class
        if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // Authentication is required for non-annotated methods
        if (!isAuthenticated(requestContext)) {
            refuseRequest();
        }
    }

    /**
     * Perform authorization based on roles.
     *
     * @param rolesAllowed
     * @param requestContext
     */
    private void performAuthorization(String[] rolesAllowed, 
                                      ContainerRequestContext requestContext) {

        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            refuseRequest();
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
                return;
            }
        }

        refuseRequest();
    }

    /**
     * Check if the user is authenticated.
     *
     * @param requestContext
     * @return
     */
    private boolean isAuthenticated(final ContainerRequestContext requestContext) {
        // Return true if the user is authenticated or false otherwise
        // An implementation could be like:
        // return requestContext.getSecurityContext().getUserPrincipal() != null;
    }

    /**
     * Refuse the request.
     */
    private void refuseRequest() {
        throw new AccessDeniedException(
            "You don't have permissions to perform this action.");
    }
}

注意:上述实现基于JerseyRolesAllowedDynamicFeature。如果使用Jersey,则不需要编写自己的筛选器,只需使用现有的实现即可。

罗飞宇
2023-03-14

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

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

  • 客户端将其凭据(用户名和密码)发送到服务器。
  • 服务器对凭据进行身份验证,如果有效,则为用户生成令牌。
  • 服务器将先前生成的令牌与用户标识符和过期日期一起存储在某个存储器中。
  • 服务器将生成的令牌发送给客户端。
  • 客户端在每个请求中向服务器发送令牌。
  • 服务器在每个请求中从传入请求中提取令牌。使用该令牌,服务器将查找用户详细信息以执行身份验证。
    • 如果令牌有效,服务器将接受请求。
    • 如果令牌无效,服务器将拒绝请求。

    注意:如果服务器已发出签名令牌(例如JWT,它允许您执行无状态身份验证),则不需要执行步骤3。

    该解决方案仅使用JAX-RS2.0API,避免使用任何特定于供应商的解决方案。因此,它应该与JAX-RS2.0实现一起工作,例如Jersey、RESTEasy和Apache CXF。

    值得一提的是,如果您使用的是基于令牌的身份验证,那么您并不依赖于servlet容器提供的、可通过应用程序的web.xml描述符配置的标准Java EE web应用程序安全html" target="_blank">机制。这是自定义身份验证。

    创建一个JAX-RS资源方法,该方法接收并验证凭据(用户名和密码),并为用户颁发令牌:

    @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
        }
    }
    

    如果验证凭据时抛出任何异常,将返回状态为403(禁止)的响应。

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

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

    username=admin&password=123456
    

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

    public class Credentials implements Serializable {
    
        private String username;
        private String password;
    
        // Getters and setters omitted
    }
    

    然后将其作为JSON使用:

    @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,这是一个元注释,用于创建其他注释,将过滤器和拦截器绑定到资源类和方法。定义@securited批注,如下所示:

    @NameBinding
    @Retention(RUNTIME)
    @Target({TYPE, METHOD})
    public @interface Secured { }
    

    上面定义的名称绑定注释将用于修饰筛选器类,该类实现ContainerRequestFilter,允许您在请求被资源方法处理之前拦截它。ContainerRequestContext可用于访问HTTP请求头,然后提取令牌:

    @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
        }
    }
    

    如果在令牌验证期间发生任何问题,将返回状态为401(未授权)的响应。否则,请求将转到资源方法。

    若要将身份验证筛选器绑定到资源方法或资源类,请使用上面创建的@securited批注对它们进行批注。对于被注释的方法和/或类,将执行过滤器。这意味着只有在使用有效令牌执行请求时才会到达此类端点。

    如果有些方法或类不需要认证,干脆不要注释:

    @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执行请求的用户。可以使用以下方法来实现:

    ContainerRequestFilter.Filter(ContainerRequestContext)方法中,可以为当前请求设置新的SecurityContext实例。然后重写SecurityContext.getUserPrincipal(),返回Principal实例:

    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;
        }
    });
    

    使用令牌查找用户标识符(username),它将是主体的名称。

    在任何JAX-RS资源类中注入SecurityContext

    @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(上下文和依赖注入),它提供了有用的特性,如事件和生产者。

    创建CDI限定符:

    @Qualifier
    @Retention(RUNTIME)
    @Target({ METHOD, FIELD, PARAMETER })
    public @interface AuthenticatedUser { }
    

    在上面创建的authenticationfilter中,注入用@authenticateduser注释的事件:

    @Inject
    @AuthenticatedUser
    Event<String> userAuthenticatedEvent;
    

    如果身份验证成功,则激发将用户名作为参数传递的事件(请记住,令牌是为用户发出的,并且将使用该令牌查找用户标识符):

    userAuthenticatedEvent.fire(username);
    

    很可能在应用程序中有一个代表用户的类。让我们将该类称为user

    创建一个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。使用以下代码段注入user实例(实际上,它是一个CDI代理):

    @Inject
    @AuthenticatedUser
    User authenticatedUser;
    

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

    • CDI:javax.enterprise.inject.produces
    • JAX-RS:javax.ws.rs.produces

    确保您在authenticatedUserProducerbean中使用了CDI@producer注释。

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

    与重写securitycontext的方法相比,CDI方法允许您从bean而不是JAX-RS资源和提供者获取经过身份验证的用户。

    关于如何支持基于角色的授权,请参阅我的另一个答案。

    令牌可以是:

    • 不透明:不显示值本身以外的任何细节(如随机字符串)
    • 自包含:包含令牌本身的详细信息(如JWT)。

    请参阅以下详细信息:

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

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

    JWT(JSON Web令牌)是一种在双方之间安全地表示声明的标准方法,由RFC7519定义。

    它是一个自包含的令牌,它使您能够在声明中存储详细信息。这些声明存储在令牌有效载荷中,它是一个编码为Base64的JSON。以下是RFC7519中注册的一些声明及其含义(阅读RFC全文以了解更多详细信息):

    • ISS:颁发令牌的主体。
    • sub:作为JWT主题的主体。
    • exp:令牌的过期日期。
    • nbf:开始接受令牌进行处理的时间。
    • IAT:发布令牌的时间。
    • JTI:令牌的唯一标识符。

    请注意,不能将敏感数据(如密码)存储在令牌中。

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

    如果不需要跟踪JWT令牌,则不需要持久化它们。尽管如此,通过持久化令牌,您将有可能使它们的访问无效或撤销。为了保持对JWT令牌的跟踪,您可以将令牌标识符(jticlaim)以及其他一些细节(如为哪个用户颁发令牌、过期日期等)保存在服务器上,而不是保存整个令牌。

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

    有几个Java库可以发布和验证JWT令牌,例如:

    • jjwt
    • Java-JWT
    • Jose4j

    要找到一些使用JWT的其他优秀资源,请查看http://JWT.io。

    如果你想撤销代币,你必须保留他们的轨迹。您不需要在服务器端存储整个令牌,只需要存储令牌标识符(必须是唯一的)和一些元数据(如果需要的话)。对于令牌标识符,可以使用uuid。

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

    出于安全目的,当用户更改密码时,吊销用户的所有令牌。

    • 您决定使用哪种类型的身份验证并不重要。始终在HTTPS连接的顶部执行此操作,以防止中间人攻击。
    • 有关令牌的详细信息,请参阅“信息安全”中的此问题。
    • 在本文中,您将找到一些有关基于令牌的身份验证的有用信息。

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

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

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

  • 在REST spring Boot中对用户进行授权和身份验证的最佳实践是什么? 我正在构建一个带有标准页面+REST API的移动应用程序。我看了很多关于Spring Security性的文章,基本上大多数都采用了某种fitler方法,允许或阻止REST调用。然而,在我的例子中,我有一些基于用户是谁的身份验证逻辑。例如,有一个API可以更新用户信息,用户可以更新自己,但不能更新其他人。最初我想使用

  • 如何用Apache CXF实现这一点?

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