【问题标题】:"An Authentication object was not found in the SecurityContext" - UserDetailsService Setup in Spring Boot“在 SecurityContext 中找不到身份验证对象” - Spring Boot 中的 UserDetailsS​​ervice 设置
【发布时间】:2017-08-03 23:21:00
【问题描述】:

在 Spring Boot 应用程序中,我有一个内存中的 Spring Security 设置。它可以按需要工作。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("kevin").password("password1").roles("USER").and()
            .withUser("diana").password("password2").roles("USER", "ADMIN");
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    http
            .httpBasic().and()
            .authorizeRequests()
            .antMatchers(HttpMethod.POST, "/foos").hasRole("ADMIN")
            .antMatchers(HttpMethod.PUT, "/foos/**").hasRole("ADMIN")
            .antMatchers(HttpMethod.PATCH, "/foos/**").hasRole("ADMIN")
            .antMatchers(HttpMethod.DELETE, "/foos/**").hasRole("ADMIN")
            .and()
            .csrf().disable();
  }
}

现在,我使用以下代码将其转换为基于数据库的方法。

@Entity
class Account {

  enum Role {ROLE_USER, ROLE_ADMIN}

  @Id
  @GeneratedValue
  private Long id;

  private String userName;

//  @JsonIgnore
  private String password;

  @ElementCollection(fetch = FetchType.EAGER)
  Set<Role> roles = new HashSet<>();
   ...
}

存储库:

@RepositoryRestResource
interface AccountRepository extends CrudRepository<Account, Long>{

  @PreAuthorize("hasRole('USER')")
  Optional<Account> findByUserName(@Param("userName") String userName);
}

UserDetailsS​​ervice:

@Component
class MyUserDetailsService implements UserDetailsService {

  private AccountRepository accountRepository;

  MyUserDetailsService(AccountRepository accountRepository){
    this.accountRepository = accountRepository;
  }

  @Override
  public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
    Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
    if(!accountOptional.isPresent())
        throw new UsernameNotFoundException(name);

    Account account = accountOptional.get();
    return new User(account.getUserName(), account.getPassword(),
        AuthorityUtils.createAuthorityList(account.getRoles().stream().map(Account.Role::name).toArray(String[]::new)));
  }
}

以及WebSecurityConfigurerAdapter配置的修改:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  private MyUserDetailsService userDetailsService;

  SecurityConfiguration(MyUserDetailsService userDetailsService){
    this.userDetailsService = userDetailsService;
  }

  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);  // <-- replacing the in-memory anthentication setup
  }
  ...
}

当我使用 一对用户名和密码作为基本身份验证发送相同的请求时,对于内存版本,我却收到 401 错误:

{
  "timestamp": 1489430818803,
  "status": 401,
  "error": "Unauthorized",
  "message": "An Authentication object was not found in the SecurityContext",
  "path": "/foos"
}

在阅读了一些相关文档和示例代码后,我看不到错误的原因。错误消息说的是用户不在 Spring Security 上下文中。 userDetailsS​​ervice(userDetailsS​​ervice) 中的 AuthenticationManagerBuilder 使用行应该负责在 SecurityContext 中设置这些用户,不是吗?

Spring Boot 版本是 1.4.3.RELEASE。

