我们发现security的数据源配置主要是实现UserDetailsService接口
我们可以看到其实现类有如下
image.png
可以看到,在几个能直接使用的实现类中,除了 InMemoryUserDetailsManager 之外,还有一个 JdbcUserDetailsManager,
使用 JdbcUserDetailsManager 可以让我们通过 JDBC 的方式将数据库和 Spring Security 连接起来。
我们在正式的开发中,我们可以写一个自己的实体类实现该接口

使用数据库

建表

JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在如下位置:

  1. org/springframework/security/core/userdetails/jdbc/users.ddl

可以看到,脚本中有一种数据类型 varchar_ignorecase,这个其实是针对 HSQLDB 数据库创建的,而我们使用的 MySQL 并不支持这种数据类型,所以这里需要大家手动调整一下数据类型,将 varchar_ignorecase 改为 varchar 即可。

  1. create database security_db;
  2. use security_db;
  3. create table users
  4. (
  5. username varchar(50) not null primary key,
  6. password varchar(500) not null,
  7. enabled boolean not null
  8. );
  9. create table authorities
  10. (
  11. username varchar(50) not null,
  12. authority varchar(50) not null,
  13. constraint fk_authorities_users foreign key (username) references users (username)
  14. );
  15. create unique index ix_auth_username on authorities (username, authority);

执行完 SQL 脚本后,我们可以看到一共创建了两张表:users 和 authorities。

  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。

我们现在可以使用数据库连接配置用户,而不是内存数据进行配置用户了

依赖导入

新增jdbc依赖和mysql依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置文件

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

配置类

@Autowired
DataSource dataSource;

@Override
@Bean
protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager detailsManager = new InMemoryUserDetailsManager();
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
    if(!manager.userExists("admin")) {
        manager.createUser(User.withUsername("admin").password("123456").roles("admin").build());
    }
    if(!manager.userExists("user")) {
        manager.createUser(User.withUsername("user").password("123456").roles("user").build());
    }
    return manager;
}


结果

image.png
image.png

数据库中的enabled表示是否禁用该账户,设置为禁用之后,就无法使用该账户进行登录了

整合JPA

使用Jdbc对于一般应用来说足够使用了,但是过于复杂,
为了更简单的管理spring security,我们引入jpa进行数据管理

导入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置实体类

UserEntity实体

@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity(name = "t_user")
public class UserEntity implements UserDetails {
    private static final long serialVersionUID = 5461613954799968213L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
    private List<RoleEntity> roleEntities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for(RoleEntity RoleEntity : getRoleEntities()) {
            authorities.add(new SimpleGrantedAuthority(RoleEntity.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;
    }

    @Override
    public boolean equals(Object o) {
        if(this == o)
            return true;
        if(o == null || Hibernate.getClass(this) != Hibernate.getClass(o))
            return false;
        UserEntity that = (UserEntity) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return 0;
    }
}

用户实体类主要需要实现 UserDetails 接口,并实现接口中的方法。
这里的字段基本都好理解,几个特殊的我来稍微说一下:

  1. accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个属性 | 属性 | 描述 | 默认值 | | —- | —- | —- | | accountNonExpired | 表示账户是否没有过期 | true | | accountNonLocked | 账户是否没有被锁定 | true | | credentialsNonExpired | 密码是否没有过期 | true | | enabled | 以及账户是否可用 | true |
  1. roles 属性表示用户的角色,User 和 Role 是多对多关系,用一个 @ManyToMany 注解来描述。
  2. getAuthorities 方法返回用户的角色信息,我们在这个方法中把自己的 Role 稍微转化一下即可。

    RoleEntity实体

    @Getter
    @Setter
    @ToString
    @RequiredArgsConstructor
    @Entity(name = "t_role")
    public class RoleEntity {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
     private String name;
     private String nameZh;
    
     @Override
     public boolean equals(Object o) {
         if(this == o)
             return true;
         if(o == null || Hibernate.getClass(this) != Hibernate.getClass(o))
             return false;
         RoleEntity that = (RoleEntity) o;
         return Objects.equals(id, that.id);
     }
    
     @Override
     public int hashCode() {
         return 0;
     }
    }
    

    这个实体类用来描述用户角色信息,有角色 id、角色名称(英文、中文),
    @Entity 表示这是一个实体类,项目启动后,将会根据实体类的属性在数据库中自动创建一个角色表。

配置Dao

配置Jpa的dao连接层

public interface UserDao extends JpaRepository<UserEntity, Long> {
    UserEntity findUserByUsername(String username);
}

配置Service

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userDao.findUserByUsername(username);
        if(user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

配置用户

在配置类中进行配置用户

  @Autowired
    UserService userService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userService);
    }

还是重写 configure 方法,只不过这次我们不是基于内存,也不是基于 JdbcUserDetailsManager,而是使用自定义的 UserService进行配置就 OK 了。

配置文件

配置一下数据库信息和jpa信息即可

server:
  port: 29898
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  jpa:
    database: mysql
    database-platform: mysql
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
    show-sql: true

测试

@SpringBootTest
class UserEntityServiceTest {
    @Autowired
    UserDao userDao;

    @Test
    void contextLoads() {
        UserEntity u1 = new UserEntity();
        u1.setUsername("admin");
        u1.setPassword("123456");
        u1.setAccountNonExpired(true);
        u1.setAccountNonLocked(true);
        u1.setCredentialsNonExpired(true);
        u1.setEnabled(true);
        List<RoleEntity> rs1 = new ArrayList<>();
        RoleEntity r1 = new RoleEntity();
        r1.setName("ROLE_admin");
        r1.setNameZh("管理员");
        rs1.add(r1);
        u1.setRoleEntities(rs1);
        userDao.save(u1);
        UserEntity u2 = new UserEntity();
        u2.setUsername("user");
        u2.setPassword("123456");
        u2.setAccountNonExpired(true);
        u2.setAccountNonLocked(true);
        u2.setCredentialsNonExpired(true);
        u2.setEnabled(true);
        List<RoleEntity> rs2 = new ArrayList<>();
        RoleEntity r2 = new RoleEntity();
        r2.setName("ROLE_user");
        r2.setNameZh("普通用户");
        rs2.add(r2);
        u2.setRoleEntities(rs2);
        userDao.save(u2);
    }
}

编写测试类,执行测试类,发现数据库生成了三张表,表中会生成数据

image.png
image.png
image.png

执行接口测试