【问题标题】:Securing REST services in Jersey在泽西岛保护 REST 服务
【发布时间】:2015-03-28 05:53:30
【问题描述】:

我对 Web 服务非常陌生。我已经使用 Jersey 2 与 Spring 集成公开了一些 REST 服务。现在我需要使用带有用户名/密码的身份验证来保护这些休息服务。我被告知不要使用 Spring Security。

我不知道该怎么做。我确实在网上搜索过,但各种链接显示了各种实现,我无法决定如何进行。

【问题讨论】:

标签: java rest jersey jax-rs jersey-2.0


【解决方案1】:

使用用户名和密码进行身份验证的常用方法是使用Basic Authentication。基本上客户端需要发送一个请求标头Authorization,标头值为Basic Base64Encoded(username:password)。我的用户名也是peeskillet,密码是pass,作为客户,我应该将标题设置为

Authorization: Basic cGVlc2tpbGxldDpwYXNz

在 servlet 环境中,容器应该支持基本身份验证。您将在 web.xml 上配置此支持。您可以在 Java EE 教程的48.2 Securing Web Applications 中看到一个示例。你也会注意到一个例子

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

那是为了 SSL 支持。建议将其用于基本身份验证。

如果您不想处理使用安全域和登录模块、领域等的麻烦,则需要自定义 servlet 支持,或者如果您只是不在 servlet 环境中,在ContainerRequestFilter 中实现基本身份验证真的不是太难。

您可以在jersey/examples/https-clientserver-grizzly 上查看如何完成此操作的完整示例。你应该关注SecurityFilter

过滤器中的基本流程是这样的

  1. 获取Authorization 标头。如果它不存在,请抛出AuthenticationException。在这种情况下,AuthenticationExceptionMapper 将发送标头"WWW-Authenticate", "Basic realm=\"" + e.getRealm() + "\",这是基本身份验证协议的一部分

  2. 一旦我们有了标头,我们就对其进行解析以获取 Base64 编码的用户名:密码。然后我们对其进行解码,然后拆分,然后将用户名和密码分开。如果此过程中的任何一个失败,请再次抛出映射到 400 错误请求的 WebApplicationException

  3. 检查用户名和密码。示例源代码仅检查用户名是否为user 和密码是否为password,但您需要在过滤器中使用一些服务来验证此信息。如果其中任何一个失败,请抛出AuthenticationException

  4. 如果一切顺利,从authenticate 方法创建一个User,并注入到Authorizer(即SecurityContext)中。在 JAX-RS 中,SecurityContext 通常用于授权。

对于授权,如果您想保护某些资源的某些区域,您可以为您的类或方法使用@RolesAllowed 注解。 Jersey 支持此注释,registering the RolesAllowedDynamicFeature

幕后发生的事情是 SecurityContext 将从请求中获得。通过我链接的示例,您可以看到Authorizer,它有一个重写的方法isUserInRole。将调用此方法来检查 @RolesAllowed({"ADMIN"}) 中的值。因此,当您创建 SecurityContext 时,您应该确保在被覆盖的方法中包含用户的角色。

对于测试,您可以简单地使用浏览器。如果一切设置正确,当您尝试访问资源时,您应该会看到(在 Firefox 中)一个对话框,如 this post 所示。如果你使用cURL,你可以这样做

C:/>curl -v -u username:password http://localhost:8080/blah/resource

这将发送一个基本认证请求。由于-v 开关,您应该看到所有涉及的标题。如果你只是想用客户端 API 进行测试,你可以看here 如何设置它。在上面提到的三种情况下,Base64编码都会为你完成,所以你不必担心。

关于 SSL,您应该查看容器的文档以获取有关如何设置它的信息。

