2017-05-09 92 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