【问题标题】:How to use custom UserDetailService in Spring OAuth2 Resource Server?如何在 Spring OAuth2 资源服务器中使用自定义 UserDetailService?
【发布时间】:2021-02-13 23:55:06
【问题描述】:

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

如何正确配置Spring Method Security?

我无法(正确)启用 Spring Method Security。我将实体保存在数据库中,并通过 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 根据经过身份验证的用户(通过 OAuth2 JWT)设置 SecurityContext。我想根据从令牌中识别的用户设置一个自定义用户对象(主体)。但是仅仅提供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>

【问题讨论】:

    标签: spring spring-boot spring-security spring-oauth2


    【解决方案1】:

    在您的 ResourceServerConfig 类中,您应该覆盖 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.UserDetailsS​​ervice 实现的引用(通过您的类中的依赖注入 @Autowird),在实现中您应该覆盖方法 loasUserByUsername 以获取数据库中的实际用户并将所需的值传递给 UserDetails 用户,此用户或 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,如示例所示。如何传递所需的值取决于您,这里我从数据库传递用户,在内部实现中我提取了所需的任何值。

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

    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...

    【讨论】:

    • 我的自定义 UserDetailsS​​ervice 仍未被调用。 SecurityContext 包含一个org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken 类型的身份验证对象。身份验证对象的主体类型为class org.springframework.security.oauth2.jwt.Jwt。我想在这里使用自定义主体(通过 UserDetailsS​​ervice)。
    • 在配置方法中你应该添加 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);配置后。 authenticationTokenFilter 是对某些对象的引用,它实现了 OncePerRequestFilter,而该对象又包含 doFilterInternal 方法,您应该覆盖它,并在那里设置您的主体 SecurityContextHolder.getContext().setAuthentication(....)
    • 我编辑了答案以展示如何扩展 OncePerRequestFilter ...
    猜你喜欢
    • 2017-12-10
    • 2018-05-17
    • 2021-12-18
    • 2020-08-02
    • 2021-06-03
    • 2013-12-21
    • 2018-04-14
    • 2017-03-29
    • 2019-06-09
    相关资源
    最近更新 更多