我正在寻找一种在泽西岛启用基于令牌的身份验证的方法。我试图不使用任何特定的框架。那可能吗?
我的计划是:一个用户注册我的web服务,我的web服务生成一个token,发送给客户端,客户端会保留。然后,对于每个请求,客户端将发送令牌而不是用户名和密码。
我正在考虑为每个请求和 使用自定义过滤器@PreAuthorize("hasRole('ROLE')"),但我只是认为这会导致大量请求数据库检查令牌是否有效。
@PreAuthorize("hasRole('ROLE')")
或者不创建过滤器并在每个请求中放置一个参数令牌?这样每个 API 首先检查令牌,然后执行某些操作以检索资源。
在基于令牌的身份验证中,客户端交换 硬凭证 (例如用户名和密码)以获得称为 令牌 的数据。对于每个请求,客户端不会发送硬凭证,而是将令牌发送到服务器以执行身份验证然后授权。
简而言之,基于令牌的身份验证方案遵循以下步骤:
此解决方案仅使用 JAX-RS 2.0 API, 避免使用任何供应商特定的解决方案 。因此,它应该适用于 JAX-RS 2.0 实现,例如Jersey、RESTEasy和Apache CXF。
值得一提的是,如果您使用基于令牌的身份验证,您将不依赖 servlet 容器提供的标准 Java EE Web 应用程序安全机制,并且可以通过应用程序的web.xml描述符进行配置。这是自定义身份验证。
web.xml
创建一个 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将返回状态为 (Forbidden) 的响应。
403
如果凭据成功验证,200将返回状态为 (OK) 的响应,并且发出的令牌将在响应负载中发送到客户端。客户端必须在每个请求中将令牌发送到服务器。
200
消费application/x-www-form-urlencoded时,客户端必须在请求负载中以以下格式发送凭据:
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 }
使用这种方法,客户端必须在请求的有效负载中以以下格式发送凭据:
{ "username": "admin", "password": "123456" }
客户端应在请求的标准 HTTPAuthorization标头中发送令牌。例如:
Authorization
Authorization: Bearer <token-goes-here>
不幸的是,标准 HTTP 标头的名称带有 身份验证 信息,而不是 授权 。但是,它是用于向服务器发送凭据的标准 HTTP 标头。
JAX-RS 提供@NameBinding了一个元注释,用于创建其他注释以将过滤器和拦截器绑定到资源类和方法。定义一个@Secured注解如下:
@NameBinding
@Secured
@NameBinding @Retention(RUNTIME) @Target({TYPE, METHOD}) public @interface Secured { }
上面定义的名称绑定注解将用于装饰一个过滤器类,该类实现ContainerRequestFilter,允许您在请求被资源方法处理之前拦截请求。可ContainerRequestContext用于访问 HTTP 请求标头,然后提取令牌:
ContainerRequestFilter
ContainerRequestContext
@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将返回状态为(未授权)的响应。否则,请求将继续执行资源方法。
401
要将身份验证过滤器绑定到资源方法或资源类,请使用@Secured上面创建的注释对其进行注释。对于被注释的方法和/或类,将执行过滤器。这意味着只有使用有效令牌执行请求时 才会到达此类端点。
如果某些方法或类不需要身份验证,只需不要对其进行注释:
@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)方法执行,因为它使用@Secured.
mySecuredMethod(Long)
您很可能需要知道对您的 REST API 执行请求的用户。可以使用以下方法来实现它:
在您的方法中,可以为当前请求设置ContainerRequestFilter.filter(ContainerRequestContext)一个新实例。SecurityContext然后覆盖SecurityContext.getUserPrincipal(),返回一个Principal实例:
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; } });
使用令牌查找用户标识符(用户名),这将是Principal的名称。
SecurityContext在任何 JAX-RS 资源类中注入:
@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 principal = securityContext.getUserPrincipal(); String username = principal.getName();
如果出于某种原因,您不想覆盖SecurityContext,则可以使用 CDI(上下文和依赖注入),它提供了有用的功能,例如事件和生产者。
创建 CDI 限定符:
@Qualifier @Retention(RUNTIME) @Target({ METHOD, FIELD, PARAMETER }) public @interface AuthenticatedUser { }
在您AuthenticationFilter上面创建的中,注入一个带有Event注释的@AuthenticatedUser:
AuthenticationFilter
Event
@AuthenticatedUser
@Inject @AuthenticatedUser Event<String> userAuthenticatedEvent;
如果身份验证成功,则触发将用户名作为参数传递的事件(请记住,令牌是为用户颁发的,令牌将用于查找用户标识符):
userAuthenticatedEvent.fire(username);
很可能有一个类代表您的应用程序中的用户。让我们称之为这个类User。
User
创建一个 CDI bean 来处理身份验证事件,找到一个User具有对应用户名的实例并将其分配给authenticatedUser生产者字段:
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:
@Produces
javax.enterprise.inject.Produces
javax.ws.rs.Produces
确保在bean中使用 CDI@Produces注释。AuthenticatedUserProducer
AuthenticatedUserProducer
这里的关键是用 注释的 bean @RequestScoped,允许您在过滤器和 bean 之间共享数据。如果您不想使用事件,您可以修改过滤器以将经过身份验证的用户存储在请求范围的 bean 中,然后从您的 JAX-RS 资源类中读取它。
@RequestScoped
与覆盖 的方法相比SecurityContext,CDI 方法允许您从除 JAX-RS 资源和提供者之外的 bean 获取经过身份验证的用户。
有关如何支持基于角色的授权的详细信息,请参阅我的其他答案。
令牌可以是:
请参阅下面的详细信息:
可以通过生成随机字符串并将其与用户标识符和到期日期一起保存到数据库来颁发令牌。可以在这里看到如何在 Java 中生成随机字符串的一个很好的示例。你也可以使用:
Random random = new SecureRandom(); String token = new BigInteger(130, random).toString(32);
JWT(JSON Web Token)是一种在两方之间安全地表示声明的标准方法,由RFC 7519定义。
它是一个独立的令牌,它使您能够在 声明 中存储详细信息。这些声明存储在令牌有效负载中,该令牌有效负载是 JSON 编码为Base64。以下是在RFC 7519中注册的一些声明及其含义(请阅读完整的 RFC 了解更多详细信息):
iss
sub
exp
nbf
iat
jti
请注意,您不得在令牌中存储敏感数据,例如密码。
客户端可以读取有效负载,并且可以通过在服务器上验证其签名来轻松检查令牌的完整性。签名是防止令牌被篡改的原因。
如果您不需要跟踪 JWT 令牌,则无需保留它们。尽管如此,通过持久化令牌,您将有可能使它们失效并撤销它们的访问权限。要跟踪 JWT 令牌,而不是将整个令牌保存在服务器上,您可以保存令牌标识符(jti声明)以及其他一些详细信息,例如您为其颁发令牌的用户、到期日期等。
持久化令牌时,请始终考虑删除旧令牌,以防止数据库无限增长。
有一些 Java 库可以发布和验证 JWT 令牌,例如:
要查找与 JWT 一起使用的其他一些重要资源,请查看http://jwt.io。
如果你想撤销令牌,你必须跟踪它们。您不需要将整个令牌存储在服务器端,只存储令牌标识符(必须是唯一的)和一些元数据(如果需要)。对于令牌标识符,您可以使用UUID。
jti声明应用于将令牌标识符存储在令牌上。jti验证令牌时,请根据您在服务器端拥有的令牌标识符检查声明的值,确保它没有被撤销。
出于安全考虑,请在用户更改密码时撤销其所有令牌。