密碼安全存放
密碼安全有兩個方面: 使用者密碼傳遞的安全性,可以使用https來保護,也有將密碼進行雜湊(例如MD5)後進行傳遞的(複雜一點的,密碼是參與雜湊,還包含一些動態參數,例如時間戳記等進行salt),或者兩者結合起來 資料庫密碼儲存的安全性,一旦被拖庫,或者別的被黑,仍能安全保護好使用者的密碼。
目前,一般可採用BCrypt加密方式,我們絕不能將密碼的明文,或者經過弱雜湊(如MD5和SHA)就存放在資料庫中。BCrypt作為工業級產品,為每個密碼產生不同的salt,使得字典生產困難得多,而MD5和SHA的破譯則簡單得多。
資料庫表格 BCrypt加密後需要60位元組存放,下面是一個存放表格的例子:
CREATE TABLE UserPrincipal ( UserId BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, Username VARCHAR(30) NOT NULL, HashedPassword BINARY(60) NOT NULL, UNIQUE KEY UserPrincipal_Username (Username)) ENGINE = InnoDB;
bcrypt加密後的字串形如:$2a$10$vacuqbDw9I7rr6RRH8sByuktOzqTheQMfnK3XCT2WlaL7vt/3AMby,其中:$是分割符,無意義;2a是bcrypt加密版本號碼;10是cost的值;而後的前22位是salt值;再然後的字串就是密碼的密文了;
我們將使用UserPrincipal作為Entity,代碼如下:
/** 考慮到在session中存放Principal,我們將實現Principal介面,並提供Colonable介面 * 對於principal,我們如何判斷其相同(本例採用使用者名稱),將重寫對象的hashcode()、equals()和toString()。*/@Entity@Table(uniqueConstraints={ @UniqueConstraint(name = "UserPrincipal_Username",columnNames="Username")})public class UserPrincipal implements Principal,Cloneable,Serializable{private static final long serialVersionUID = 1L;private static final String SESSION_ATTRIBUTE_KEY = "cn.wei.flowingflying.customer_support.user.principal";private long id;private String username;private byte[] password;@Id@Column(name="UserId")@GeneratedValue(strategy=GenerationType.IDENTITY)public long getId() {return id;}public void setId(long id) {this.id = id;}@Basicpublic String getUsername() {return username;}public void setUsername(String username) {this.username = username;}@Basic@Column(name = "HashedPassword")public byte[] getPassword() {return password;}public void setPassword(byte[] password) {this.password = password;}@Override@Transientpublic String getName() {return this.username;}/* 對於Principal而言,重要的是兩者之間的比較,因此重寫object的下面幾個方法 */@Override@Transientpublic int hashCode() {return this.username.hashCode();}@Override@Transientpublic boolean equals(Object obj) {return obj instanceof UserPrincipal && ((UserPrincipal)obj).username.equals(this.username);}@Override@Transientprotected UserPrincipal clone() {try {return (UserPrincipal) super.clone();} catch (CloneNotSupportedException e) {throw new RuntimeException(e); //轉成RuntimeException,使得transaction可以復原}}@Override@Transientpublic String toString() {return this.username;}/* 對於提供靜態方法供調用,以便在session中存放和擷取principal */public static Principal getPrincipal(HttpSession session){return session == null ? null : (Principal)session.getAttribute(SESSION_ATTRIBUTE_KEY);}public static void setPrincipal(HttpSession session, Principal principal){session.setAttribute(SESSION_ATTRIBUTE_KEY, principal);}}
密碼校正
引入jbcrypt
jbcrypt:OpenBSD-style Blowfish password hashing for Java industry-standard jBCrypt Java implementation of the BCrypt hash algorithm.
在pom.xml中
<dependency> <groupId>org.mindrot</groupId> <artifactId>jbcrypt</artifactId> <version>0.4</version></dependency>
repository介面的實現 之前已經介紹了通用的repository介面的實現,我們需要加上通過UNIQUE KEY(username)的擷取,具體如下:
public interface UserRepository extends GenericRepository<Long, UserPrincipal>{ UserPrincipal getByUsername(String username);}
@Repositorypublic class DefaultUserRepository extends GenericJpaRepository<Long, UserPrincipal> implements UserRepository{ @Override public UserPrincipal getByUsername(String username) {// 【方式1】通過Java Persistence query,即JPQL// return this.entityManager.createQuery(// "SELECT u FROM UserPrincipal u WHERE u.username = :username",// UserPrincipal.class// ).setParameter("username", username).getSingleResult();// 【方式2】通過JPA的標準介面 CriteriaBuilder builder = this.entityManager.getCriteriaBuilder(); CriteriaQuery<UserPrincipal> query = builder.createQuery(this.entityClass); Root<UserPrincipal> root = query.from(this.entityClass); return this.entityManager.createQuery( query.select(root).where(builder.equal(root.get("username"), username)) ).getSingleResult();}}
密碼校正相關代碼
@Validatedpublic interface AuthenticationService {Principal authenticate(@NotBlank(message = "{validate.authenticate.username}") String username, @NotBlank(message = "{validate.authenticate.password}") String password);void saveUser(@NotNull(message = "{validate.authenticate.saveUser}") @Valid UserPrincipal principal, String newPassword );}
@Servicepublic class DefaultAuthenticationService implements AuthenticationService{ private static final Logger log = LogManager.getLogger(); private static final SecureRandom RANDOM; private static final int HASHING_ROUNDS = 10; static { try { RANDOM = SecureRandom.getInstanceStrong(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } } @Inject UserRepository userRepository; @Override @Transactional public Principal authenticate(@NotBlank(message = "{validate.authenticate.username}") String username, @NotBlank(message = "{validate.authenticate.password}") String password) { UserPrincipal principal = this.userRepository.getByUsername(username); if(principal == null){ log.warn("Authentication failed for non-existent user {}.", username); return null; } if(!BCrypt.checkpw(password, new String(principal.getPassword(),StandardCharsets.UTF_8))){ log.warn("Authentication failed for user {}.", username); return null; } log.debug("User {} successfully authenticated.", username); return principal; } @Override @Transactional public void saveUser(@NotNull(message = "{validate.authenticate.saveUser}") @Valid UserPrincipal principal, String newPassword) { if(newPassword != null && newPassword.length() > 0){ String salt = BCrypt.gensalt(HASHING_ROUNDS, RANDOM); principal.setPassword(BCrypt.hashpw(newPassword, salt).getBytes()); } if(principal.getId() < 1) this.userRepository.add(principal); else this.userRepository.update(principal); }}相關連結: 我的Professional Java for Web Applications相關文章