【问题讨论】:

    标签: spring-boot spring-security


    【解决方案1】:

    crud repository 中删除此preAuthorize 注释。当您尝试登录时使用此方法,但使用 preauthorize 注释它希望用户登录。

      @PreAuthorize("hasRole('USER')")
      Optional<Account> findByUserName(@Param("userName") String userName);
    

    我对@9​​87654326@ 中的configure 方法进行了一些更改 您可能需要在 configure 方法中允许在没有登录权限的情况下访问 signlogin url。

    @Override
      protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeUrls()
            .antMatchers("/signup","/about").permitAll() // #4
            .antMatchers("/admin/**").hasRole("ADMIN") // #6
            .anyRequest().authenticated() // 7
            .and()
        .formLogin()  // #8
            .loginUrl("/login") // #9
            .permitAll(); // #5
      }
    

    在要求管理员权限时要小心/foo/** 这类url 这表明所有以foo 开头的url 只允许管理员使用。

    【讨论】:

    • 感谢您的意见。它在删除 preAuhorize 注释后工作。错误消息与身份验证有关,但与授权无关。此外,错误响应消息中所述的路径是关于 foo,而不是 account。关于URLs安全配置,也可以用http方法定义。
    【解决方案2】:

    一个问题是,您的存储库查找器带有 @PreAuthorize("hasRole('USER')") 注释。在请求可以(完全)验证之前调用此方法,因此即使您通过了正确的凭据,它也永远无法通过检查并引发异常。

    另外一点是,您真的想将您的用户帐户数据公开为 ReST 资源吗?目前,curl http://localhost:8080/accounts 之类的请求会将您的所有用户数据作为 JSON+HAL 响应返回,即使是匿名用户也是如此。

    它会返回类似:

    {
      "_embedded" : {
        "accounts" : [ {
          "userName" : "admin",
          "password" : "admin",
          "roles" : [ "ROLE_USER", "ROLE_ADMIN" ],
          "_links" : {
        "self" : {
          "href" : "http://localhost:8080/accounts/1"
        },
        "account" : {
          "href" : "http://localhost:8080/accounts/1"
        }
          }
        } ]
      },
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/accounts"
        },
        "profile" : {
          "href" : "http://localhost:8080/profile/accounts"
        },
        "search" : {
          "href" : "http://localhost:8080/accounts/search"
        }
      }
    }
    

    所以@RepositoryRestResource 在这里很危险,如果您只想使用数据库表来验证您的请求,则使用@Repository 是一种方法。

    详情请参阅documentation

    即使您按照代码的建议从响应中删除了密码(在您的 Account 类中带有注释的 @JsonIgnore 注释),您也会暴露与安全相关的信息,例如用户名和角色。除此之外,“滥用”您的实体来塑造您的响应对象是一种不好的做法。最好改用Projections

    如果您想保留您的 /accounts 端点,您还应该通过至少将 .antMatchers("/accounts/**").hasRole("ADMIN") 添加到您的配置来保护它。

    【讨论】:

    • 感谢您的意见,凯文。并感谢您提供的信息。就 Account 实体的安全配置而言,我确实将所有代码都放在了这里。我确实有端点安全设置。只有 ADMIN 角色可以访问这些端点,但受到了太多限制。任何人都应该能够创建一个新帐户并对其进行编辑以用于一般用例。
    • 我同意只有控制器才能实现更精细的安全性。例如,创建和编辑操作调用相同的存储库方法 save。虽然应允许任何人创建用户数据,但只有用户本人或具有 ADMIN 角色的人才能编辑用户数据。
    • 好的。 :) 主要是你意识到风险。
    【解决方案3】:

    你真正的问题在于UserDetailService.loadUseByUsername - 你打电话给:

    Optional<Account> accountOptional = this.accountRepository.findByUserName(name);
    

    它是在授权过程中完成的。但是accountRepository 方法是用PreAuthorize 注释的 - 并且只能从包含Authentication Objectcontext 调用。

    做一个简单的测试来理解它:

    @RepositoryRestResource
    interface AccountRepository extends CrudRepository<Account, Long>{
    
      @PreAuthorize("hasRole('USER')")
      Page<Account> findAll(Pageable pageable);
    
      Optional<Account> findByUserName(@Param("userName") String userName);
    }
    

    然后检查,您可以正确查询所有用户。唯一的问题在于findByUserName 方法的递归性质。

    干杯!

    【讨论】:

      猜你喜欢
      • 2017-09-01
      • 2014-12-20
      • 2018-02-15
      • 2015-06-02
      • 2021-04-02
      • 2017-05-10
      • 2015-10-12
      • 2015-10-16
      相关资源
      最近更新 更多