如何在 Spring OAuth2 资源服务器中使用自定义 UserDetailService?

How to use custom UserDetailService in Spring OAuth2 Resource Server?

我正在使用 Spring Boot (2.3.4.RELEASE) 来实现充当 OAuth2 资源服务器的网络服务。到目前为止,我能够保护所有端点并确保存在有效令牌。在下一步中,我想使用 Spring 方法安全性。第三步是填充自定义用户详细信息(通过 UserDetailsService)。

如何正确配置 Spring 方法安全性?

我无法(正确)启用 Spring 方法安全性。我将实体保存在数据库中,还通过 MutableAclService 设置了权限。创建新资源没问题。

我在 读取 实体时收到以下错误

o.s.s.acls.AclPermissionEvaluator        : Checking permission 'OWNER' for object 'org.springframework.security.acls.domain.ObjectIdentityImpl[Type: io.mvc.webserver.repository.entity.ProjectEntity; Identifier: my-second-project]'
o.s.s.acls.AclPermissionEvaluator        : Returning false - no ACLs apply for this principal
o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@120d62d, returned: -1
o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.access.vote.RoleVoter@429b9eb9, returned: 0
o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.access.vote.AuthenticatedVoter@65342bae, returned: 0
o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.security.access.AccessDeniedException: Zugriff verweigert
o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is not anonymous); delegating to AccessDeniedHandler

我使用以下表达式:

@PreAuthorize("hasPermission(#projectKey, 'io.mvc.webserver.repository.entity.ProjectEntity', 'OWNER')")
ProjectEntity findByKey(String projectKey);

如何提供自定义用户详情服务?

据我了解 Spring 安全性相应地为经过身份验证的用户设置 SecurityContext(通过 OAuth2 JWT)。我想根据令牌中识别的用户设置自定义用户对象(主体)。但是仅仅提供一个 UserDetailsService 类型的 Bean 似乎是行不通的。我的 UserDetailsS​​ervice 从未被调用过...

安全配置

@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .httpBasic().disable()
                .formLogin().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests(authorize -> authorize
                    .antMatchers("/actuator/**").permitAll() // TODO: Enable basic auth for actuator
                    .anyRequest().authenticated()
                )
                .oauth2ResourceServer().jwt();
    }
}

ACL 配置

@Configuration
public class AclConfiguration {
    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);

        return expressionHandler;
    }

    @Bean
    public PermissionEvaluator permissionEvaluator(PermissionFactory permissionFactory, AclService aclService) {
        AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService);
        permissionEvaluator.setPermissionFactory(permissionFactory);

        return permissionEvaluator;
    }

    @Bean
    public PermissionFactory permissionFactory() {
        return new DefaultPermissionFactory(MvcPermission.class);
    }

    @Bean
    public MutableAclService aclService(LookupStrategy lookupStrategy, AclCache aclCache, AclRepository aclRepository) {
        return new MongoDBMutableAclService(aclRepository, lookupStrategy, aclCache);
    }

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(
                new SimpleGrantedAuthority("ROLE_ADMIN"));
    }

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }

    @Bean
    public AclCache aclCache(PermissionGrantingStrategy permissionGrantingStrategy,
                             AclAuthorizationStrategy aclAuthorizationStrategy,
                             EhCacheFactoryBean ehCacheFactoryBean) {
        return new EhCacheBasedAclCache(
                ehCacheFactoryBean.getObject(),
                permissionGrantingStrategy,
                aclAuthorizationStrategy
        );
    }

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(ehCacheManagerFactoryBean.getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    }

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager() {
        EhCacheManagerFactoryBean cacheManagerFactory = new EhCacheManagerFactoryBean();
        cacheManagerFactory.setShared(true);
        return cacheManagerFactory;
    }

    @Bean
    public LookupStrategy lookupStrategy(MongoTemplate mongoTemplate,
                                         AclCache aclCache,
                                         AclAuthorizationStrategy aclAuthorizationStrategy) {
        return new BasicMongoLookupStrategy(
                mongoTemplate,
                aclCache,
                aclAuthorizationStrategy,
                new ConsoleAuditLogger()
        );
    }
}

依赖关系

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.11</version>
</dependency>

在您的 ResourceServerConfig class 中,您应该覆盖 configureGlobal 和 authenticationManagerBean 方法,并提供 passwordEncoderBean 以调用您的 userDeatailsS​​ervice:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoderBean());
}

@Bean
public PasswordEncoder passwordEncoderBean() {
    return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

configureGlobal 中的变量 userDetailsS​​ervice 应该持有对 org.springframework.security.core.userdetails.UserDetailsService 实现的引用(通过依赖注入 @Autowird 在你的 class 中),在实现中你应该覆盖方法 loasUserByUsername 以获取数据库中的实际用户并将所需的值传递给 UserDetails 用户,此用户或委托人将在身份验证管理器中使用:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional<UserFromDb> user = userRepository.findByUsername(username);
    if (!user.isPresent()) {
        throw new UsernameNotFoundException("User not found!");
    }
    return new MyUser(user.get());
}

class MyUser 应该实现 org.springframework.security.core.userdetails.UserDetails 并将所需的值传递给 MyUser,如示例所示。如何传递所需的值取决于您,这里我从数据库传递了用户,并在实现内部提取了所需的任何值。

您应该将以下行添加到配置方法的末尾

http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

authenticationTokenFilter是实现了OncePerRequestFilter的类型,你应该重写方法doFilterInternal:

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse, FilterChain filterChain)
        throws ServletException, IOException {
    
    
    final String requestTokenHeader = httpServletRequest.getHeader("Authorization");//sometime it's lowercase: authorization

    String username = getUserName(requestTokenHeader);
    String jwtToken = getJwtToken(requestTokenHeader);
    
    if (username != null) {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
        if (isValidToken(jwtToken, userDetails)) {
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        }
    }

    filterChain.doFilter(httpServletRequest, httpServletResponse);
}

getUserName、getJwtToken、isValidToken方法的逻辑当然要自己写,需要了解JWT token和http headers...