使用JAX-RS和Jersey进行基于REST令牌的身份验证的最佳做法?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (314)

我正在寻找一种方式来启用身份验证基于令牌在泽西。

我是尽量不使用特定的框架。这可能吗?

我想:

在当时的用户报名参加我的web服务,我的web服务生成的令牌,将它发送给客户端,客户端将保留。

然后为每个请求的客户端将不发送用户名或密码,但此令牌。

我在考虑使用自定义过滤器为每个请求和 @ preAuthorize(“hasRole(”角色“)”)的

但我认为这会导致很多的请求到数据库,看看如果令牌是正确的。

和不创建过滤器,并在每个请求把参数令牌?结果

因此,每个API首先检查标记,并执行一些获取资源之后

提问于
用户回答回答于

基于令牌的身份验证是如何工作的?

在基于令牌的身份验证中,客户端交换硬证书(例如用户名和密码)令牌对于每个请求,客户端将向服务器发送令牌,以执行身份验证和授权,而不是发送硬凭据。

简而言之,基于令牌的身份验证方案遵循以下步骤:

  1. 客户端向服务器发送他们的凭据(用户名和密码)。
  2. 服务器对凭据进行身份验证,如果它们有效,则为用户生成令牌。
  3. 服务器将先前生成的令牌与用户标识符和过期日期一起存储在某些存储中。
  4. 服务器将生成的令牌发送给客户端。
  5. 客户端在每个请求中向服务器发送令牌。
  6. 在每个请求中,服务器从传入请求中提取令牌。使用令牌,服务器查找用户详细信息以执行身份验证。
    • 如果令牌有效,服务器将接受请求。
    • 如果令牌无效,服务器将拒绝请求。
  7. 服务器,在每个请求,提取从传入的请求令牌,查找用户标识符与令牌来获取用户信息做认证/授权。
  8. 如果令牌已到期,服务器生成另一个标记,并将其发送给客户端。。

注:如果服务器已发出签名令牌(如JWT),则不需要步骤3。无国籍认证)。

你能用JAX-RS 2.0做什么(泽西,RESTEasy和ApacheCXF)

此解决方案只使用了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();

使用CDI(上下文和依赖项注入)

如果出于某种原因,您不想覆盖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注释:

一定要使用CDI@Produces注释AuthenticatedUserProducer豆子。

这里的关键是带注释的bean@RequestScoped允许您在过滤器和bean之间共享数据。如果不使用事件,则可以修改筛选器,将经过身份验证的用户存储在请求作用域bean中,然后从JAX-RS资源类中读取它。

与重写SecurityContext,CDI方法允许您从JAX-RS资源和提供者以外的bean中获取经过身份验证的用户。

支持基于角色的授权

请参考我的另一个回答有关如何支持基于角色的授权的详细信息.

发行券

令牌可以是:

  • 不透明的:显示除值本身以外的其他任何细节(如随机字符串)。
  • 自成一体:包含有关令牌本身的详细信息(如JWT)。

详情见下文:

随机字符串作为标记

令牌可以通过生成随机字符串并将其与用户标识符和过期日期一起保存到数据库来发出。可以看到如何在Java中生成随机字符串的一个很好的例子这儿.你也可以利用:

Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);

JWT(JSONWeb令牌)

JWT(JSONWeb令牌)是一种标准方法,用于在双方之间安全地表示索赔,并由RFC 7519...

它是一个独立的令牌,它使您能够将详细信息存储在索赔。这些声明存储在令牌有效负载中,令牌负载是一个JSON,编码为基准64.以下是在RFC 7519以及他们的意思(更多细节,请阅读全文):

  • iss发行令牌的本金。
  • sub::JWT的主题。
  • exp*令牌的到期日期。
  • nbf令牌开始接受处理的时间。
  • iat发行令牌的时间。
  • jti令牌的唯一标识符。

请注意,您不能在令牌中存储敏感数据(如密码)。

客户端可以读取有效负载,通过在服务器上验证令牌的签名,可以轻松地检查令牌的完整性。签名是防止令牌被篡改的原因。

