我正在寻找一种方式来启用身份验证基于令牌在泽西。
我是尽量不使用特定的框架。这可能吗?
我想:
在当时的用户报名参加我的web服务,我的web服务生成的令牌,将它发送给客户端,客户端将保留。
然后为每个请求的客户端将不发送用户名或密码,但此令牌。
我在考虑使用自定义过滤器为每个请求和 @ preAuthorize(“hasRole(”角色“)”)的
但我认为这会导致很多的请求到数据库,看看如果令牌是正确的。
和不创建过滤器,并在每个请求把参数令牌?结果
因此,每个API首先检查标记,并执行一些获取资源之后
发布于 2018-02-07 09:31:48
在基于令牌的身份验证中,客户端交换硬证书(例如用户名和密码)令牌对于每个请求,客户端将向服务器发送令牌,以执行身份验证和授权,而不是发送硬凭据。
简而言之,基于令牌的身份验证方案遵循以下步骤:
注:如果服务器已发出签名令牌(如JWT),则不需要步骤3。无国籍认证)。
此解决方案只使用了JAX-RS 2.0 API,避免任何厂商具体的解决办法的。因此,它应该与最流行的JAX-RS 2.0的实现,如新泽西工作,的 RestEasy的和的Apache CXF 。
值得一提的是,如果您使用的是基于令牌的身份验证,不依赖servlet容器提供的标准JavaEEweb应用程序安全机制,并且可以通过应用程序的web.xml
进行自定义身份验证。
创建接收并验证凭据(用户名和密码)的REST端点,并发出用户令牌:
@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
}
}
如果任何异常验证凭证,具有状态响应时发生 401未授权将被退回。
如果资格被成功验证,有状态 200 OK 的响应将返回与所发行令牌发送到客户端的响应。客户端必须发送令牌到服务器的每个请求。
使用这种方法,你的客户端发送的凭据以下列格式的请求体:
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"
}
客户端应该在标准HTTP中发送令牌Authorization
请求头。例如:
Authorization: Bearer <token-goes-here>
需要注意的是标准的HTTP标头的名称是不正确的,因为它承载的验证的信息,而不是的授权的
Jax-RS提供@NameBinding
一种元注释,用于创建其他注释,将过滤器和拦截器绑定到资源类和方法。定义@Secured
注释如下:
@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
(未经授权)将被退回。否则,请求将转到资源方法。
若要将身份验证筛选器绑定到资源方法或资源类,请使用@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
...
您需要知道执行请求的用户是否再次使用休息的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;
}
});
使用令牌查找用户标识符(用户名),这将是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
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
如果身份验证成功,触发传递用户名作为参数的事件(请记住,令牌是为用户发出的,令牌将用于查找用户标识符):
userAuthenticatedEvent.fire(username);
很可能有一个类代表应用程序中的用户。让我们把这门课叫做User
...
创建一个CDIbean来处理身份验证事件,找到一个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服务、CDIbean、servlet和EJB。使用下面的代码注入User
实例(实际上,它是一个CDI代理):
@Inject
@AuthenticatedUser
User authenticatedUser;
注意CDI@Produces
注释是异类从JAX-RS@Produces
注释:
javax.enterprise.inject.Produces
javax.ws.rs.Produces
一定要使用CDI@Produces
注释AuthenticatedUserProducer
豆子。
这里的关键是带注释的bean@RequestScoped
允许您在过滤器和bean之间共享数据。如果不使用事件,则可以修改筛选器,将经过身份验证的用户存储在请求作用域bean中,然后从JAX-RS资源类中读取它。
与重写SecurityContext
,CDI方法允许您从JAX-RS资源和提供者以外的bean中获取经过身份验证的用户。
请参考我的另一个回答有关如何支持基于角色的授权的详细信息.
令牌可以是:
详情见下文:
令牌可以通过生成随机字符串并将其与用户标识符和过期日期一起保存到数据库来发出。可以看到如何在Java中生成随机字符串的一个很好的例子这儿.你也可以利用:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT(JSONWeb令牌)是一种标准方法,用于在双方之间安全地表示索赔,并由RFC 7519...
它是一个独立的令牌,它使您能够将详细信息存储在索赔。这些声明存储在令牌有效负载中,令牌负载是一个JSON,编码为基准64.以下是在RFC 7519以及他们的意思(更多细节,请阅读全文):
请注意,您不能在令牌中存储敏感数据(如密码)。
客户端可以读取有效负载,通过在服务器上验证令牌的签名,可以轻松地检查令牌的完整性。签名是防止令牌被篡改的原因。
如果不需要跟踪JWT令牌,则不需要持久化JWT令牌。尽管如此,通过持久化令牌,您将有可能使其无效并撤销对它们的访问。为了跟踪JWT令牌,而不是将整个令牌持久化在服务器上,您可以持久化令牌标识符(jti
声明)以及其他一些细节,如您签发的令牌、到期日期等。
在持久化令牌时,请始终考虑删除旧令牌,以防止数据库无限期地增长。
有一些Java库可以发出和验证JWT令牌,例如:
要找到其他一些用于使用jWT的优秀资源,请查看http://jwt.io...
接受只用于茶点的有效(和未过期)令牌。中所示的到期日期之前刷新令牌是客户端的响应性。exp
索赔。
您应该防止令牌无限期地刷新。请参阅下面的几个方法,您可以考虑。
您可以通过向令牌添加两个声明(索赔名由您决定)来跟踪令牌刷新:
refreshLimit
::指示可以刷新令牌多少次。refreshCount
::指示刷新令牌的次数。因此,只有在下列条件为真时才刷新令牌:
exp >= now
)。refreshCount < refreshLimit
)。当刷新令牌时:
exp = now + some-amount-of-time
)。refreshCount++
)。或者,为了跟踪茶点的数量,您可以拥有一个指示绝对有效期(它的工作原理非常类似于refreshLimit
(上文所述索赔)。在绝对有效期,任何数目的茶点都是可以接受的。
另一种方法是发出一个单独的长寿命刷新令牌,用于发出短暂的JWT令牌。
最佳方法取决于您的需求。
如果要撤销令牌,则必须跟踪它们。您不需要将整个令牌存储在服务器端,只需要存储令牌标识符(必须是唯一的)和一些元数据(如果需要的话)。对于您可以使用的令牌标识符UUID...
大jti
声明应该用于将令牌标识符存储在令牌上。验证令牌时,请检查jti
针对服务器端的令牌标识符进行索赔。
出于安全考虑,当用户更改密码时,撤销其所有令牌。
https://stackoverflow.com/questions/-100005253
复制相似问题