Spring Security里用户的定义

340 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

Spring Security里用户的定义

\quad在之前几篇博客中,我们的登录用户是基于配置文件来配置的(本质是基于内存),但是在实际开发中,肯定不是以这种方式来存储用户的信息,在实际项目中,用户信息都是要存入数据库之中。 Spring Security 支持多种用户定义方式,比如使用JDBC、MyBatis、JPA等方式。
\quad通过前面的介绍,我们对于 UserDetailsService 以及它的子类都有了一定的了解, 自定义用户其实就是使用 UserDetailsService 的不同实现类来提供用户数据,同时将配置好的 UserDetailsService 配置给 AuthenticationManagerBuilder,系统再将 UserDetailsService 提供给 AuthenticationProvider 使用。

1. 使用JDBC

\quad使用JDBC其实是通过JdbcUserDetailsManager 的支持,并将用户数据持久化到数据库,同时它封装了一系列操作用户的方法,例如用户的添加、更新、查找等。
\quadSpring 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);

\quad可以看到这里一共创建了两张表,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>

\quad然后在 resources/application.properties 中配置数据库连接信息:

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    #这些信息得按照自己的实际环境来配置

\quad最后重写 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);
    }

}
  1. 当引入 spring-boot-starter-jdbc 并配置了数据库连接信息后,一个 DataSource 实例就有了,这里首先引入 DataSource 实例。
  2. 在 configure 方法中,创建一个JdbcUserDetailsManager 实例,在创建时传入DataSource 实例。通过 userExists 方法可以判断一个用户是否存在,该方法本质上就是去数据库中查询对应的用户;如果用户不存在,则通过 createUser 方法可以创建一个用户,该方法本质上就是向数据库中添加一个用户。
  3. 最后将 manager 实例设置到 auth 对象中。此时,我们就可以使用 javaboy/123、sang/123 进行登录测试了。

\quad在 JdbcUserDetailsManager 的继承体系中,首先是JdbcDaoImpl 实现了 UserDetailsService 接 口, 并实现了基本的 loadUserByUsername 方 法。JdbcUserDetailsManager 则继承自 JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改查方法。

2. 使用Mybatis

\quad使用 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 引入依赖

\quad数据库的准备完成后,再在 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
    }

\quad自定义用户类需要实现 UserDetails 接口,并实现接口中的方法,其中 roles 属性用来保存用户所具备的角色信息,由于系统获取用户角色调用的方法是 getAuthorities,所以我们在 getAuthorities 方法中,将 roles 中的角色转为系统可识别的对象并返回。

2.4 实现UserDetailsService

\quad接下来我们自定义 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);
}

\quad自定义 MyUserDetailsService 实现 UserDetailsService 接口并实现loadUserByUsername方法,该方法就是根据用户名去数据库中加载用户,如果从数据库中没有查到用户,则抛出 UsernameNotFoundException 异常;如果查询到用户了,则给用户设置 roles 属性。

2.5 定义查询 SQL

\quadUserMapper 中定义两个方法用于支持 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>

\quad为了方便,我们将 UserMapper.xml 文件和 UserMapper 接口放在了相同的包下。为了防止 Maven 打包时自动忽略了 XML 文件,还需要在 pom.xml 中进行配置。

2.6 注入 UserDetailsService

\quad最后一步,就是在 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()
        //省略
    }
}

\quad配置 UserDetailsService 的方式和之前配置 JdbcUserDetailsManager 的方式基本一致,只不过配置对象变成了 myUserDetailsService 而已。 至此,整个配置工作就完成了。接下来启动项目,利用数据库中添加的模拟用户进行登录测试,就可以成功登录了。