如何在Spring OAuth2 Resource Server中使用自定义UserDetailService?

xcitsw88  于 12个月前  发布在  Spring
关注(0)|答案(2)|浏览(92)

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

如何正确配置Spring Method Security?

我无法正确启用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 Security将SecurityContext相应地设置为经过身份验证的用户(通过OAuth2 JWT)。我想根据令牌中标识的用户设置一个自定义用户对象(主体)。但是仅仅提供UserDetailsService类型的Bean似乎不起作用。我的UserDetailsService从未被调用.
安全配置

@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>
rsaldnfx

rsaldnfx1#

简单,Spring原生解决方案

如果你想使用自定义用户对象,你需要实现并编译Converter<Jwt,? extends AbstractAuthenticationToken>

@Bean
    public SecurityWebFilterChain filterChain(
        ServerHttpSecurity http,
        MyConverter converter
    ) {
        http.oauth2ResourceServer()
            .jwt()
                .jwtAuthenticationConverter(converter);
        return http.build();
    }

如果你有一个UserDetailsService,你应该考虑使用details而不是principal。从提供的创建一个新的JwtAuthenticationToken,解析权限并使用jwt.setDetails(myUserDetails)。如果您需要自己实现UserDetailsService,那么只实现转换器并使用您自己的AbstractAuthenticationToken变体而不是替换主体可能会更容易。

下一步/考虑

Jwt是一种无状态的身份验证机制。您的转换器将用于每个经过身份验证的请求。你要小心副作用,如API或数据库查找。如果要从数据库或API查询其他用户信息,请考虑创建一个新令牌来加密应用程序所需的所有内容。您也可以考虑缓存。会话缓存可以是一个简单的、有价值的开始,因为缓存主要是关于性能提升的。这是一个关于无国籍的灰色地带。

关于当前接受的答案

有各种各样的问题与公认的答案。最大的问题是,它用一个自定义的身份验证过滤器替换了整个spring资源服务器jwt逻辑。答案甚至提到,你需要连接各种逻辑,比如jwt解析,这需要更深入地理解jwt或spring内部,所以只有当你需要对Authentication Header处理进行强有力的控制时才有意义,但如果你只是想使用自定义的身份验证类,就没有意义了。

62o28rlo

62o28rlo2#

在ResourceServerConfig类中,您应该覆盖configureGlobal和authenticationManagerBean方法,并提供passwordEncoderBean以调用userDeatailsService:

@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中的变量userDetailsService应该持有对org.springframework.security.core.userdetails.UserDetailsService实现的引用(通过类中的依赖注入@Autowird),在实现中你应该重写方法loasUserByData以获取数据库中的实际用户并将所需的值传递给UserDetails user,此用户或Principal将在身份验证管理器中使用:

@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());
}

MyUser类应该实现org.springframework.security.core.userdetails.UserDetails并将所需的值传递给MyUser,如示例所示。如何传递所需的值取决于你,这里我从数据库中传递用户,在实现中我提取了所需的任何值。
您应该在configure方法的末尾添加以下行

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);
}

当然,你应该编写geterName、getJwtToken和isValidToken方法的逻辑,这需要理解JWT token和http header。

相关问题