【问题标题】:What is the best practice to salt a password with spring security in spring boot?在 Spring Boot 中使用 Spring Security 对密码进行加盐的最佳做法是什么?
【发布时间】:2017-10-07 06:07:52
【问题描述】:

我正在使用 Spring Boot 为在线商店在 java 中创建一个 REST API,我想将用户密码安全地存储在数据库中, 为此,我使用 Spring Security 附带的 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.

}

这是用户类的存储库:

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

这是用户类的服务:

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

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

我正在寻找的是使用 Spring Boot 处理密码安全性的更好方法。

【问题讨论】:

    标签: java jpa spring-boot spring-security salt


    【解决方案1】:

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

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

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

    @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 Security 的主类是一个扩展 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()))
    ...
    }
    

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

    编辑

    在任何 spring 组件或配置中定义 bean,如下所示

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

    然后在你的 UserService 类中自动装配它

    @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;
    
        }
    
        ...
    }
    

    之后,您可以在 User 类中保留简单的 setter。

    【讨论】:

    • 您的回答很有意义,因为您认为正在遵循 MVC 模型并且我正在从 thymeleaf 之类的东西生成视图,情况是访问控制由客户端处理一方面,为此使用了变量type,API 只需要正确地将密码存储在数据库中并回答这个问题是相同的密码吗?并正确更新数据,我的问题与实施PasswordEncoder 或其他解决方案的最佳方式有关
    • 在这种情况下,我建议定义BCryptPasswordEncoder bean 和 autowire PasswordEncoder 接口(使将来更容易切换到另一个编码器)及其方法encode(..)matches(...)。通常,它与您的代码执行相同的操作,但您的代码中没有对 BCrypt 的严格依赖。我会将 PasswordEncoder 相关代码放在具有自描述名称的服务中,而不是放在 setter 中。另外,建议使用String []char [] 操作原始密码(防止将它们存储在字符串常量池中)。
    • 如果你有时间举一个你解释的小例子,这样我就可以把它作为这个问题的答案
    • 更新了答案。
    猜你喜欢
    • 2011-08-05
    • 2018-12-13
    • 1970-01-01
    • 2011-11-14
    • 2016-05-16
    • 2022-10-14
    • 2012-06-12
    • 2016-01-23
    • 2018-12-10
    相关资源
    最近更新 更多