文章目录

  • 1. 基于内存的用户定义认证
  • 1. 配置文件方式
  • 2. 配置类中重写configure方式
  • 3. 自定义 InMemoryUserDetailsManager 方式
  • 2. 提供用户数据源的接口 UserDetailsService
  • 1. UserDetails 接口
  • 2. UserDetailsService 接口
  • 3. 基于MyBatis数据源的用户定义认证


1. 基于内存的用户定义认证

测试接口:启动项目访问 localhost:8080/info

@RestController
public class UserController {
    @RequestMapping("/info")
    public void info(HttpServletRequest request){
        // 当前登录用户的用户名
        String remoteUser = request.getRemoteUser();
        Principal userPrincipal = request.getUserPrincipal();
        Authentication authentication = (Authentication) userPrincipal;
        // 判断当前用户是否具备某一个指定的角色
        boolean admin = request.isUserInRole("admin");
        // 登录用户的用户名
        System.out.println("remoteUser = " + remoteUser);
        System.out.println("authentication.getName():"+authentication.getName());
        System.out.println("admin = " + admin);
    }
}
1. 配置文件方式

默认的用户定义在SecurityProperties里边,是一个静态内部类,如果要定义自己的用户名密码,必然是要去覆盖默认配置,在配置文件中配置:

spring.security.user.name=admin
spring.security.user.password=admin
spring.security.user.roles=ADMIN
2. 配置类中重写configure方式
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zhangsan").password("123").roles()
                .and()
                .withUser("lisi").password("123").roles();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        //暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例
        return NoOpPasswordEncoder.getInstance();
    }
}

启动项目,此时就可以使用这里配置的用户登录了。

3. 自定义 InMemoryUserDetailsManager 方式

SpringSecurity 支持多种用户定义方式,自定义用户其实就是使用UserDetailService的不同实现类来提供用户数据,同时将配置好的UserDetailsService配置给AuthenticationManagerBuilder,系统再将UserDetailsService提供给AuthenticationProvider使用。

通过自定义 InMemoryUserDetailsManager 来看一下基于内存的用户是如何自定义的。

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        // 创建对象设置用户名,密码,角色 (密码加上{noop}代表密码不加密,明文存储)
        manager.createUser(User.withUsername("zhangsan").password("123").roles("admin").build());
        manager.createUser(User.withUsername("lisi").password("123").roles("user").build());
        auth.userDetailsService(manager);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        //暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例
        return NoOpPasswordEncoder.getInstance();
    }
}

2. 提供用户数据源的接口 UserDetailsService

1. UserDetails 接口

SpringSecurity 中定义了UserDetails 接口来规范开发者自定义的用户对象,接口定义如下:

public interface UserDetails extends Serializable {
    // 返回当前账户锁具备的权限
    Collection<? extends GrantedAuthority> getAuthorities();
    // 返回当前账户的密码
    String getPassword();
    // 返回当前账户的用户名
    String getUsername();
    // 返回当前账户是否未过期
    boolean isAccountNonExpired();
    // 返回当前账户是否未锁定
    boolean isAccountNonLocked();
    // 返回当前账号凭证是否未过期
    boolean isCredentialsNonExpired();
    // 返回当前账号是否可用
    boolean isEnabled();
}
2. UserDetailsService 接口

这是用户对象的定义,而负责提供用户数据源的接口是UserDetailsService,UserDetailService中只有一个查询用户的方法:

