持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
Spring Security里用户的定义
在之前几篇博客中,我们的登录用户是基于配置文件来配置的(本质是基于内存),但是在实际开发中,肯定不是以这种方式来存储用户的信息,在实际项目中,用户信息都是要存入数据库之中。 Spring Security 支持多种用户定义方式,比如使用JDBC、MyBatis、JPA等方式。
通过前面的介绍,我们对于 UserDetailsService 以及它的子类都有了一定的了解, 自定义用户其实就是使用 UserDetailsService 的不同实现类来提供用户数据,同时将配置好的 UserDetailsService 配置给 AuthenticationManagerBuilder,系统再将 UserDetailsService 提供给 AuthenticationProvider 使用。
1. 使用JDBC
使用JDBC其实是通过JdbcUserDetailsManager 的支持,并将用户数据持久化到数据库,同时它封装了一系列操作用户的方法,例如用户的添加、更新、查找等。
Spring Security 中为JdbcUserDetailsManager 提供了数据库脚本,位置在 org/springframework/security/core/userdetails/jdbc/users.ddl,内容如下:
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
可以看到这里一共创建了两张表,users 表就是存放用户信息的表,authorities 则是存放用 户角色的表。这里需要注意的是该 SQL 语句里面有一个 varchar_ignorecase类型,这个是针对 HSQLDB 的数据类型,我们这里使用的是 MySQL 数据库,所以需要手动将 varchar_ignorecase 类型修改为 varchar 类型,然后再去数据库中执行修改后的脚本。其次需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
然后在 resources/application.properties 中配置数据库连接信息:
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
#这些信息得按照自己的实际环境来配置
最后重写 WebSecurityConfigurerAdapter 类的configure (AuthenticationManagerBuilder) 方法,内容如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
if (!manager.userExists("javaboy")) {
manager.createUser(User.withUsername("javaboy")
.password("{noop}123").roles("admin").build());
}
if (!manager.userExists("sang")) {
manager.createUser(User.withUsername("sang")
.password("{noop}123").roles("user").build());
}
auth.userDetailsService(manager);
}
}
- 当引入 spring-boot-starter-jdbc 并配置了数据库连接信息后,一个 DataSource 实例就有了,这里首先引入 DataSource 实例。
- 在 configure 方法中,创建一个JdbcUserDetailsManager 实例,在创建时传入DataSource 实例。通过 userExists 方法可以判断一个用户是否存在,该方法本质上就是去数据库中查询对应的用户;如果用户不存在,则通过 createUser 方法可以创建一个用户,该方法本质上就是向数据库中添加一个用户。
- 最后将 manager 实例设置到 auth 对象中。此时,我们就可以使用 javaboy/123、sang/123 进行登录测试了。
在 JdbcUserDetailsManager 的继承体系中,首先是JdbcDaoImpl 实现了 UserDetailsService 接 口, 并实现了基本的 loadUserByUsername 方 法。JdbcUserDetailsManager 则继承自 JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改查方法。
2. 使用Mybatis
使用 MyBatis 做数据持久化是目前大多数应用采取的方案,Spring Security 中结合 MyBatis 可以灵活地定制用户表以及角色表。
2.1 初始化数据
首先需要设计三张表,分别是用户表、角色表以及用户角色关联表这三张表。数据库脚本如下:
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL, --角色的中文名称
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL, --账户是否可用
`accountNonExpired` tinyint(1) DEFAULT NULL, --账户是否没有过期
`accountNonLocked` tinyint(1) DEFAULT NULL, --账户是否没有锁定
`credentialsNonExpired` tinyint(1) DEFAULT NULL, --凭证(密码)是否没有过期
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
--再向数据库中添加几条模拟数据,代码如下:
INSERT INTO `role` (`id`, `name`, `nameZh`) VALUES
(1,'ROLE_dba','数据库管理员'),
(2,'ROLE_admin','系统管理员'),
(3,'ROLE_user','用户');
INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`) VALUES
(1,'root','{noop}123',1,1,1,1),
(2,'admin','{noop}123',1,1,1,1),
(3,'sang','{noop}123',1,1,1,1);
INSERT INTO `user_role` (`id`, `uid`, `rid`) VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
2.2 引入依赖
数据库的准备完成后,再在 Spring Security 项目中,引入 MyBatis 和 MySQL 依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
2.3 创建用户类和角色类:
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
//省略其他 getter/setter
}
public class Role {
private Integer id;
private String name;
private String nameZh;
//省略 getter/setter
}
自定义用户类需要实现 UserDetails 接口,并实现接口中的方法,其中 roles 属性用来保存用户所具备的角色信息,由于系统获取用户角色调用的方法是 getAuthorities,所以我们在 getAuthorities 方法中,将 roles 中的角色转为系统可识别的对象并返回。
2.4 实现UserDetailsService
接下来我们自定义 UserDetailsService 以及对应的数据库查询方法:
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
user.setRoles(userMapper.getRolesByUid(user.getId()));
return user;
}
}
@Mapper
public interface UserMapper {
List<Role> getRolesByUid(@Param("id") Integer id);
User loadUserByUsername(@Param("username") String username);
}
自定义 MyUserDetailsService 实现 UserDetailsService 接口并实现loadUserByUsername方法,该方法就是根据用户名去数据库中加载用户,如果从数据库中没有查到用户,则抛出 UsernameNotFoundException 异常;如果查询到用户了,则给用户设置 roles 属性。
2.5 定义查询 SQL
UserMapper 中定义两个方法用于支持 MyUserDetailsService 中的查询操作(这里需要进行字段的映射,因为这里数据库表的字段不是很规范,为了避免出错还是进行映射比较保险。)
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qiuye.testsecurity.mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.qiuye.testsecurity.entity.User">
select * from user where username=#{username};
</select>
<select id="getRolesByUid" resultType="com.qiuye.testsecurity.entity.Role">
select r.* from role r,user_role ur where r.`id`=ur.id and ur.uid = #{id}
</select>
</mapper>
为了方便,我们将 UserMapper.xml 文件和 UserMapper 接口放在了相同的包下。为了防止 Maven 打包时自动忽略了 XML 文件,还需要在 pom.xml 中进行配置。
2.6 注入 UserDetailsService
最后一步,就是在 SecurityConfig 中注入 UserDetailsService:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//省略
}
}
配置 UserDetailsService 的方式和之前配置 JdbcUserDetailsManager 的方式基本一致,只不过配置对象变成了 myUserDetailsService 而已。 至此,整个配置工作就完成了。接下来启动项目,利用数据库中添加的模拟用户进行登录测试,就可以成功登录了。