Answers:
在基于令牌的身份验证中,客户端将硬凭证(例如用户名和密码)交换为称为token的数据。对于每个请求,客户端将发送令牌到服务器以执行身份验证,然后授权,而不是发送硬凭证。
简而言之,基于令牌的身份验证方案遵循以下步骤:
注意:如果服务器已发布签名令牌(例如,JWT,它允许您执行无状态,则不需要执行步骤3身份验证)。
该解决方案仅使用JAX-RS 2.0 API,避免了任何特定于供应商的解决方案。因此,它应与JAX-RS 2.0实现(例如Jersey,RESTEasy和Apache CXF)一起使用。
值得一提的是,如果您使用的是基于令牌的身份验证,那么您就不会依赖servlet容器提供的可通过应用程序的配置进行配置的标准Java EE Web应用程序安全性机制。 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
则将返回状态为(禁止)的响应。
如果成功验证了凭据,200
则将返回状态为(OK)的响应,并且已发出的令牌将在响应有效负载中发送给客户端。客户端必须在每个请求中将令牌发送到服务器。
使用时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"
}
客户端应Authorization
在请求的标准HTTP 标头中发送令牌。例如:
Authorization: Bearer <token-goes-here>
标准HTTP标头的名称很不幸,因为它包含身份验证信息,而不是授权。但是,这是用于将凭据发送到服务器的标准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
。
您很有可能需要通过REST 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
。
创建一个CDI bean来处理认证事件,找到一个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服务,CDI Bean,Servlet和EJB。使用以下代码来注入User
实例(实际上,它是CDI代理):
@Inject
@AuthenticatedUser
User authenticatedUser;
请注意,CDI @Produces
注释是不同从JAX-RS @Produces
注解:
javax.enterprise.inject.Produces
javax.ws.rs.Produces
确保@Produces
在AuthenticatedUserProducer
bean中使用CDI 批注。
这里的关键是带有注释的bean @RequestScoped
,允许您在过滤器和bean之间共享数据。如果您不想使用事件,则可以修改过滤器以将经过身份验证的用户存储在请求范围的Bean中,然后从JAX-RS资源类中读取它。
与覆盖的方法相比SecurityContext
,CDI方法使您可以从JAX-RS资源和提供程序之外的bean中获取经过身份验证的用户。
请参阅我的其他答案,以获取有关如何支持基于角色的授权的详细信息。
令牌可以是:
请参阅以下详细信息:
可以通过生成随机字符串并将其与用户标识符和到期日期一起保存到数据库中来发行令牌。如何产生一个随机数串一个很好的例子可以看出这里。您还可以使用:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT(JSON Web令牌)是一种用于在两方之间安全地表示声明的标准方法,由RFC 7519定义。
这是一个自包含的令牌,可让您将详细信息存储在Claims中。这些声明存储在令牌有效载荷中,该有效载荷是JSON编码为Base64。以下是在RFC 7519中注册的一些声明及其含义(有关更多详细信息,请阅读完整的RFC):
请注意,您不得在令牌中存储敏感数据,例如密码。
客户端可以读取有效负载,并且可以通过在服务器上验证其签名轻松地检查令牌的完整性。签名可以防止令牌被篡改。
如果不需要跟踪JWT令牌,则无需持久化它们。虽然如此,通过保留令牌,您将有可能使令牌无效并撤销它们的访问。为了跟踪JWT令牌,您可以将令牌标识符持久化(而不是将整个令牌持久化在服务器上)jti
声明)以及一些其他详细信息(例如,您为其发行令牌的用户,到期日期等)与一样。
保留令牌时,请始终考虑删除旧令牌,以防止数据库无限期增长。
有一些Java库可以发布和验证JWT令牌,例如:
要找到其他一些可以与JWT一起使用的资源,请访问http://jwt.io。
如果要撤消令牌,则必须跟踪它们。您不需要将整个令牌存储在服务器端,只需存储令牌标识符(必须是唯一的)和一些元数据(如果需要)。对于令牌标识符,您可以使用UUID。
的jti
权利要求应该被用来存储在令牌的令牌标识符。验证令牌时,jti
请通过根据服务器端的令牌标识符检查声明的值来确保未将其撤消。
为了安全起见,请在用户更改密码时撤消该用户的所有令牌。
为什么还要另一个答案?我试图通过添加有关如何支持JSR-250批注的详细信息来扩展以前的答案。但是,原始答案太长了,超过了30,000个字符的最大长度。因此,我将整个授权详细信息移到了该答案上,而另一个答案则集中在执行身份验证和颁发令牌上。
@Secured
注释支持基于角色的授权除了其他答案中所示的身份验证流程外,REST端点中还可以支持基于角色的授权。
创建一个枚举并根据需要定义角色:
public enum Role {
ROLE_1,
ROLE_2,
ROLE_3
}
更改@Secured
之前创建的名称绑定注释以支持角色:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
Role[] value() default {};
}
然后注释资源类和方法@Secured
以执行授权。方法注释将覆盖类注释:
@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// But it's declared within a class annotated with @Secured({Role.ROLE_1})
// So it only can be executed by the users who have the ROLE_1 role
...
}
@DELETE
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
@Secured({Role.ROLE_1, Role.ROLE_2})
public Response myOtherMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
// The method annotation overrides the class annotation
// So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
...
}
}
创建具有AUTHORIZATION
优先级的过滤器,该AUTHENTICATION
过滤器在先前定义的优先级过滤器之后执行。
该ResourceInfo
可用于获取资源Method
和资源Class
将处理请求,然后提取@Secured
从他们的注释:
@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the resource class which matches with the requested URL
// Extract the roles declared by it
Class<?> resourceClass = resourceInfo.getResourceClass();
List<Role> classRoles = extractRoles(resourceClass);
// Get the resource method which matches with the requested URL
// Extract the roles declared by it
Method resourceMethod = resourceInfo.getResourceMethod();
List<Role> methodRoles = extractRoles(resourceMethod);
try {
// Check if the user is allowed to execute the method
// The method annotations override the class annotations
if (methodRoles.isEmpty()) {
checkPermissions(classRoles);
} else {
checkPermissions(methodRoles);
}
} catch (Exception e) {
requestContext.abortWith(
Response.status(Response.Status.FORBIDDEN).build());
}
}
// Extract the roles from the annotated element
private List<Role> extractRoles(AnnotatedElement annotatedElement) {
if (annotatedElement == null) {
return new ArrayList<Role>();
} else {
Secured secured = annotatedElement.getAnnotation(Secured.class);
if (secured == null) {
return new ArrayList<Role>();
} else {
Role[] allowedRoles = secured.value();
return Arrays.asList(allowedRoles);
}
}
}
private void checkPermissions(List<Role> allowedRoles) throws Exception {
// Check if the user contains one of the allowed roles
// Throw an Exception if the user has not permission to execute the method
}
}
如果用户没有执行该操作的权限,则该请求将被中止403
(禁止)。
要了解执行请求的用户,请参阅我以前的回答。您可以从中获取SecurityContext
(应该已经在中设置了ContainerRequestContext
),也可以使用CDI注入它,具体取决于您采用的方法。
如果@Secured
注释没有声明角色,则可以假定所有经过身份验证的用户都可以访问该端点,而无需考虑用户具有的角色。
另外,以确定在角色@Secured
如上图所示的注释,你可以考虑JSR-250注解,如@RolesAllowed
,@PermitAll
和@DenyAll
。
JAX-RS不支持现成的注释,但是可以使用过滤器来实现。如果要支持所有这些注意事项,请记住以下几点:
@DenyAll
在方法优先@RolesAllowed
,并@PermitAll
在类。@RolesAllowed
方法上的优先级高于@PermitAll
类。@PermitAll
方法上的优先级高于@RolesAllowed
类。@DenyAll
不能附加到类上。@RolesAllowed
在班级上优先@PermitAll
于班级。因此,用于检查JSR-250批注的授权过滤器可能类似于:
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
// @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
if (method.isAnnotationPresent(DenyAll.class)) {
refuseRequest();
}
// @RolesAllowed on the method takes precedence over @PermitAll
RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
return;
}
// @PermitAll on the method takes precedence over @RolesAllowed on the class
if (method.isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// @DenyAll can't be attached to classes
// @RolesAllowed on the class takes precedence over @PermitAll on the class
rolesAllowed =
resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
}
// @PermitAll on the class
if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// Authentication is required for non-annotated methods
if (!isAuthenticated(requestContext)) {
refuseRequest();
}
}
/**
* Perform authorization based on roles.
*
* @param rolesAllowed
* @param requestContext
*/
private void performAuthorization(String[] rolesAllowed,
ContainerRequestContext requestContext) {
if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
refuseRequest();
}
for (final String role : rolesAllowed) {
if (requestContext.getSecurityContext().isUserInRole(role)) {
return;
}
}
refuseRequest();
}
/**
* Check if the user is authenticated.
*
* @param requestContext
* @return
*/
private boolean isAuthenticated(final ContainerRequestContext requestContext) {
// Return true if the user is authenticated or false otherwise
// An implementation could be like:
// return requestContext.getSecurityContext().getUserPrincipal() != null;
}
/**
* Refuse the request.
*/
private void refuseRequest() {
throw new AccessDeniedException(
"You don't have permissions to perform this action.");
}
}
注意:上面的实现基于Jersey RolesAllowedDynamicFeature
。如果使用Jersey,则不需要编写自己的过滤器,只需使用现有的实现即可。
user_id
== token.userId
或类似的东西,但这很重复。
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.
RESTful如何?