public interface UserDetailsService {
    // username是用户在认证时传入的用户名,最常见的是用户在登录表单中输入的用户名
    // 拿到用户名之后,再去数据库中查询用户,最终返回一个UserDetails实例
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在实际项目中,一般需要自定义UserDetailsService的实现。如果没有自定义UserDetailsService的实现,Spring Security 也为UserDetailsService 提供了默认实现:

(1) UserDetailsManager 在UserDetailsService 的基础上,定义了5种方法:

public interface UserDetailsManager extends UserDetailsService {
    // 创建用户
    void createUser(UserDetails var1);
	// 更新用户
    void updateUser(UserDetails var1);
	// 删除用户
    void deleteUser(String var1);
	// 修改密码
    void changePassword(String var1, String var2);
	// 判断用户是否存在
    boolean userExists(String var1);
}

(2) JdbcDaoImpl 在UserDetailsService 的基础上,通过spring-jdbc实现了从数据库中查询用户的方法;

(3) InMemoryUserDetailsManager 实现了UserDetailsService 中关于用户的增删改查方法,不过都是基于内存的操作,数据没有持久化;

(4) JdbcUserDetailsManager 继承自 JdbcDaoImpl 同时又实现了 UserDetailsManager 接口,因此可以通过JdbcUserDetailsManager 实现对用户的在呢过删改查操作,这些操作都会持久化到数据化中,但是操作数据库中用户的SQL都是提前写好的,不够灵活,因此在是开发中 JdbcUserDetailsManager 用的并不多;

(5) CachingUserDetailsService 的特点是会将 UserDetailsService 缓存起来;

(6) UserDetailsServiceDelegator 提供了 UserDetailsService 的懒加载功能;

当我们使用Spring Security 时,如果仅仅引入了一个Spring Security依赖,则默认使用的用户就是 InMemoryUserDetailsManager 提供的;

SpringBoot 之所以能做到零配置使用SpringSecurity,就是因为它提供了众多的自动化配置类。其中UserDetailsService 的自动化配置类时UserDetailsServiceAutoConfiguration;

3. 基于MyBatis数据源的用户定义认证

使用MyBatis做数据持久化是目前大多数企业应用采取的方案,SpringSecurity中结合MyBatis可以灵活的定制用户表和角色表。

首先设计三张表,分别为用户表,角色表,以及用户角色关联表,三张表的关系如图所示:

spring security 自定义user无法反序列化 spring security 自定义provider_jvm

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,
    enable 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 `test`.`role` (`id`, `name`, `nameZh`) VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO `test`.`role` (`id`, `name`, `nameZh`) VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `test`.`role` (`id`, `name`, `nameZh`) VALUES (3, 'ROLE_user', '用户');

INSERT INTO `test`.`user` (`id`, `username`, `password`, `enable`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`) VALUES (1, 'root', '123', 1, 1, 1, 1);
INSERT INTO `test`.`user` (`id`, `username`, `password`, `enable`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`) VALUES (2, 'admin', '123', 1, 1, 1, 1);
INSERT INTO `test`.`user` (`id`, `username`, `password`, `enable`, `accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`) VALUES (3, 'zhangsan', '123', 1, 1, 1, 1);

INSERT INTO `test`.`user_role` (`id`, `uid`, `rid`) VALUES (1, 1, 1);
INSERT INTO `test`.`user_role` (`id`, `uid`, `rid`) VALUES (2, 1, 2);
INSERT INTO `test`.`user_role` (`id`, `uid`, `rid`) VALUES (3, 2, 2);
INSERT INTO `test`.`user_role` (`id`, `uid`, `rid`) VALUES (4, 3, 3);

在SpringSecurity项目中引入MyBatis和MySQL的依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

在resources/application.yml中配置数据库基本连接信息:

spring:
  application:
    name: uua
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong

mybatis:
  mapper-locations: classpath:mapper/*.xml

自定义用户类需要实现UserDetails接口,并实现接口中的方法:

@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

@Data
public class UserRole {
    private Integer id;
    private Integer uid;
    private Integer rid;
}
@Data
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;
    }
}

实现UserDetailsService接口自定义MyUserDetailsService以及对应的数据库查询方法:

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if(Objects.isNull(user)){
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

@Mapper
public interface UserMapper {
    User loadUserByUsername(String username);
    List<Role> getRolesByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.uua.dao.UserMapper">
    <select id="loadUserByUsername" resultType="com.imooc.uua.entity.User">
        select * from user wherr username=#{username};
    </select>
    <select id="getRolesByUid" resultType="com.imooc.uua.entity.Role">
        select r.*
        from  user_role ur
        join role r
        on ur.rid=r.id and ur.uid=#{uid}
    </select>
</mapper>

SecurityConfig中注入UserDetailsService :

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }
    
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

启动项目利用数据库中添加的模拟用户进行登录测试,就可以登录成功了。