将用户数据存入数据库

  • Spring Security 支持多种不同的数据源,这些不同的数据源最终都将被封装成 UserDetailsService 的实例, 除了自己封装,我们也可以使用系统默认提供的 UserDetailsService 实例

JdbcUserDetailsManager

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);
  • 可以看到,脚本中有一种数据类型 varchar_ignorecase,这个其实是针对 HSQLDB 数据库创建的,而我们使用的 MySQL 并不支持这种数据类型,所以这里需要大家手动调整一下数据类型,将 varchar_ignorecase 改为 varchar 即可。
  • 两张表:users 和 authorities。
  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。 配置完成后,接下来, 通过 InMemoryUserDetailsManager 提供的用户数据用 JdbcUserDetailsManager 代替掉,如下:

@Autowired
DataSource dataSource;

@Override
@Bean
protected UserDetailsService userDetailsService() {
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
    manager.setDataSource(dataSource);
    if (!manager.userExists("javaboy")) {
        manager.createUser(User.withUsername("javaboy").password("123").roles("admin").build());
    }
    if (!manager.userExists("江南一点雨")) {
        manager.createUser(User.withUsername("江南一点雨").password("123").roles("user").build());
    }
    return manager;
}

这段配置的含义如下:

  1. 首先构建一个 JdbcUserDetailsManager 实例。给 JdbcUserDetailsManager 实例添加一个 DataSource 对象。
  2. 调用 userExists 方法判断用户是否存在,如果不存在,就创建一个新的用户出来(因为每次项目启动时这段代码都会执行,所以加一个判断,避免重复创建用户)。
  3. 用户的创建方法和我们之前 InMemoryUserDetailsManager 中的创建方法基本一致。
  4. 这里的 createUser 或者 userExists 方法其实都是调用写好的 SQL 去判断的,我们从它的源码里就能看出来(部分):
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager,
  GroupManager {
 public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";

 private String userExistsSql = DEF_USER_EXISTS_SQL;

 public boolean userExists(String username) {
  List<String> users = getJdbcTemplate().queryForList(userExistsSql,
    new String[] { username }, String.class);

  if (users.size() > 1) {
   throw new IncorrectResultSizeDataAccessException(
     "More than one user found with name '" + username + "'", 1);
  }

  return users.size() == 1;
 }
}
  • 从这段源码中就可以看出来,userExists 方法的执行逻辑其实就是调用 JdbcTemplate 来执行预定义好的 SQL 脚本,进而判断出用户是否存在

use Spring DataJPA

  • 用户角色:
@Entity(name = "t_role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String nameZh;
    //省略 getter/setter
}

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

  • 用户实体类:
@Entity(name = "t_user")
public class User implements UserDetails {
    @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<Role> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : getRoles()) {
            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;
    }
    //省略其他 get/set 方法
}

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

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

UserService ,如下:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}
  • 我们自己定义的 UserService 需要实现 UserDetailsService 接口,实现该接口,就要实现接口中的方法,也就是 loadUserByUsername
  • 这个方法的参数就是用户在登录的时候传入的用户名,根据用户名去查询用户信息(查出来之后,系统会自动进行密码比对)。

在 SecurityConfig 中,我们通过如下方式来配置用户:

@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}
  • 大家注意,还是重写 configure 方法,只不过这次我们不是基于内存,也不是基于 JdbcUserDetailsManager,而是使用自定义的 UserService,就这样配置就 OK 了。

最后,我们再在 application.properties 中配置一下数据库和 JPA 的基本信息,如下:

spring.datasource.username=root
spring.datasource.password=123
spring.datasource.url=jdbc:mysql:///withjpa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

spring.jpa.database=mysql
spring.jpa.database-platform=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

Authentication

  1. 在 Spring Security 中有一个非常重要的对象叫做 Authentication,我们可以在任何地方注入 Authentication 进而获取到当前登录用户信息,Authentication 本身是一个接口,它有很多实现类:
  2. 在这众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken 了,但是当我们打开这个类的源码后,却发现这个类平平无奇,他只有两个属性、两个构造方法以及若干个 get/set 方法;当然,他还有更多属性在它的父类上。

配置过滤器

Spring Security 的配置中,配置过滤器,如下:

@Configuration
publicclass SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    VerifyCodeFilter verifyCodeFilter;
    ...
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                ...
                ...
                .permitAll()
                .and()
                .csrf().disable();
    }
}

这里只贴出了部分核心代码,即 http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class); ,如此之后,整个配置就算完成了。

当我们登录成功后,可以通过如下方式获取到当前登录用户信息:

SecurityContextHolder.getContext().getAuthentication()在 Controller 的方法中,加入 Authentication 参数 这两种办法,都可以获取到当前登录用户信息。