本章概要

  • 基于数据库的认证

10.2 基于数据库的认证

在真实的项目中,用户的基本信息以及角色等都存储在数据库中,因此需要从数据库中获取数据进行认证和授权。

1. 设计数据表

用户表、角色表、用户角色关联表
十、Spring Boot 安全管理(2)-LMLPHP

建表语句

CREATE TABLE `role` (
  `id` int(11) NOT NULL,
  `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,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` varchar(1) DEFAULT NULL,
  `locked` varchar(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始化数据

INSERT INTO `user`(`id`, `username`, `password`, `enabled`, `locked`) VALUES (1, 'root', '$2a$10$x6CBW1qnQqKPIxUSefZN7ebfTEiYNnfzzVjPJzlRhg5XyMzSWoO4e', '1', '0');
INSERT INTO `user`(`id`, `username`, `password`, `enabled`, `locked`) VALUES (2, 'admin', '$2a$10$x6CBW1qnQqKPIxUSefZN7ebfTEiYNnfzzVjPJzlRhg5XyMzSWoO4e', '1', '0');
INSERT INTO `user`(`id`, `username`, `password`, `enabled`, `locked`) VALUES (3, 'tangsan', '$2a$10$x6CBW1qnQqKPIxUSefZN7ebfTEiYNnfzzVjPJzlRhg5XyMzSWoO4e', '1', '0');
INSERT INTO `role`(`id`, `name`, `nameZh`) VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO `role`(`id`, `name`, `nameZh`) VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role`(`id`, `name`, `nameZh`) VALUES (3, 'ROLE_user', '普通用户');
INSERT INTO `user_role`(`id`, `uid`, `rid`) VALUES (1, 1, 1);
INSERT INTO `user_role`(`id`, `uid`, `rid`) VALUES (2, 1, 2);
INSERT INTO `user_role`(`id`, `uid`, `rid`) VALUES (3, 2, 2);
INSERT INTO `user_role`(`id`, `uid`, `rid`) VALUES (4, 3, 3);

2. 创建项目

MyBatis 灵活,JPA 便利,此处选择前者,创建 Spring Boot Web 项目添加如下依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.2</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.10</version>
</dependency>

3. 配置数据库

在 application.properties 中进行数据连接配置

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/jpa

4. 创建实体类

Role

public class Role {
    private Integer id;
    private String name;
    private String nameZh;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}

User

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;
    @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 true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

   public Boolean getEnabled() {
       return enabled;
   }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public Boolean getLocked() {
        return locked;
    }

    public void setLocked(Boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

代码解释:

  • 用户实体类需要实现 UserDetails 接口,并且实现该接口中的 7 个方法
    | 方法名 | 解释 |
    | — | — |
    | getAuthorities() | 获取当前用户对象所具有的角色信息 |
    | getPassword() | 获取当前用户对象的密码 |
    | getUsername() | 获取当前用户对象的用户名 |
    | isAccountNonExpired() | 当前账号是否未过期 |
    | isAccountNonLocked() | 当前账号是否未锁定 |
    | isCredentialsNonExpired() | 当前账号密码是否未过期 |
    | isEnabled() | 当前账号是否可用 |

  • 用户根据实际情况设置这 7 个方法的返回值。因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可,例如 getPassword() 方法返回的密码和用户输入的密码不匹配,会自动抛出 BadCredentialsException 异常,isAccountNonExpired() 方法返回了 false ,会自动抛出 AccountExpiredException 异常,因此对开发者而言,只需要按照数据库中的数据在这里返回相应的配置即可。此处因为数据库中只有 enabled 和 locked 字段,故帐号未过期和密码未过期两个方法都返回 true

  • getAuthorities() 用来获取当前用户所具有的角色信息,此处用户所具有的角色存储在 roles 属性中,因此该方法直接遍历 roles 属性,然后构造 SimpleGrantedAuthority 集合并返回

5. 创建 UserService

@Service
public class UserService 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.getUserRolesByUid(user.getId()));
        return user;
    }
}

代码解释:

  • 定义 UserService 实现 UserDetailsService 接口,并实现该接口中的 loadUserByUsername 方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,如果没有查询到用户,就抛出一个账号不存在的异常,如果查找到了用户,就继续查询该用户所具有的角色信息,并将获取到的 user 对象返回,再由系统提供的 DaoAuthenticationProvider 类去比对密码是否正确
  • loadUserByUsername 方法将在用户登录时自动调用

涉及到的 UserMapper 和 UserMapper.xml 如下

@Mapper
public interface UserMapper {
    User loadUserByUsername(String username);
    List<Role> getUserRolesByUid(Integer id);
}
<?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="org.sang.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="org.sang.model.User">
        select * from user where username=#{username}
    </select>
    <select id="getUserRolesByUid" resultType="org.sang.model.Role">
        select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id}
    </select>
</mapper>

6. 配置 Spring Security

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/db/**").hasRole("dba")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }
}

此处的配置 与 10.1 节 介绍的一致,唯一不同的是没有配置内存用户,而是将刚刚创建好的 UserService 配置到 AuthenticationManagerBuilder 中。

7.创建 Controller

@RestController
public class HelloController {
    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }
    @GetMapping("/db/hello")
    public String dba() {
        return "hello dba";
    }
    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }
}

8. 测试

登录 admin 用户,访问 /admin/hello,报了以下错误

Invalid bound statement (not found)

pom.xml 新增以下配置

<build>
  <resources>
    <resource>
      <directory>src/main/java</directory>
      <includes>
        <include>**/*.xml</include>
      </includes>
    </resource>
    <resource>
      <directory>src/main/resources</directory>
    </resource>
  </resources>
</build>

再次登录访问 /admin/hello,报了以下错误

Caused by: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Illegal overloaded getter method with ambiguous type for property enabled in class class org.sang.model.User. This breaks the JavaBeans specification and can cause unpredictable results.

去掉 User 实体类中 enabled 属性的 get set 方法,如下

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;
    @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 true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Boolean getLocked() {
        return locked;
    }

    public void setLocked(Boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

再次登录访问 /admin/hello
十、Spring Boot 安全管理(2)-LMLPHP

访问 /db/hello
十、Spring Boot 安全管理(2)-LMLPHP

访问 /user/hello
十、Spring Boot 安全管理(2)-LMLPHP

08-11 16:36