在春季启动时使用弹簧安全保护密码的最佳做法是什么?

时间:2017-05-09 04:32:48

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

我在java中为使用Spring Boot的在线商店创建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);
    }
}

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

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

1 个答案:

答案 0 :(得分:3)

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

我们假设您为用户字段别名(private String alias;)添加了唯一约束,并添加了简单的private String role;字段。现在您要设置Spring Security以保持&#39; / shop&#39;所有子资源(f.e.&#39; / shop / search&#39;)向所有人开放,不受保护,资源和/折扣&#39;仅适用于注册用户和资源&#39; / admin&#39;仅供管理员使用。

要实现它,您需要定义几个类。让我们从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将根据配置自动执行。还有很多事情要设置和考虑,但我希望我能够解释整体想法。

修改

在任何弹簧组件或配置

中定义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。