Como a autenticação baseada em token funciona
Na autenticação baseada em token, o cliente troca credenciais concretas (como nome de usuário e senha) por um dado chamado token . Para cada solicitação, em vez de enviar as credenciais físicas, o cliente enviará o token ao servidor para executar autenticação e autorização.
Em poucas palavras, um esquema de autenticação baseado em tokens segue estas etapas:
- O cliente envia suas credenciais (nome de usuário e senha) para o servidor.
- O servidor autentica as credenciais e, se forem válidas, gera um token para o usuário.
- O servidor armazena o token gerado anteriormente em algum armazenamento, juntamente com o identificador do usuário e uma data de validade.
- O servidor envia o token gerado para o cliente.
- O cliente envia o token para o servidor em cada solicitação.
- O servidor, em cada solicitação, extrai o token da solicitação recebida. Com o token, o servidor consulta os detalhes do usuário para executar a autenticação.
- Se o token for válido, o servidor aceitará a solicitação.
- Se o token for inválido, o servidor recusará a solicitação.
- Depois que a autenticação é realizada, o servidor executa a autorização.
- O servidor pode fornecer um terminal para atualizar os tokens.
Nota: A etapa 3 não será necessária se o servidor tiver emitido um token assinado (como JWT, que permite executar autenticação sem estado ).
O que você pode fazer com o JAX-RS 2.0 (Jersey, RESTEasy e Apache CXF)
Esta solução usa apenas a API JAX-RS 2.0, evitando qualquer solução específica do fornecedor . Portanto, ele deve funcionar com implementações JAX-RS 2.0, como Jersey , RESTEasy e Apache CXF .
Vale ressaltar que, se você estiver usando autenticação baseada em token, não estará confiando nos mecanismos de segurança padrão de aplicativos da web Java EE oferecidos pelo contêiner de servlet e configuráveis por meio do web.xml
descritor do aplicativo . É uma autenticação personalizada.
Autenticando um usuário com seu nome de usuário e senha e emitindo um token
Crie um método de recurso JAX-RS que receba e valide as credenciais (nome de usuário e senha) e emita um token para o usuário:
@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
}
}
Se alguma exceção for lançada ao validar as credenciais, uma resposta com o status 403
(Proibido) será retornada.
Se as credenciais forem validadas com êxito, uma resposta com o status 200
(OK) será retornada e o token emitido será enviado ao cliente na carga útil da resposta. O cliente deve enviar o token para o servidor em todas as solicitações.
Ao consumir application/x-www-form-urlencoded
, o cliente deve enviar as credenciais no seguinte formato na carga útil da solicitação:
username=admin&password=123456
Em vez de parâmetros de formulário, é possível agrupar o nome de usuário e a senha em uma classe:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
E consuma-o como 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
}
Usando essa abordagem, o cliente deve enviar as credenciais no seguinte formato na carga útil da solicitação:
{
"username": "admin",
"password": "123456"
}
Extraindo o token da solicitação e validando-o
O cliente deve enviar o token no Authorization
cabeçalho HTTP padrão da solicitação. Por exemplo:
Authorization: Bearer <token-goes-here>
O nome do cabeçalho HTTP padrão é lamentável, pois carrega informações de autenticação , não autorização . No entanto, é o cabeçalho HTTP padrão para o envio de credenciais para o servidor.
O JAX-RS fornece @NameBinding
uma meta-anotação usada para criar outras anotações para vincular filtros e interceptores a classes e métodos de recursos. Defina uma @Secured
anotação da seguinte maneira:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
A anotação de ligação de nome definida acima será usada para decorar uma classe de filtro, que é implementada ContainerRequestFilter
, permitindo que você intercepte a solicitação antes de ser tratada por um método de recurso. O ContainerRequestContext
pode ser usado para acessar os cabeçalhos de solicitação HTTP e, em seguida, extrair o token:
@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
}
}
Se ocorrer algum problema durante a validação do token, uma resposta com o status 401
(Não autorizado) será retornada. Caso contrário, a solicitação prosseguirá para um método de recurso.
Protegendo seus terminais REST
Para vincular o filtro de autenticação a métodos ou classes de recursos, anote-os com a @Secured
anotação criada acima. Para os métodos e / ou classes anotadas, o filtro será executado. Isso significa que esses pontos de extremidade serão alcançados apenas se a solicitação for executada com um token válido.
Se alguns métodos ou classes não precisarem de autenticação, simplesmente não os anote:
@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
...
}
}
No exemplo mostrado acima, o filtro será executado apenas para o mySecuredMethod(Long)
método porque está anotado com @Secured
.
Identificando o usuário atual
É muito provável que você precise conhecer o usuário que está executando a solicitação novamente em sua API REST. As seguintes abordagens podem ser usadas para alcançá-lo:
Substituindo o contexto de segurança da solicitação atual
Dentro do seu ContainerRequestFilter.filter(ContainerRequestContext)
método, uma nova SecurityContext
instância pode ser definida para a solicitação atual. Em seguida, substitua o SecurityContext.getUserPrincipal()
, retornando uma Principal
instância:
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;
}
});
Use o token para procurar o identificador de usuário (nome de usuário), que será o Principal
nome do usuário .
Injete SecurityContext
em qualquer classe de recurso JAX-RS:
@Context
SecurityContext securityContext;
O mesmo pode ser feito em um método de recurso JAX-RS:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
E então obtenha o Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Usando CDI (injeção de contexto e dependência)
Se, por algum motivo, você não quiser substituir o SecurityContext
, poderá usar o CDI (Injeção de Contexto e Dependência), que fornece recursos úteis, como eventos e produtores.
Crie um qualificador CDI:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
No AuthenticationFilter
criado acima, injete uma Event
anotação com @AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Se a autenticação for bem-sucedida, ative o evento que passa o nome de usuário como parâmetro (lembre-se, o token é emitido para um usuário e o token será usado para procurar o identificador do usuário):
userAuthenticatedEvent.fire(username);
É muito provável que exista uma classe que represente um usuário no seu aplicativo. Vamos chamar essa classe User
.
Crie um bean CDI para manipular o evento de autenticação, encontre uma User
instância com o nome de usuário correspondente e atribua-o ao authenticatedUser
campo produtor:
@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
}
}
O authenticatedUser
campo produz uma User
instância que pode ser injetada nos beans gerenciados por contêiner, como serviços JAX-RS, beans CDI, servlets e EJBs. Use o seguinte trecho de código para injetar uma User
instância (de fato, é um proxy CDI):
@Inject
@AuthenticatedUser
User authenticatedUser;
Observe que a @Produces
anotação CDI é diferente da @Produces
anotação JAX-RS :
Certifique-se de usar a @Produces
anotação CDI no seu AuthenticatedUserProducer
bean.
A chave aqui é o bean anotado @RequestScoped
, permitindo que você compartilhe dados entre filtros e seus beans. Se você não quiser usar eventos, poderá modificar o filtro para armazenar o usuário autenticado em um bean com escopo de solicitação e, em seguida, lê-lo em suas classes de recurso JAX-RS.
Comparada à abordagem que substitui a SecurityContext
, a abordagem CDI permite obter o usuário autenticado de beans que não sejam os recursos e provedores JAX-RS.
Suporte à autorização baseada em função
Consulte minha outra resposta para obter detalhes sobre como dar suporte à autorização baseada em função.
Emitindo tokens
Um token pode ser:
- Opaco: não revela detalhes além do próprio valor (como uma sequência aleatória)
- Independente: contém detalhes sobre o próprio token (como JWT).
Veja os detalhes abaixo:
Sequência aleatória como token
Um token pode ser emitido gerando uma sequência aleatória e persistindo em um banco de dados, juntamente com o identificador do usuário e uma data de validade. Um bom exemplo de como gerar uma sequência aleatória em Java pode ser visto aqui . Você também pode usar:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (Token da Web JSON)
O JWT (JSON Web Token) é um método padrão para representar reivindicações de forma segura entre duas partes e é definido pelo RFC 7519 .
É um token independente e permite armazenar detalhes nas reivindicações . Essas declarações são armazenadas na carga útil do token, que é um JSON codificado como Base64 . Aqui estão algumas reivindicações registradas na RFC 7519 e o que elas significam (leia a RFC completa para obter mais detalhes):
iss
: Principal que emitiu o token.
sub
: Principal que é o assunto da JWT.
exp
: Data de validade do token.
nbf
: Hora em que o token começará a ser aceito para processamento.
iat
: Hora em que o token foi emitido.
jti
: Identificador exclusivo para o token.
Esteja ciente de que você não deve armazenar dados confidenciais, como senhas, no token.
A carga útil pode ser lida pelo cliente e a integridade do token pode ser facilmente verificada, verificando sua assinatura no servidor. A assinatura é o que impede que o token seja adulterado.
Você não precisará persistir tokens JWT se não precisar rastreá-los. No entanto, ao persistir os tokens, você terá a possibilidade de invalidar e revogar o acesso a eles. Para manter o controle dos tokens JWT, em vez de persistir em todo o token no servidor, você pode persistir no identificador de token ( jti
reivindicação) junto com alguns outros detalhes, como o usuário para o qual emitiu o token, a data de validade etc.
Ao persistir tokens, sempre considere remover os antigos para impedir que seu banco de dados cresça indefinidamente.
Usando JWT
Existem algumas bibliotecas Java para emitir e validar tokens JWT, como:
Para encontrar outros ótimos recursos para trabalhar com o JWT, consulte http://jwt.io .
Manipulando a revogação de token com JWT
Se você deseja revogar os tokens, deve acompanhá-los. Você não precisa armazenar o token inteiro no lado do servidor, apenas o identificador do token (que deve ser exclusivo) e alguns metadados, se necessário. Para o identificador de token, você pode usar o UUID .
A jti
declaração deve ser usada para armazenar o identificador de token no token. Ao validar o token, verifique se ele não foi revogado, verificando o valor da jti
declaração nos identificadores de token existentes no lado do servidor.
Por motivos de segurança, revogue todos os tokens para um usuário quando ele alterar sua senha.
Informação adicional
- Não importa que tipo de autenticação você decida usar. Sempre faça isso na parte superior de uma conexão HTTPS para impedir o ataque man-in-the-middle .
- Dê uma olhada nesta pergunta da Segurança da informação para obter mais informações sobre tokens.
- Neste artigo, você encontrará algumas informações úteis sobre autenticação baseada em token.
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
Como isso é RESTful?Suporte à autorização baseada em função com a
@Secured
anotaçãoAlém do fluxo de autenticação mostrado na outra resposta , a autorização baseada em função pode ser suportada nos terminais REST.
Crie uma enumeração e defina as funções de acordo com suas necessidades:
Altere a
@Secured
anotação de ligação de nome criada antes para suportar funções:E anote as classes e métodos de recurso
@Secured
para executar a autorização. As anotações do método substituirão as anotações da classe:Crie um filtro com a
AUTHORIZATION
prioridade, que é executado após oAUTHENTICATION
filtro de prioridade definido anteriormente.O
ResourceInfo
pode ser usado para obter o recursoMethod
e recursoClass
que irá lidar com o pedido e, em seguida, extrair os@Secured
anotações a partir deles:Se o usuário não tiver permissão para executar a operação, a solicitação será abortada com um
403
(Proibido).Para conhecer o usuário que está executando a solicitação, consulte minha resposta anterior . Você pode obtê-lo no
SecurityContext
(que já deve estar definido noContainerRequestContext
) ou injetá-lo usando o CDI, dependendo da abordagem a ser adotada.Se uma
@Secured
anotação não possui funções declaradas, você pode assumir que todos os usuários autenticados podem acessar esse terminal, desconsiderando as funções que os usuários têm.Suporte à autorização baseada em função com anotações JSR-250
Como alternativa, para definir as funções na
@Secured
anotação, como mostrado acima, você pode considerar anotações no JSR-250, como@RolesAllowed
,@PermitAll
e@DenyAll
.O JAX-RS não suporta essas anotações prontas para uso, mas pode ser alcançado com um filtro. Aqui estão algumas considerações a serem lembradas se você deseja dar suporte a todas elas:
@DenyAll
no método tem precedência sobre@RolesAllowed
e@PermitAll
na classe.@RolesAllowed
no método tem precedência sobre@PermitAll
a classe.@PermitAll
no método tem precedência sobre@RolesAllowed
a classe.@DenyAll
não pode ser anexado às aulas.@RolesAllowed
na classe tem precedência sobre@PermitAll
a classe.Portanto, um filtro de autorização que verifica as anotações do JSR-250 pode ser como:
Nota: A implementação acima é baseada em Jersey
RolesAllowedDynamicFeature
. Se você usa Jersey, não precisa escrever seu próprio filtro, basta usar a implementação existente.fonte
user_id
==token.userId
ou algo parecido, mas isso é muito repetitivo.