Spring OAuth2 资源服务器中为特定端点添加自定义 Token 授权

本文介绍了如何在 Spring OAuth2 资源服务器中为特定端点实现自定义 Token 授权。通过利用 JWT 的私有声明和 Keycloak 的 mapper 功能,以及自定义的 AbstractAuthenticationToken 实现,可以实现灵活且安全的访问控制策略。文章提供了一种基于订阅数据的访问控制方案,并讨论了如何使用客户端凭据流来认证受信任的客户端。

在标准的 Spring OAuth2 资源服务器配置中,通常使用 JWT 验证来自授权服务器的 Token。然而,在某些情况下,可能需要为特定的端点添加自定义的 Token 验证逻辑。本文将探讨如何实现这种自定义授权,并提供一种基于订阅数据的访问控制方案。

利用 JWT 私有声明实现细粒度访问控制

一种有效的方法是将访问控制所需的所有数据存储在 JWT 中。这需要在授权服务器(例如 Keycloak)上配置,以便将相关数据添加到私有声明中。在 Keycloak 中,可以使用 "mappers" 来实现这一点。

以下是一个 Keycloak mapper 的示例,它查询一个 REST API 来获取订阅数据,并在用户登录时将返回的值作为私有声明添加到 access-token 中:

  1. 创建 REST API: 创建一个 REST API,用于公开订阅数据。该 API 应该能够根据给定的用户 ID 返回该用户的订阅信息和有效期。
  2. 声明 Keycloak 客户端: 在 Keycloak 中声明一个 "confidential" 客户端,并为其分配一个专门的角色,用于访问上述 REST API。
  3. 创建 Keycloak Mapper: 创建一个 Keycloak mapper,该 mapper 使用客户端凭据流,通过上述声明的客户端来调用 REST API。然后,将返回的订阅信息作为私有声明添加到 access-token 中。

通过将订阅信息添加到 access-token 中,可以在资源服务器上使用这些信息来实现细粒度的访问控制。

自定义 AbstractAuthenticationToken

为了更方便地访问和使用 JWT 中的私有声明,可以创建一个自定义的 AbstractAuthenticationToken 实现。这个自定义实现可以将私有声明的解析逻辑封装起来,并提供更易于使用的 API。

以下是一个自定义 AbstractAuthenticationToken 实现的示例:

public class CustomAuthenticationToken extends AbstractAuthenticationToken {

    private final Object principal;
    private final Map attributes;

    public CustomAuthenticationToken(Object principal, Map attributes, Collection authorities) {
        super(authorities);
        this.principal = principal;
        this.attributes = attributes;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public Map getAttributes() {
        return this.attributes;
    }

    public  T getAttribute(String name, Class type) {
        Object value = this.attributes.get(name);
        if (value == null) {
            return null;
        }
        return type.cast(value);
    }
}

然后,在 jwtAuthen

ticationConverter bean 中,返回这个自定义的 AbstractAuthenticationToken 实现,而不是默认的 JwtAuthenticationToken。

@Bean
public Converter jwtAuthenticationConverter() {
    JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
    jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
    return jwt -> {
        // Extract attributes from JWT claims
        Map attributes = jwt.getClaims();
        // Extract authorities from JWT
        Collection authorities = jwtConverter.getJwtGrantedAuthoritiesConverter().convert(jwt);
        // Create custom authentication token
        return new CustomAuthenticationToken(jwt.getSubject(), attributes, authorities);
    };
}

使用 @PreAuthorize 进行访问控制

有了自定义的 AbstractAuthenticationToken 实现,就可以在 @PreAuthorize 注解中使用更具可读性的安全表达式。

以下是一个使用 @PreAuthorize 注解的示例:

@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasSubscription('premium')")
public String getPremiumContent() {
    // ...
}

在这个示例中,只有具有 ROLE_ADMIN 权限或具有 premium 订阅的用户才能访问 getPremiumContent() 方法。

认证受信任的客户端

如果需要认证不受资源所有者("真实用户")上下文约束的受信任客户端,可以使用客户端凭据流从 Keycloak 获取 access-token。

  1. 声明 Keycloak 客户端: 在 Keycloak 中声明 "confidential" 客户端,并为每个客户端创建一个不同的客户端。
  2. 启用客户端凭据: 为这些客户端启用客户端凭据。
  3. 分配角色: 为这些客户端分配所需的角色。
  4. 配置客户端: 配置客户端以使用客户端凭据流从授权服务器获取 access-token,并将 access-token 作为 Bearer authorization header 发送到资源服务器。

从资源服务器的角度来看,这将与使用用户 access-token 的请求没有区别:所有请求都将使用由同一授权服务器颁发的 access-token 进行授权。

总结

通过结合 JWT 私有声明、Keycloak mapper 和自定义 AbstractAuthenticationToken 实现,可以实现灵活且安全的自定义 Token 授权方案。这种方法可以根据用户的订阅信息、角色或其他自定义属性来控制对特定端点的访问。此外,使用客户端凭据流可以认证不受资源所有者上下文约束的受信任客户端。

注意事项:

  • 确保在授权服务器上正确配置 Keycloak mapper,以便将所需的数据添加到 JWT 中。
  • 仔细设计自定义 AbstractAuthenticationToken 实现,以便提供易于使用的 API 来访问 JWT 中的私有声明。
  • 使用 @PreAuthorize 注解时,确保安全表达式的逻辑正确,以避免意外的访问控制漏洞。
  • 在生产环境中,务必保护好客户端凭据,以防止未经授权的访问。