欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java for Web学习笔记(一零七):Spring框架中使用JPA(7)密码和BCrypt

程序员文章站 2022-04-22 13:40:28
...

密码安全存放

密码安全有两个方面:

  1. 用户密码传递的安全性,可以使用https来保护,也有将密码进行哈希(例如MD5)后进行传递的(复杂一点的,密码是参与哈希,还包含一些动态参数,例如时间戳等进行salt),或者两者结合起来
  2. 数据库密码存储的安全性,一旦被拖库,或者别的被黑,仍能安全保护好用户的密码。

目前,一般可采用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;
	}

	@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

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);
}
@Repository
public 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();			
	}
}

密码校验相关代码

@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相关文章