如果不需要跟踪JWT令牌,则不需要持久化JWT令牌。尽管如此,通过持久化令牌,您将有可能使其无效并撤销对它们的访问。为了跟踪JWT令牌,而不是将整个令牌持久化在服务器上,您可以持久化令牌标识符(jti声明)以及其他一些细节,如您签发的令牌、到期日期等。

在持久化令牌时,请始终考虑删除旧令牌,以防止数据库无限期地增长。

使用JWT

有一些Java库可以发出和验证JWT令牌,例如:

要找到其他一些用于使用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针对服务器端的令牌标识符进行索赔。

出于安全考虑,当用户更改密码时,撤销其所有令牌。

补充

  • 您决定使用哪种类型的身份验证并不重要。在HTTPS连接顶部执行此操作,以防止中间人攻击...
  • 看一看这个问题有关令牌的更多信息,请参见信息安全。
  • 在本文中您将发现一些有关基于令牌的身份验证的有用信息。

热门问答

域名注册时写了企业,可以转为个人的吗?

滑稽园扛把子

Swoole · PHP开发工程师 (已认证)

As a PHP Developer
推荐
可以的,操作如下 登录控制台 登录 腾讯云控制台。 选择 “云产品 > 域名与网站 > 域名注册”,进入 “域名服务” 页面,查看已购买的所有域名信息。 修改/过户域名信息 在需要修改域名信息的域名行中,单击【更多】,选择【域名信息修改】。如下图所示: 也可直接单击需要修改域名信...... 展开详请

如何按照上传时间顺序,获取cos bucket 中的object信息?

波斯狗儿对象存储产品经理
推荐
对象存储是 KV 有序存储,只能按对象键 UTF-8 字符顺序排。详细了解对象的概念:https://cloud.tencent.com/document/product/436/13324 如果需要按时间列表,需要在上传时就指定好路径,这样列表的时候也是按顺序的。比如 pho...... 展开详请

云开发环境和开发者自己的服务器能连通吗?

李成熙heyli

腾讯 · 高级工程师 (已认证)

腾讯高级工程师,专注于工程化及性能优化。 https://github.com/lcxfs1991
可以的请参考这份教程: https://github.com/TencentCloudBase/mp-book/blob/master/guide/readme.md#3-%E5%9C%A8%E8%87%AA%E5%B7%B1%E7%9A%84%E6%9C%8D%E5%8A%A1...... 展开详请

腾讯云 COS 怎么才能外链调用 m3u8 到别的网站播放?

滑稽园扛把子

Swoole · PHP开发工程师 (已认证)

As a PHP Developer
推荐
设置公有读私有写:当访问对象时,COS 读取到对象的权限为公有读,此时无论存储桶为何种权限,对象都可以被直接下载 设置步骤 登录 对象存储控制台,选择左侧菜单栏【存储桶列表】,进入存储桶列表页面。单击需要修改对象权限的对应存储桶,进入存储桶。 📷 找到需要设置权限的对象(如 e...... 展开详请

云通信IM 可以发送语音消息吗?

YingJoy_腾讯云+校园合伙人
可以的哦,在云通信IM的文档中有写 消息类型(文本,图片,语音,表情等自定义消息): 文本:最大 1~2k 字节(支持透传特殊字符); 图片:原图/缩略图/大图(支持格式:png/gif/jpeg/jpg/webp); 语音:异步语音消息(语音支持暂无上限); 表情等自定义消息...... 展开详请

Ubuntu搭建的WordPress如何修改php.ini?

滑稽园扛把子

Swoole · PHP开发工程师 (已认证)

As a PHP Developer
推荐
php新手很多不知道怎么查配置文件在哪,这里提供一个很简单的方法 使用 php -i 命令可以打印php的详细信息,可以把这堆东西输出一下 php -i > outputphp.txt,结合 grep 查找命令 php -i| grep php.ini 打印结果如下 Config...... 展开详请

所属标签

扫码关注云+社区