使用 JPA 及 UserDetailsService

March 19, 2017 · View on GitHub

本文展示了如何使用 JPA 自定义 UserDetailsService 及数据库 的方式来进行认证。在本例,我们将认证信息存储于 H2 数据库中。

ldap-authentication项目的基础上,我们构建了一个jpa-userdetailsservice项目。

build.gradle

修改 build.gradle 文件,让我们的jpa-userdetailsservice项目成为一个新的项目。

修改内容也比较简单,修改项目名称及版本即可。

jar {
	baseName = 'jpa-userdetailsservice'
	version = '1.0.0'
}

我们将 spring-boot-starter-jdbc依赖改为了 spring-boot-starter-data-jpa

实体

修改 User

修改 User,增加了 username、password 两个字段,用于登录时使用。authorities 字段,用于存储该用户的权限信息。同时,User 也实现了 org.springframework.security.core.userdetails.UserDetails 接口。其中 User 与 Authority是多对多的关系。

@Entity // 实体
public class User implements UserDetails, Serializable {

	private static final long serialVersionUID = 1L;

	@Id // 主键
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
	private Long id; // 用户的唯一标识

	@Column(nullable = false, length = 50) // 映射为字段,值不能为空
	private String name;

	@Column(nullable = false)
	private Integer age;

	@Column(nullable = false, length = 50, unique = true)
	private String username; // 用户账号,用户登录时的唯一标识

	@Column(length = 100)
	private String password; // 登录时密码

	@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	@JoinTable(name = "user_authority", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), 
		inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
	private List<Authority> authorities;

	protected User() { // JPA 的规范要求无参构造函数;设为 protected 防止直接使用
	}

	public User(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.authorities;
	}

	public void setAuthorities(List<Authority> authorities) {
		this.authorities = authorities;
	}

	@Override
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	@Override
	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public String toString() {
		return String.format("User[id=%d, username='%s', name='%s', age='%d', password='%s']", id, username, name, age,
				password);
	}
}

新增 Authority

新增 Authority 用于存储权限信息。 Authority 实现了 GrantedAuthority 接口,并重写了其 getAuthority 方法。

@Entity // 实体
public class Authority implements GrantedAuthority {

	private static final long serialVersionUID = 1L;

	@Id // 主键
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
	private Long id; // 用户的唯一标识

	@Column(nullable = false) // 映射为字段,值不能为空
	private String name;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.core.GrantedAuthority#getAuthority()
	 */
	@Override
	public String getAuthority() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

新增存储库

新增com.waylau.spring.boot.security.repository.UserRepository,由于继承自了 org.springframework.data.jpa.repository.JpaRepository接口,所以大部分与数据库操作的接口,我们在UserRepository都无需定义了。我们只需要定义一个

public interface UserRepository extends JpaRepository<User, Long>{

	User findByUsername(String username);
}

findByUsername是用来根据用户账号来查询用户,这个在查询用户权限时要用到。

服务类

我们的服务类继承自我们定义的UserService,以及org.springframework.security.core.userdetails.UserDetailsService。其中,UserDetailsService是用来查询认证信息相关的接口,我们重写其loadUserByUsername方法。

@Service
public class UserServiceImpl implements UserService,UserDetailsService {

	@Autowired
	private UserRepository userRepository;
	
	......
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userRepository.findByUsername(username);
	}

}

修改配置类 SecurityConfig

在配置类中,我们把 UserDetailsService 的实现赋给 AuthenticationManagerBuilder:

@Autowired
private UserDetailsService userDetailsService;
......

/**
 * 认证信息管理
 * @param auth
 * @throws Exception
 */
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(userDetailsService);
} 

初始化数据库

用于是使用了 JPA ,所以,默认表结构是会根据实体逆向生成的。

至于数据,我们在项目的 src/main/resources 目录下,放置一个 import.sql 脚本文件。里面是我们要初始化的数据,只要应用启动,就会自动把 import.sql 的数据给导入数据库。

以下为我们要初始化的数据:

INSERT INTO user (id, username, password, name, age) VALUES (1, 'waylau', '123456', '老卫', 30);
INSERT INTO user (id, username, password, name, age)  VALUES (2, 'admin', '123456', 'Way Lau', 29);

INSERT INTO authority (id, name) VALUES (1, 'ROLE_USER');
INSERT INTO authority (id, name) VALUES (2, 'ROLE_ADMIN');

INSERT INTO user_authority (user_id, authority_id) VALUES (1, 1);
INSERT INTO user_authority (user_id, authority_id) VALUES (2, 1);
INSERT INTO user_authority (user_id, authority_id) VALUES (2, 2);