2017-05-09 90 views
1

我正在使用Spring Boot在线创建一个Java的REST API,我想安全地将用户密码存储在数据库中, 为此,我使用的BCrypt包含在Spring安全中,我使用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

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

我们假设您为用户字段别名(private String alias;)添加了唯一约束并添加了简单的private String role;字段。现在,您希望设置Spring Security以保持'/ shop'和所有子资源(fe'/ shop/search')向所有人开放,不受保护的资源'/折扣'仅适用于注册用户和资源'/ 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())) 
... 
} 

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

编辑

任何弹簧组件或配置中的如下定义豆

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

    } 

    ... 
} 

之后,你可以保持简单的二传手在用户类。

+0

你的回答让有很大的意义,从这个想法来的MVC模型被跟随,并且我从产生类似thymeleaf的意见,这种情况是访问控制是由客户端处理,为的是变量'type'被使用,该API只需要适当地存储在数据库中的密码和回答的问题,这是相同的密码?并更新正确的,我的问题是有关的最佳方式中的数据来实现'PasswordEncoder'或一些其他的解决方案 – CorrOrtiz

+0

在这种情况下,我会推荐给define'BCryptPasswordEncoder'豆和自动装配'PasswordEncoder'接口(更容易使未来很可能切换到其他编码器)以及它的方法'encode(..)'和'matches(...)'。一般来说,做同样的事情为你的代码,但你不必在你的代码上'BCrypt'严格依赖。我会将PasswordEncoder相关代码放在一个具有自描述名称的服务中,而不是放在setter中。此外,建议使用'字符串[]'或'的char []'与原始密码操作(防止它们存储在字符串常量池)。 – dimuha

+0

如果你有时间把你解释什么一个小例子,这样我可以表明它的答案,更新了回答这个问题 – CorrOrtiz