在 spring 引导中使用 spring 安全性加盐的最佳做法是什么?

What is the best practice to salt a password with spring security in spring boot?

我正在 java 中为带有 Spring 引导的在线商店创建 REST API,我想将用户密码安全地存储在数据库中, 为此,我使用 spring 安全性附带的 BCrypt,我使用 MySQL 和 JPA-Hibernate 来实现持久性。

而我的实现方式如下:

这是用户实体:

@Entity
@SelectBeforeUpdate
@DynamicUpdate
@Table (name = "USER")
public class User {

    @Id
    @GeneratedValue
    @Column(name = "USER_ID")
    private Long userId;

    @Column(name = "ALIAS")
    private String alias;

    @Column(name = "NAME")
    private String name;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "TYPE")
    private String type;

    @Column(name = "PASSWORD")
    private String password;

    public String getPassword() {
        return password;
    }

    /**
    * When adding the password to the user class the setter asks if it is necessary or not to add the salt, 
    * if this is necessary the method uses the method BCrypt.hashpw (password, salt), 
    * if it is not necessary to add the salt the string That arrives is added intact
    */
    public void setPassword(String password, boolean salt) {
        if (salt) {
            this.password = BCrypt.hashpw(password, BCrypt.gensalt());
        } else {
            this.password = password;
        }
    }

//Setters and Getters and etc.

}

这是用户的仓库 class:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

这是用户class的服务:

@Service
public class UserService{
    private UserRepository userRepository;
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User addEntity(User user) {
      //Here we tell the password setter to generate the salt
        user.setPassword(user.getPassword(), true);
        return userRepository.save(user);
    }

    public User updateEntity(User user) {
        User oldUser = userRepository.findOne(user.getUserId());
        /*
        *This step is necessary to maintain the same password since if we do not do this 
        *in the database a null is generated in the password field, 
        *this happens since the JSON that arrives from the client application does not 
        *contain the password field, This is because to carry out the modification of 
        *the password a different procedure has to be performed
        */
        user.setPassword(oldUser.getPassword(), false);

        return userRepository.save(user);
    }

    /**
     * By means of this method I verify if the password provided by the client application 
     * is the same as the password that is stored in the database which is already saved with the salt, 
     * returning a true or false boolean depending on the case
     */
    public boolean isPassword(Object password, Long id) {
        User user = userRepository.findOne(id);
        //To not create an entity that only has a field that says password, I perform this mapping operation
        String stringPassword = (String)((Map)password).get("password");
        //This method generates boolean
        return BCrypt.checkpw(stringPassword, user.getPassword());
    }

    /**
     *This method is used to update the password in the database
     */
    public boolean updatePassword(Object passwords, Long id) {
        User user = userRepository.findOne(id);
        //Here it receive a JSON with two parameters old password and new password, which are transformed into strings
        String oldPassword = (String)((Map)passwords).get("oldPassword");
        String newPassword = (String)((Map)passwords).get("newPassword");

        if (BCrypt.checkpw(oldPassword, user.getPassword())){
            //If the old password is the same as the one currently stored in the database then the new password is updated 
            //in the database for this a new salt is generated
            user.setPassword(newPassword, true);
            //We use the update method, passing the selected user
            updateEntity(user);
            //We return a true boolean
            return true;
        }else {
            //If the old password check fails then we return a false boolean
            return false;
        }
    }

    //CRUD basic methods omitted because it has no case for the question 
}

这是公开 API 端点的控制器:

@RestController
@CrossOrigin
@RequestMapping("/api/users")
public class UserController implements{
    UserService userService;
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping( value = "", method = RequestMethod.POST )
    public User addEntity(@RequestBody User user) {
        return userService.addEntity(user);
    }

    @RequestMapping( value = "", method = RequestMethod.PUT )
    public User updateEntity(@RequestBody User user) {
        return userService.updateEntity(user);
    }

    @RequestMapping( value = "/{id}/checkPassword", method = RequestMethod.POST )
    public boolean isPassword(@PathVariable(value="id") Long id, @RequestBody Object password) {
        return userService.isPassword(password, id);
    }

    @RequestMapping( value = "/{id}/updatePassword", method = RequestMethod.POST )
    public boolean updatePassword(@PathVariable(value="id") Long id, @RequestBody Object password) {
        return userService.updatePassword(password, id);
    }
}

