【发布时间】:2015-03-28 05:53:30
【问题描述】:
我对 Web 服务非常陌生。我已经使用 Jersey 2 与 Spring 集成公开了一些 REST 服务。现在我需要使用带有用户名/密码的身份验证来保护这些休息服务。我被告知不要使用 Spring Security。
我不知道该怎么做。我确实在网上搜索过,但各种链接显示了各种实现,我无法决定如何进行。
【问题讨论】:
标签: java rest jersey jax-rs jersey-2.0
我对 Web 服务非常陌生。我已经使用 Jersey 2 与 Spring 集成公开了一些 REST 服务。现在我需要使用带有用户名/密码的身份验证来保护这些休息服务。我被告知不要使用 Spring Security。
我不知道该怎么做。我确实在网上搜索过,但各种链接显示了各种实现,我无法决定如何进行。
【问题讨论】:
标签: java rest jersey jax-rs jersey-2.0
使用用户名和密码进行身份验证的常用方法是使用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
过滤器中的基本流程是这样的
获取Authorization 标头。如果它不存在,请抛出AuthenticationException。在这种情况下,AuthenticationExceptionMapper 将发送标头"WWW-Authenticate", "Basic realm=\"" + e.getRealm() + "\",这是基本身份验证协议的一部分
一旦我们有了标头,我们就对其进行解析以获取 Base64 编码的用户名:密码。然后我们对其进行解码,然后拆分,然后将用户名和密码分开。如果此过程中的任何一个失败,请再次抛出映射到 400 错误请求的 WebApplicationException。
检查用户名和密码。示例源代码仅检查用户名是否为user 和密码是否为password,但您需要在过滤器中使用一些服务来验证此信息。如果其中任何一个失败,请抛出AuthenticationException
如果一切顺利,从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,您应该查看容器的文档以获取有关如何设置它的信息。
【讨论】:
所以这确实是您想要实现的目标。我的情况是让这个东西在移动设备和 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,但这很明显。
【讨论】:
安全主要有两种形式:
保护 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。 应用安全本身几乎是一门职业,但我希望这会有所帮助。
【讨论】:
正如之前的帖子所说,您可以选择不同的选项,但实施的开销也各不相同。从实际的角度来看,如果您打算从这个开始并正在寻找一种简单的实现方式,我建议使用 BASIC 身份验证的基于容器的选项。
如果你使用tomcat,可以设置一个realm,实现起来比较简单。您可以使用 JDBCRealm,它从数据库中的指定列获取用户和密码,并通过 server.xml 和 web.xml 进行配置。 这将在您每次尝试访问应用程序时自动提示您输入凭据。您无需为此执行任何应用程序端实现。
【讨论】:
我现在可以告诉您的是,您已经完成了将 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/
【讨论】: