【问题标题】:User authentication in oauth2 with Spring Boot使用 Spring Boot 在 oauth2 中进行用户身份验证
【发布时间】:2017-10-01 01:19:58
【问题描述】:

我已经使用 Spring Boot 实现了 OAuth 2.0。我将散列密码(包括盐)存储到我的数据库中。

我是 Spring 新手,不知道如何验证用户名/密码。

这是我的 UserDAO 类:

@Service
public class UserDAO implements UserDetailsService{

@Autowired
private LoginDetailsManager loginDetailsManager;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    System.out.println("Get user");
    LoginDetails user = loginDetailsManager.getByUsername(username);
    System.out.println(user.toString());
    if (user == null) {
        // Not found...
        throw new UsernameNotFoundException(
                "User " + username + " not found.");
    }

    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
    List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    grantedAuthorities.add(grantedAuthority);

    String password = user.getPasswordHash();
    String salt = user.getSalt();
    return new UserDetailsImpl(
            user.getUsername(),
            user.getPasswordHash(),
            salt,
            grantedAuthorities);
}
}

这是来自 Application 类的安全配置器:

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security.allowFormAuthenticationForClients();
}

问题:

  1. 如何找回用户输入的密码? UserDetailsS​​ervice 的实现给了我用户输入的用户名。
  2. 是否有处理这种“散列密码+盐”身份验证的内置方法?我错了吗?

【问题讨论】:

  • 我明白了。但是盐呢?我不需要将盐从数据库传递给spring ..吗?该数据库当前是手动填充的。另外,spring怎么知道我使用的是哪种哈希算法?
  • 你可以定义saltSource或者实现自己的passwordEncoder
  • @Downvoter,请对改进发表评论!

标签: java spring authentication spring-boot oauth-2.0


【解决方案1】:

在 Spring Security 中,检索主体、提供盐和分别匹配密码的编码都是相互解耦的。您的 UserDetails 服务基本上是获取主体的正确方法。

您必须提供一个 PasswordEncoder,Spring Boot 中的默认值是 BCrypt,它与您的哈希不匹配。 Spring Boot 和 Spring Security 不再使用旧的密码编码器(ShaPasswordEncoder 等)以及盐源。您必须手动定义它们。

假设您有一个包含盐列的UserDetails,此配置基本上应该可以满足您的需求。如果你不知道怎么做,这里是我即将出版的书中的完整实现ShaPasswordEncoderConfig.java

@Configuration
public class ShaPasswordEncoderConfig 
    extends WebSecurityConfigurerAdapter {

    final UserDetailsService userDetailsService;

    public ShaPasswordEncoderConfig(final UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void configure(
        AuthenticationManagerBuilder auth
    ) {
        DaoAuthenticationProvider authProvider
            = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(
            userDetailsService);
        authProvider.setPasswordEncoder(
            new ShaPasswordEncoder(256));
        authProvider.setSaltSource(
            user -> ((UserWithSalt)user).getSalt());

        auth.authenticationProvider(authProvider);
    }
}

要完成这项工作,您必须扮演自己的 UserDetails 实现角色,而不是使用 Spring Security 本身的那个,因为它没有存储盐的属性。

我在示例中使用的 SaltSource 是一个 lambda,它将 UserDetails 转换为我的实现并获取盐。您还可以使用ReflectionSaltSource,这是一种通过反射获取盐的实现。

编辑

UserDetails 很容易实现,见:

你可以使用这样的实现:

class UserDetailsImpl implements UserDetails {

    private final String username;
    private final String hashedPassword;
    private final String salt;
    private final List<GrantedAuthority> grantedAuthorities;

    public UserDetailsImpl(String username, String hashedPassword, String salt, List<GrantedAuthority> grantedAuthorities) {
        this.username = username;
        this.hashedPassword = hashedPassword;
        this.salt = salt;
        this.grantedAuthorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return hashedPassword;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public String getSalt() {
        return salt;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }   
}

【讨论】:

  • 我有一个疑问。我刚刚创建了一个接口UserDetails,它有getSalt() 方法。我在哪里使用它?我应该创建另一个类“用户”来实现该接口并从 UserDAO 返回一个“用户”实例吗?很困惑。
  • 没有。您扩展 UserDetails 并添加您的 salt 属性以及 getter 和 setter。可以为您想要的任何属性命名。该名称是您在上面配置的名称。您无需直接调用该方法/访问该属性,Spring 会为您执行此操作。
  • 非常感谢,但仍不清楚。 Spring如何知道盐的价值?它是如何从数据库中获取的?它会自动从数据库中提取吗?此外,在通过我自己的 UserDetails 扩展 spring 的 UserDetails 之后,我无法从我的 UserDAO 类返回 User 对象。因为User 实现了spring 的UserDetails 不是我的。
  • 好的。现在我拥有了一切。在应用程序类中启用了安全配置器。 SecurityConfig 类的 Bean 在那里。实现了包含盐属性的 UserDetails。仍然收到错误的凭据响应。似乎盐没有被捡起来。无论如何,谢谢你承受这么远。
  • 不客气 :) 最好的猜测:哈希比较区分大小写。我不知道编码器是否将其编码为十六进制、大写或其他。无论如何,必须与您存储的格式相同。
【解决方案2】:

我最终实现了我自己的 PasswordEncoder 接口并使用本地静态变量通过 db 将盐传递给它。

根据我对控制流的观察,实现是实验性的:

  1. PasswordEncoder 中的客户端密码验证
  2. 如果步骤 1 被清除,loadUserByUsername 在 userDAO。
  3. PasswordEncoder 中的密码验证

public class PasswordEncoderImpl implements PasswordEncoder {

    public static Constants constants = new Constants();

    @Override
    public String encode(CharSequence rawPassword) {
        return DigestUtils.sha256Hex(rawPassword.toString());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        String salt = constants.getSalt();

    /**
     * If salt is null, it means the value in constants object is empty now.
     * => client-secret validation is going on
     * => we just need the comparision of plain-text string.
     */
        if(salt != null) {
//    case of password authentication
            return DigestUtils.sha256Hex(salt + rawPassword).equalsIgnoreCase(encodedPassword);
        } else {
        //case of client-secret authentication
            return rawPassword.equals(encodedPassword);
        }
    }
}

在我的 UserDAO 中,我从持久性中获取用户详细信息,我会在返回用户之前将 salt 存储到对象常量中:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    System.out.println("Get user");
    user = loginDetailsManager.getByUsername(username);
    System.out.println(user.toString());
    if (user == null) {
        // Not found...
        throw new UsernameNotFoundException(
                "User " + username + " not found.");
    }

    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
    List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    grantedAuthorities.add(grantedAuthority);

    constants.set(user.getSalt());

    return new User(
            user.getUsername(),
            user.getPasswordHash(),
            true, true, true, true,
            grantedAuthorities);
}

【讨论】:

  • 真的吗?用户特定状态的全局静态对象?请帮您自己和您的用户一个忙,再看看我的回答和文档。
  • 是的,这是一个可怕的错误。现在使用 threadlocal 来传递盐。
  • 我修正了我的答案。事实上,旧的编码器和盐源不再自动拾取。我真的建议不要像你建议的那样使用盐。
猜你喜欢
  • 1970-01-01
  • 2021-01-21
  • 2019-03-18
  • 2016-08-20
  • 2018-02-08
  • 2019-02-08
  • 2018-08-29
  • 2023-03-19
  • 2022-01-05
相关资源
最近更新 更多