【讨论】:

    【解决方案2】:

    所以这确实是您想要实现的目标。我的情况是让这个东西在移动设备和 One-Page-App JavaScript 上运行。

    基本上,您需要做的就是生成某种标头,该值将在您的客户端发出的每个连续请求中都需要。

    所以你做了一个端点,你在其中等待带有用户/密码的帖子:

    @Path("/login")
    public class AuthenticationResource {
    
    @POST
    @Consumes("application/json")
    public Response authenticate(Credentials credential) {
        boolean canBeLoggedIn = (...check in your DB or anywher you need to)
    
        if (canBeLoggedIn) {
            UUID uuid = UUID.randomUUID();
            Token token = new Token();
            token.setToken(uuid.toString());
            //save your token with associated with user
            (...)
    
            return Response.ok(token).type(MediaType.APPLICATION_JSON_TYPE).build();
        } else {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }
    }
    }
    

    现在您需要保护需要该令牌的资源:

       @Path("/payment")
       @AuthorizedWithToken
       public class Payments {
    
        @GET
        @Produces("application/json")
        public Response sync() {
         (...)
        }
    
        }
    

    注意@AuthorizedWithToken 注释。您可以使用特殊的元注释 @NameBinding 自行创建此注释

    @NameBinding
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AuthorizedWithToken {}
    

    现在对于实现标题检查的过滤器:

    @AuthorizedWithToken
    @Provider
    public class XAuthTokenFilter implements ContainerRequestFilter {
    
        private static String X_Auth_Token = "X-Auth-Token";
    
        @Override
        public void filter(ContainerRequestContext crc) throws IOException {
            String headerValue = crc.getHeaderString(X_Auth_Token);
            if (headerValue == null) {
                crc.abortWith(Response.status(Response.Status.FORBIDDEN).entity("Missing " + X_Auth_Token + " value").build());
                return;
            }
    
            if(! TOKEN_FOUND_IN_DB) {
                crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Wrong " + X_Auth_Token + " value").build());
                return;
            }
        }
    }
    

    您可以创建任意数量的自己的注释来检查 http 请求中的各种内容并将它们混合在一起。但是,您需要注意优先级,但这实际上很容易找到。此方法需要使用https,但这很明显。

    【讨论】:

      【解决方案3】:

      安全主要有两种形式:

      • 基于容器
      • 基于应用程序

      保护 Spring 应用程序的标准方法是使用 Spring Security(以前称为 Acegi)。 知道为什么不允许您使用它会很有趣。

      您可以使用基于容器的安全性,但我猜您使用 spring 也排除了该选项。 由于选择 Spring 通常是为了避免使用完整的 J2EE 容器(编辑:尽管正如其他人在下面指出的那样,大多数普通的 servlet 容器确实允许您实现各种基于容器的安全方法)

      这真的只给你一个选择,那就是推出你自己的安全性。

      您对 Jersey 的使用表明这可能是一个 REST 应用程序。 在这种情况下,您真的应该坚持使用标准的 HTTP 身份验证方法 以相反的强度顺序出现以下口味:

      • 基本的
      • 摘要
      • 表格
      • 证书

      REST 应用程序通常应该是“无状态”的,这基本上排除了基于表单的身份验证(因为您需要使用 Session) 为您留下 BASIC、Digest 和 Certificate。

      您的下一个问题是,我在验证谁。如果您可以期望根据用户请求的 URL(例如,如果它是所有用户的一组凭据)知道用户的用户名和密码,那么 Digest 是最好的选择,因为密码永远不会发送,只有哈希。 如果您不知道密码(因为您要求第三方系统对其进行验证等),那么您将被 BASIC 卡住。 但是您可以通过使用 SSL 来增强 BASIC 的安全性,或者更好的是,将 BASIC 与客户端证书身份验证相结合。 事实上,基于 HTTPS 的 BASIC 身份验证是保护大多数 REST 应用程序的标准技术。

      您可以轻松实现一个 Servlet 过滤器,该过滤器会查找身份验证标头并自己验证凭据。 这种过滤器有很多例子,它是一个独立的类文件。 如果未找到凭据,则过滤器返回 401,并在响应标头中传递基本身份验证提示。 如果凭据无效,则返回 403。 应用安全本身几乎是一门职业,但我希望这会有所帮助。

      【讨论】:

      • 谢谢 Richard..正如您在上一段中提到的那样..您的意思是根据可能存储用户名和密码的某些数据库表验证过滤器本身中的凭据吗?
      • 是的。但是如果您在 Spring 中创建过滤器,您可以将任何您需要的东西注入该过滤器,以便实际进行身份验证(数据库连接等)。所以我可能建议你创建一个 spring servlet 过滤器。
      【解决方案4】:

      正如之前的帖子所说,您可以选择不同的选项,但实施的开销也各不相同。从实际的角度来看,如果您打算从这个开始并正在寻找一种简单的实现方式,我建议使用 BASIC 身份验证的基于容器的选项。

      如果你使用tomcat,可以设置一个realm,实现起来比较简单。您可以使用 JDBCRealm,它从数据库中的指定列获取用户和密码,并通过 server.xml 和 web.xml 进行配置。 这将在您每次尝试访问应用程序时自动提示您输入凭据。您无需为此执行任何应用程序端实现。

      【讨论】:

        【解决方案5】:

        我现在可以告诉您的是,您已经完成了将 Jersey 与 Spring 集成的大部分“肮脏”工作。我建议您使用基于应用程序的解决方案,它不会将您绑定到特定容器。 Spring Security 一开始可能很吓人,但是当你驯服这只野兽时,你会发现它实际上是一只友好的小狗。

        事实上,Spring Security 是高度可定制的,只需实现它们的接口即可。并且有很多文档和支持。另外,您已经有了一个基于 Spring 的应用程序。

        由于您所寻求的只是指导,我可以为您提供一些教程。您可以利用此博客。

        http://www.baeldung.com/rest-with-spring-series/ http://www.baeldung.com/2011/10/31/securing-a-restful-web-service-with-spring-security-3-1-part-3/

        【讨论】:

          猜你喜欢
          • 2015-08-21
          • 2016-09-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-06
          • 1970-01-01
          相关资源
          最近更新 更多