这就是我的问题所在,我的方法有效,但我觉得这不是最好的方法,我不愿意更改密码 setter 我更愿意保留 setter,因为在用户服务中,我认为有机会以不同方式处理用户和密码更新,所以尝试在实体中使用 @DynamicUpdate 注释,但由于未提供字段,它根本无法正常工作在更新中而不是像空值一样保存它们。

我正在寻找的是使用 Spring Boot.

处理密码安全性的更好方法

首先,您希望在线商店中的每个用户都有一个唯一的字段(f.e。别名或电子邮件),将其用作标识符,而不会将 id 值暴露到最后用户。 另外,据我了解,您想使用 Spring 安全性来保护您的 Web 应用程序。 Spring 安全性使用 ROLE 来指示用户权限 (f.e。ROLE_USER,ROLE_ADMIN)。所以最好有一个字段(一个列表,一个单独的 UserRole 实体)来跟踪用户角色。

假设您向用户字段别名 (private String alias;) 添加了唯一约束并添加了简单的 private String role; 字段。现在您要设置 Spring 安全性以保持 '/shop' 和所有子资源 (f.e. '/shop/search') 对所有人开放,不安全,资源 '/discounts' 可用仅供注册用户使用,资源“/admin”仅供管理员使用。

要实现它,您需要定义几个class。让我们从 UserDetailsS​​ervice 的实现开始(Spring 安全需要获取用户信息):

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

private final UserRepository repository;

@Autowired
public UserDetailsServiceImpl(UserRepository repository) {
    this.repository = repository;
}

@Override
public UserDetails loadUserByUsername(String alias) {
    User user = repository.findByAlias(alias);
    if (user == null) {
        //Do something about it :) AFAIK this method must not return null in any case, so an un-/ checked exception might be a good option
        throw new RuntimeException(String.format("User, identified by '%s', not found", alias));
    }
    return new org.springframework.security.core.userdetails.User(
                           user.getAlias(), user.getPassword(),
                           AuthorityUtils.createAuthorityList(user.getRole()));
  }
}

然后,用于配置 Spring 安全性的主要 class 是一个扩展 WebSecurityConfigurerAdapter(该示例取自具有基于表单的身份验证的应用程序,但您可以调整它满足您的需求):

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;


@Override
protected void configure(HttpSecurity http) throws Exception {
    http
                .authorizeRequests()
                .antMatchers("/", "/shop/**").permitAll()
                .antMatchers("/discounts/**").hasRole("USER")
                .antMatchers("/admin/**").hasRole("ADMIN")
            .and()
                .formLogin()
                .usernameParameter("alias")
                .passwordParameter("password")
                .loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/")
                .permitAll()
            .and()
                .logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID", "remember-me")
                .logoutSuccessUrl("/")
                .permitAll();
}


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

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

}

然后,在您的 UserService 中,您可以使用如下内容:

...
@Autowired
private PasswordEncoder passwordEncoder;

public User addEntity(User user) {
...
    user.setPassword(passwordEncoder.encode(user.getPassword()))
...
}

所有其他检查(f.e。用于登录尝试或访问资源)Spring 安全将根据配置自动执行。还有很多事情需要设置和考虑,但我希望我能够解释总体思路。

编辑

在任何 spring 组件或配置中按如下方式定义 bean

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

然后在您的 UserService 中自动装配它 class

@Service
public class UserService {

    private final UserRepository userRepository;

    private final PasswordEncoder passwordEncoder;

    @Autowired
    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public User addEntity(User user) {
        user.setPassword(passwordEncoder.encode(user.getPassword());
        return userRepository.save(user);
    }

   ...

    public boolean isPassword(Object password, Long id) {
        User user = userRepository.findOne(id);
        String stringPassword = (String)((Map)password).get("password");
        return passwordEncoder.matches(stringPassword, user.getPassword());
    }

    public boolean updatePassword(Object passwords, Long id) {
        User user = userRepository.findOne(id);
        String oldPassword = (String)((Map)passwords).get("oldPassword");
        String newPassword = (String)((Map)passwords).get("newPassword");

        if (!passwordEncoder.matches(oldPassword, newPassword)) {
             return false;
        }
            user.setPassword(passwordEncoder.encode(newPassword));
            updateEntity(user);
            return true;

    }

    ...
}

之后您可以在用户 class 中保持简单 setter。