密码安全有两个方面:
目前,一般可采用BCrypt加密方式,我们绝不能将密码的明文,或者经过弱哈希(如MD5和SHA)就存放在数据库中。BCrypt作为工业级产品,为每个密码产生不同的salt,使得字典生产困难得多,而MD5和SHA的破译则简单得多。
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;
}
@Basic
public 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
@Transient
public String getName() {
return this.username;
}
/* 对于Principal而言,重要的是两者之间的比较,因此重写object的下面几个方法 */
@Override
@Transient
public int hashCode() {
return this.username.hashCode();
}
@Override
@Transient
public boolean equals(Object obj) {
return obj instanceof UserPrincipal &&
((UserPrincipal)obj).username.equals(this.username);
}
@Override
@Transient
protected UserPrincipal clone() {
try {
return (UserPrincipal) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e); //转成RuntimeException,使得transaction可以回滚
}
}
@Override
@Transient
public 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:OpenBSD-style Blowfish password hashing for Java industry-standard jBCrypt Java implementation of the BCrypt hash algorithm.
在pom.xml中
org.mindrot
jbcrypt
0.4
public interface UserRepository extends GenericRepository{
UserPrincipal getByUsername(String username);
}
@Repository
public class DefaultUserRepository extends GenericJpaRepository 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 query = builder.createQuery(this.entityClass);
Root root = query.from(this.entityClass);
return this.entityManager.createQuery(
query.select(root).where(builder.equal(root.get("username"), username))
).getSingleResult();
}
}
@Validated
public 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
);
}
@Service
public 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相关文章