我正在寻找一种在Jersey中启用基于令牌的身份验证的方法 . 我试图不使用任何特定的框架 . 那可能吗?
我的计划是:用户注册我的Web服务,我的Web服务生成令牌,将其发送到客户端,客户端将保留它 . 然后,对于每个请求,客户端将发送令牌而不是用户名和密码 .
我正在考虑为每个请求和 @PreAuthorize("hasRole('ROLE')")
使用自定义过滤器,但我只是认为这会导致很多请求数据库检查令牌是否有效 .
或者不创建过滤器并在每个请求中放置一个参数令牌?这样每个API首先检查令牌,然后执行一些东西来检索资源 .
2 回答
基于令牌的身份验证的工作原理
在基于令牌的身份验证中,客户端为称为令牌的数据交换硬凭证(例如用户名和密码) . 对于每个请求,客户端不会发送硬凭证,而是将令牌发送到服务器以执行身份验证然后授权 .
简而言之,基于令牌的身份验证方案遵循以下步骤:
客户端将其凭据(用户名和密码)发送到服务器 .
服务器对凭据进行身份验证,如果有效,则为该用户生成令牌 .
服务器将先前生成的令牌与用户标识符和到期日期一起存储在某个存储中 .
服务器将生成的令牌发送给客户端 .
客户端在每个请求中将令牌发送到服务器 .
每个请求中的服务器从传入请求中提取令牌 . 使用令牌,服务器查找用户详细信息以执行身份验证 .
如果令牌有效,则服务器接受该请求 .
如果令牌无效,则服务器拒绝该请求 .
执行身份验证后,服务器将执行授权 .
服务器可以提供 endpoints 来刷新令牌 .
注意:如果服务器已发出签名令牌(例如JWT,允许您执行无状态身份验证),则不需要执行步骤3 .
使用JAX-RS 2.0(Jersey,RESTEasy和Apache CXF)可以做些什么
此解决方案仅使用JAX-RS 2.0 API,避免任何特定于供应商的解决方案 . 因此,它应该与JAX-RS 2.0实现一起使用,例如Jersey,RESTEasy和Apache CXF .
值得一提的是,如果使用基于令牌的身份验证,则不依赖于servlet容器提供的标准Java EE Web应用程序安全机制,并且可以通过应用程序的
web.xml
描述符进行配置 . 这是一个自定义身份验证 .使用用户名和密码验证用户并发出令牌
创建一个JAX-RS资源方法,该方法接收并验证凭据(用户名和密码)并为用户发出令牌:
如果在验证凭据时抛出任何异常,将返回状态为
403
(Forbidden)的响应 .如果成功验证凭据,将返回状态为
200
(OK)的响应,并且已发布的令牌将在响应有效内容中发送到客户端 . 客户端必须在每个请求中将令牌发送到服务器 .在使用
application/x-www-form-urlencoded
时,客户端必须在请求有效负载中以以下格式发送凭据:而不是形式参数,可以将用户名和密码包装到类中:
然后将其作为JSON使用:
使用此方法,客户端必须在请求的有效负载中以以下格式发送凭据:
从请求中提取令牌并验证它
客户端应该在请求的标准HTTP
Authorization
标头中发送令牌 . 例如:标准HTTP标头的名称很不幸,因为它带有身份验证信息,而不是授权 . 但是,它是用于将凭据发送到服务器的标准HTTP标头 .
JAX-RS提供@NameBinding,这是一个元注释,用于创建其他注释以将过滤器和拦截器绑定到资源类和方法 . 定义
@Secured
注释如下:上面定义的名称绑定注释将用于修饰实现ContainerRequestFilter的过滤器类,允许您在资源方法处理之前拦截请求 . ContainerRequestContext可用于访问HTTP请求标头,然后提取令牌:
如果在令牌验证期间发生任何问题,将返回状态为
401
(未授权)的响应 . 否则,请求将进入资源方法 .保护REST endpoints
要将身份验证筛选器绑定到资源方法或资源类,请使用上面创建的
@Secured
注释对其进行注释 . 对于方法和/或注释的类,将执行过滤器 . 这意味着只有在使用有效令牌执行请求时才会到达此类 endpoints .如果某些方法或类不需要身份验证,则只需不注释它们:
在上面显示的示例中,过滤器仅针对
mySecuredMethod(Long)
方法执行,因为它使用@Secured
进行了注释 .识别当前用户
您很可能需要知道在REST API中再次执行请求的用户 . 可以使用以下方法来实现它:
覆盖当前请求的安全上下文
在ContainerRequestFilter.filter(ContainerRequestContext)方法中,可以为当前请求设置新的SecurityContext实例 . 然后覆盖SecurityContext.getUserPrincipal(),返回一个Principal实例:
使用令牌查找用户标识符(用户名),这将是Principal的名称 .
在任何JAX-RS资源类中注入SecurityContext:
可以在JAX-RS资源方法中完成相同的操作:
然后得到Principal:
使用CDI(上下文和依赖注入)
如果由于某种原因,您不想覆盖SecurityContext,则可以使用CDI(上下文和依赖注入),它提供有用的功能,如事件和生成器 .
创建CDI限定符:
在上面创建的
AuthenticationFilter
中,使用@AuthenticatedUser
注入Event注释:如果验证成功,则触发将用户名作为参数传递的事件(请记住,为用户颁发令牌,令牌将用于查找用户标识符):
它's very likely that there'是一个代表应用程序中用户的类 . 我们称这个类为
User
.创建一个CDI bean来处理身份验证事件,找到一个带有相应用户名的
User
实例,并将其分配给authenticatedUser
producer字段:authenticatedUser
字段生成一个User
实例,可以将其注入容器托管bean,例如JAX-RS服务,CDI bean,servlet和EJB . 使用以下代码注入一个User
实例(事实上,它是一个CDI代理):请注意,CDI @Produces注释与JAX-RS @Produces注释不同:
CDI:javax.enterprise.inject.Produces
JAX-RS:javax.ws.rs.Produces
确保在
AuthenticatedUserProducer
bean中使用CDI @Produces注释 .这里的关键是使用@RequestScoped注释的bean,允许您在过滤器和bean之间共享数据 . 如果您没有使用事件,则可以修改过滤器以将经过身份验证的用户存储在请求范围的bean中,然后从JAX-RS资源类中读取它 .
与覆盖SecurityContext的方法相比,CDI方法允许您从除JAX-RS资源和提供程序之外的bean获取经过身份验证的用户 .
支持基于角色的授权
有关如何支持基于角色的授权的详细信息,请参阅我的其他answer .
发行令牌
令牌可以是:
Opaque: 除了值本身之外没有显示任何细节(如随机字符串)
Self-contained: 包含有关令牌本身的详细信息(如JWT) .
详情如下:
随机字符串作为标记
可以通过生成随机字符串并将其与用户标识符和到期日期一起持久保存到数据库来发出令牌 . 可以看到如何在Java中生成随机字符串的一个很好的例子here . 你也可以使用:
JWT(JSON Web令牌)
JWT(JSON Web令牌)是一种在两方之间安全地表示索赔的标准方法,由RFC 7519定义 .
它是一个自包含的令牌,它使您可以在声明中存储详细信息 . 这些声明存储在令牌有效载荷中,该载荷是JSON编码为Base64 . 以下是在RFC 7519中注册的一些声明及其含义(请阅读完整的RFC以获取更多详细信息):
iss:发出令牌的委托人 .
sub:作为JWT主题的校长 .
exp:令牌的到期日期 .
nbf:开始接受令牌进行处理的时间 .
iat:发出令牌的时间 .
jti:令牌的唯一标识符 .
请注意,您不得在令牌中存储敏感数据,例如密码 .
客户端可以读取有效负载,并且可以通过验证服务器上的签名来轻松检查令牌的完整性 . 签名是防止令牌被篡改的原因 .
你赢了't need to persist JWT tokens if you don' t需要跟踪它们 . 尽管如此,通过持久存在令牌,您将有可能使其无效并撤销其访问权限 . 要跟踪JWT令牌的跟踪,您可以将令牌标识符(jti claim)与其他一些详细信息(例如您发出令牌的用户,到期日期等)一起保留,而不是将整个令牌保留在服务器上 .
持久令牌时,请始终考虑删除旧令牌防止数据库无限增长 .
使用JWT
有一些Java库可以发布和验证JWT令牌,例如:
jjwt
java-jwt
jose4j
要找到一些与JWT合作的其他优秀资源,请查看http://jwt.io .
使用JWT处理令牌刷新
仅接受有效(和未过期)令牌以进行更新 . 客户有责任在exp索赔中指明的到期日期之前刷新令牌 .
您应该防止令牌无限期刷新 . 请参阅以下几种您可以考虑的方法 .
您可以通过向令牌添加两个声明来保留令牌更新的跟踪(声明名称取决于您):
refreshLimit
:表示可以刷新令牌的次数 .refreshCount
:表示令牌已刷新的次数 .因此,只有在满足以下条件时才刷新令牌:
令牌未过期(
exp >= now
) .令牌刷新的次数少于令牌刷新的次数(
refreshCount < refreshLimit
) .刷新令牌时:
更新到期日期(
exp = now + some-amount-of-time
) .增加令牌刷新的次数(
refreshCount++
) .或者,为了跟踪茶点的数量,您可以声明指示绝对到期日期(其工作方式与上述
refreshLimit
索赔非常相似) . 在绝对有效期之前,可以接受任意数量的茶点 .另一种方法涉及发布一个单独的长期刷新令牌,用于发布短期JWT令牌 .
最好的方法取决于您的要求 .
使用JWT处理令牌撤销
如果要撤消令牌,则必须跟踪它们 . 您不需要在服务器端存储整个令牌,只存储令牌标识符(必须是唯一的)和一些元数据(如果需要) . 对于令牌标识符,您可以使用UUID .
jti声明应该用于在令牌上存储令牌标识符 . 验证令牌时,请通过检查服务器端的令牌标识符的jti声明值来确保它未被撤销 .
出于安全考虑,请在用户更改密码时撤消所有令牌 .
其他信息
您决定使用哪种类型的身份验证无关紧要 . Always 在HTTPS连接的顶部执行以防止man-in-the-middle attack .
有关令牌的更多信息,请查看信息安全中的this question .
In this article您将找到有关基于令牌的身份验证的一些有用信息 .
使用@Secured注释支持基于角色的授权
除了另一个answer中显示的身份验证流程外,REST endpoints 还可以支持基于角色的授权 .
创建枚举并根据您的需要定义角色:
更改之前创建的
@Secured
名称绑定注释以支持角色:然后使用
@Secured
注释资源类和方法以执行授权 . 方法注释将覆盖类注释:创建具有AUTHORIZATION优先级的过滤器,该过滤器在先前定义的AUTHENTICATION优先级过滤器之后执行 .
ResourceInfo可用于获取将处理请求的资源Method和资源Class,然后从中提取
@Secured
注释:如果用户没有执行操作的权限,则使用
403
(禁止)中止请求 .要了解正在执行请求的用户,请参阅my previous answer . 您可以从SecurityContext(应该已经在ContainerRequestContext中设置)获取它,或者使用CDI注入它,具体取决于您的方法 .
如果
@Secured
注释未声明任何角色,则可以假定所有经过身份验证的用户都可以访问该 endpoints ,而忽略用户拥有的角色 .使用JSR-250注释支持基于角色的授权
或者如上所示在
@Secured
注释中定义角色,您可以考虑JSR-250注释,例如@RolesAllowed,@PermitAll和@DenyAll .JAX-RS不支持这种开箱即用的注释,但可以通过过滤器实现 . 如果您想支持所有这些,请记住以下几点注意事项:
方法需要
@DenyAll在课堂上优先于@RolesAllowed和@PermitAll .
该方法的
@RolesAllowed优先于该类的@PermitAll .
该方法的
@PermitAll优先于该类的@RolesAllowed .
@DenyAll无法附加到 class .
类上的
@RolesAllowed优先于类上的@PermitAll .
因此,检查JSR-250注释的授权过滤器可能类似于:
注意:上面的实现基于Jersey RolesAllowedDynamicFeature . 如果您使用Jersey,则无需编写自己的过滤器,只需使用现有实现即可 .