Spring OAuth2资源服务器从数据库加载同步用户

0x1*_*C1B 7 java spring-mvc oauth-2.0 spring-boot

初始情况的简要描述:让我们假设一个基于 Spring Boot 的 RESTful API 充当 OAuth2 资源服务器。资源服务器使用 Spring Security 5 和常见方法进行配置。外部授权服务器按照 JWT 声明传递用户信息(例如电子邮件、名字、姓氏),并在客户端进行身份验证时接收到。授权服务器保存的通用用户信息通过资源服务器特定用户信息(例如业务角色、域UID)进行扩展。域用户由来自两个数据源的信息组成:

  • 授权服务器用户数据
  • 资源服务器(应用程序)用户数据

从未通过 JWT 在资源服务器上进行身份验证的新用户将在资源服务器的数据库中创建。资源服务器的数据库相应地包含每个使用资源服务器的API的用户的用户实体,由授权服务器同步的信息和资源服务器的业务逻辑补充的信息组成。

public class User {

    // Information synchronized from Authorization Server
    private String subject;
    private String preferredUsername;
    private String firstName;
    private String lastName;
    private String email;

    // Information added by Resource Server's business logic
    private UUID id;
    private String businessRole;
}
Run Code Online (Sandbox Code Playgroud)

域用户的同步是通过侦听 AuthenticationSuccessEvent 并在数据库中创建用户或根据需要更新用户来执行的。总而言之,对于由主题声明标识的每个 OAuth2 用户,数据库中有一个包含特定于域的附加信息的域配置文件。

本文清晰地描述了这种用户数据的同步和分发。

现在讨论实际问题:虽然 OAuth2 范围控制应用程序拥有哪些权限,但还必须控制用户拥有哪些权限。用户授权是特定于域的,并记录在资源服务器的数据库中。例如,用户应该只能删除他创建的评论。此类访问控制无法通过 OAuth2 范围进行控制。顺便说一句,我正在讨论使用or的 Spring 方法安全性@PreAuthorize@PostAuthorize

@GetMapping
@PreAuthorize("...")
public void func(@AuthenticationPrincipal JWT jwt) {
}
Run Code Online (Sandbox Code Playgroud)

例如,可以通过基于属性的访问控制(ABAC)来控制此类访问。然而,这假设当前@AuthenticationPrincipal不是JWTSpring 资源服务器的标准,而是特定于域的配置文件的实例User。有没有办法将 a 转换JWTUser配置文件,可能使用 a UserDetailsService

Spring Security 5 OAuth2 资源服务器是否有推荐的方法来基于 JWT 从数据库加载信息,更准确地说是User

Cha*_*rly 6

我通常最终得到的是 JWT 转换器并创建自定义会话对象(AbstractAuthenticationToken)

@Autowired
private final UserService userService;

@Override
public void configure(HttpSecurity http) {
    http
      .oauth2ResourceServer().jwt()
      .jwtAuthenticationConverter(new JwtConverter(userService))
      ... other security config ...
Run Code Online (Sandbox Code Playgroud)

智威汤逊转换器:

public class JwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    private final UserService userService;

    public JwtConverter(UserService userService) {
        this.userManager = userService;
    }

    @Override
    public AbstractAuthenticationToken convert(@NotNull final Jwt jwt) {
        // Here is where I usually lazy-create or sync users instead of AuthenticationSuccessEvent
        User user = userService.getByEmail(jwt.getClaim("email"));
        Collection<? extends GrantedAuthority> authorities = translateAuthorities(jwt);
        return new CustomSession(user, jwt, authorities);
    }

    // Translate from your jwt as seen fit (I use a roles claim)
    private static Collection<? extends GrantedAuthority> translateAuthorities(final Jwt jwt) {
        Collection<String> userRoles = jwt.getClaimAsStringList("roles");
        if (userRoles != null)
            return userRoles
                    .stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
                    .collect(Collectors.toSet());
        return Collections.emptySet();
    }
}
Run Code Online (Sandbox Code Playgroud)

CustomSession 的粗略示例:

public class CustomSession extends AbstractAuthenticationToken {
    final private User user;
    final private Jwt jwt;

    public CustomSession(User user, Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.user = user;
        this.jwt  = jwt;
        this.setAuthenticated(true);
    }

    @Override
    public Object getPrincipal() {
        return getUser();
    }

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

    public Jwt getJwt() {
        return jwt;
    }

    public User getUser() {
        return user;
    }
    

    //*********************************************************************
    //* Static Helpers
    //*********************************************************************
    public static Optional<CustomSession> GetSession() {
        return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                .map(s -> s instanceof CustomSession ? (CustomSession) s : null);
    }

    public static Optional<Jwt> GetJwt() {
        return GetSession().map(CustomSession::getJwt);
    }

    public static Optional<User> GetUser() {
        return GetSession().map(CustomSession::getUser);
    }

    public static Optional<String> GetUserId() {
        return GetUser().map(User::getId);
    }

    public static Optional<String> GetUserEmail() {
        return GetUser().map(User::getEmail);
    }
}
Run Code Online (Sandbox Code Playgroud)

就我而言,我在后面使用 JPA,userService.getByEmail()所以需要注意的是 User 对象是“分离的”,因此您不应该使用该对象进行用户更新。

另一种选择是扩展BearerTokenAuthenticationToken并提供一个构造函数来设置权限(而不是 CustomSession)。

====

吃完午饭回来,完全忘记提及权限检查。我介绍一个自定义权限评估器(一个简单的例子来帮助您):

public class CustomPermissionEvaluator implements PermissionEvaluator {
    private SecurityService securityService;

    public CustomPermissionEvaluator(SecurityService securityService) {
        this.securityService = securityService;
    }

    @Override
    public boolean hasPermission(Authentication auth, Object resourceId, Object action) {
        if ((auth == null)
                || !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
                || !(action instanceof String) || StringUtils.isBlank((String) action)){
            return false;
        }
        try {
            return securityService.hasAccess((String) resourceId, (String) action);
        } catch(Exception e){
            return false;
        }
    }

    @Override
    public boolean hasPermission(Authentication auth, Serializable resourceId, String component, Object action) {
        if ((auth == null)
                || !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
                || StringUtils.isBlank(component)
                || !(action instanceof String) || StringUtils.isBlank((String) action)) {
            return false;
        }
        try {
            return securityService.hasAccess((String) resourceId, component, (String) action);
        } catch(Exception e){
            return false;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

配置通过:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Autowired SecurityService securityService;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(securityService));
        return expressionHandler;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后使用@PreAuthorize("hasPersmission(...)"")注释,例如:

// Can I Create {SubDomain} on resourceId
@PreAuthorize("hasPermission(#resourceId, 'SubDomain', 'CREATE')")
void example1(int resourceId) {...}    

// Can I Update resource
@PreAuthorize("hasPermission(#resourceId, 'UPDATE')")
Run Code Online (Sandbox Code Playgroud)

可能不完全是您正在寻找的,而是另一个(我认为干净的)解决方案。