Spring 29 Spring Security - 通过数据库读取用户和角色

Spring 29 Spring Security - 通过数据库读取用户和角色

使用数据库存储用户信息和角色 观察我们的配置类可以发现,我们目前把所有的用户名,密码和角色信息都硬编码在配置类里。角色名一旦有变动,需要修改全部代码中对应的这个角色名。这显然不够好。更常见的做法是用数据库来存储相关的内容。每次进行验证的时候,到数据库中去存取数据。 Spring Security支持

使用数据库存储用户信息和角色

观察我们的配置类可以发现,我们目前把所有的用户名,密码和角色信息都硬编码在配置类里。角色名一旦有变动,需要修改全部代码中对应的这个角色名。这显然不够好。更常见的做法是用数据库来存储相关的内容。每次进行验证的时候,到数据库中去存取数据。 Spring Security支持从数据库中读取用户信息,默认情况下,存放用户的数据表必须符合Spring Security预定义的格式,也可以自定义数据格式。 先来使用预定义的格式,把目前的权限验证弄到数据库里去。

使用默认数据表

开发步骤如下:
  1. 使用SQL脚本创建数据库
  2. 在Maven中配置数据库相关依赖包
  3. 创建JDBC的配置文件
  4. 在Spring里配置数据库相关的Bean
  5. 修改Spring Security的配置,让其使用数据库
总的来说就是先配置数据库,然后配置Spring Security使用数据库就可以了。来一步一步操作。

Spring Security 默认用户表

Spring Security默认的表有两张,如下:
Default SS Database Schema
users authorities
username VARCHAR(50) 主键 username VARCHAR(50) 外键关联到users表主键
password VARCHAR(50) authority VARCHAR(50)
enabled TINYINT(1)
在创建数据库表的时候,需要属性和名称都符合上边的要求,authorities实际就是role角色表。创建的脚本如下:
DROP DATABASE  IF EXISTS `spring_security_demo_plaintext`;

CREATE DATABASE  IF NOT EXISTS `spring_security_demo_plaintext`;
USE `spring_security_demo_plaintext`;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50) NOT NULL,
  `password` varchar(50) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

INSERT INTO `users`
VALUES
('john','{noop}test123',1),
('mary','{noop}test123',1),
('susan','{noop}test123',1);

DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL,
  `authority` varchar(50) NOT NULL,
  UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
  CONSTRAINT `authorities_ibfk_1` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

INSERT INTO `authorities`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_ADMIN');
这里实际上还涉及到密码的问题,Spring Security 5 的密码使用特殊的类似{id}encodedPassword的方式存储,其中id表示加密的方式,写成{noop}就表示之后是明文密码。先用明文密码直接插入和进行验证,之后会转换到{bcrypt}加密方式。 配置好了数据库和表之后,像原来我们需要数据库驱动自己添加包一样,需要在Maven里添加MySQL的驱动和Hibernate以及连接池。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.3</version>
</dependency>
仅仅是为了使用验证功能的话,无需使用Hibernate。然后在src/main/resources目录下创建连接配置文件persistence-mysql.properties: 这里还需要注意的是,如果resources目录没有显示出资源目录的图标,需要到Project Structure里手工指定。
#
# JDBC connection properties
#
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_security_demo_plaintext?useSSL=false
jdbc.user=springstudent
jdbc.password=springstudent

#
# Connection pool properties
#
connection.pool.initialPoolSize=5
connection.pool.minPoolSize=5
connection.pool.maxPoolSize=20
connection.pool.maxIdleTime=3000
Maven项目在编译的时候,resources文件夹下的内容在编译的时候会Maven被放到classes目录下,就是类路径下。 在配置文件中配置好了MySQL和连接池之后,需要修改Spring的配置类DemoAppConfig.java来导入Properties文件:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "cc.conyli")
@PropertySource("classpath:persistence-mysql.properties")
public class DemoAppConfig {
    ......
}
在Intellij里如果配置正确的话,按住CTRL点击配置类中的persistence-mysql.properties,可以跳转到这个文件,说明路径正确。 由于我们没有像增删改查项目一样使用Hibernate的注入类来生成数据库操作对象,所以必须要自定义一个对象,这里把来自C3PO连接池的DataSource对象组装成一个Bean。看一下完整的配置类:
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.util.logging.Logger;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "cc.conyli")
@PropertySource("classpath:persistence-mysql.properties")
public class DemoAppConfig {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Autowired
    private Environment environment;

    private Logger logger = Logger.getLogger(getClass().getName());

    @Bean
    public DataSource securityDataSource() {
        //利用C3PO创建连接池
        ComboPooledDataSource securityDataSource = new ComboPooledDataSource();
        //设置连接池对象的数据库属性
        try {
            securityDataSource.setDriverClass(environment.getProperty("jdbc.driver"));
            securityDataSource.setJdbcUrl(environment.getProperty("jdbc.url"));
            securityDataSource.setUser(environment.getProperty("jdbc.user"));
            securityDataSource.setPassword(environment.getProperty("jdbc.password"));

            logger.info(">>>> jdbc.url=" + environment.getProperty("jdbc.url"));
            logger.info(">>>> jdbc.user=" + environment.getProperty("jdbc.user"));
        } catch (PropertyVetoException ex) {
            throw new RuntimeException(ex);
        }
        //设置连接池的连接属性
        securityDataSource.setInitialPoolSize(Integer.parseInt(environment.getProperty("connection.pool.initialPoolSize")));
        securityDataSource.setMinPoolSize(Integer.parseInt(environment.getProperty("connection.pool.minPoolSize")));
        securityDataSource.setMaxPoolSize(Integer.parseInt(environment.getProperty("connection.pool.maxPoolSize")));
        securityDataSource.setMaxIdleTime(Integer.parseInt(environment.getProperty("connection.pool.maxIdleTime")));

        return securityDataSource;
    }

}
这里有几个地方要解释:
  1. private Environment environment;由于是类配置,这里使用了注入方式装配一个域,实际上是装配了一个Bean(Spring官方不推荐这么做),当配置了@PropertySource之后,Spring内部实际上会将这个配置文件载入到环境对象中。使用注入方式取得了环境的Bean对象,进而可以获取配置文件的具体内容。
  2. public DataSource securityDataSource()这个是组装DataSource的Bean
  3. environment.getProperty("***")方法就是从配置文件中取出键对应的值
在完成了Spring的配置之后,项目运行后容器中就有了一个以DataSource组装的Bean,接下来就可以配置Spring Security的配置文件了。这里需要做的是注入刚才的Bean给Spring Security容器,然后去掉硬编码的用户等信息,改成让其从数据库中获取。修改后的类如下:
package cc.conyli.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        //使用不加密的密码验证
        User.UserBuilder users = User.withDefaultPasswordEncoder();

        //使用数据连接池作为验证来源
        auth.jdbcAuthentication().dataSource(securityDataSource);

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").hasRole("EMPLOYEE")
                .antMatchers("/leader/**").hasRole("MANAGER")
                .antMatchers("/system/**").hasRole("ADMIN")
                .and()
                .formLogin().loginPage("/showMyLoginPage").loginProcessingUrl("/authenticateTheUser")
                .permitAll()
                .and()
                .logout().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/access-denied/");
    }

    //把Spring IOC容器的Bean注入到securityDataSource这个对象中。
    @Autowired
    private DataSource securityDataSource;
}
此时再启动项目,发现没有硬编码了,但是项目功能依然正常。这里实际上要注意字符串形式的角色名ADMIN与数据库里字段ROLE_ADMIN的对应关系,以及输入的明文密码test123{noop}test123的对应关系。

使用自定义数据表

很多情况下,默认数据表满足不了我们的要求,就需要自定义数据表。 使用自定义数据表的关键是告诉Spring Security如何获取用户名,密码和用户名对应的角色。开发步骤如下:
  1. 创建自定义的数据表
  2. 配置JDBC连接到新的数据表
  3. 修改Spring Security的配置,其中需要提供如何查询用户和如何通过用户查询角色的方法。
这一次我们创建自定义的数据表,名称和字段都与默认的不同:
Custom SS Database Schema
members roles
user_id VARCHAR(50) 主键 user_id VARCHAR(50) 外键关联到members表主键
pw VARCHAR(68) role VARCHAR(50)
active TINYINT(1)
使用如下SQL来创建:
DROP DATABASE  IF EXISTS `spring_security_demo_custom`;

CREATE DATABASE  IF NOT EXISTS `spring_security_demo_custom`;
USE `spring_security_demo_custom`;

DROP TABLE IF EXISTS `members`;
CREATE TABLE `members` (
  `user_id` varchar(50) NOT NULL,
  `pw` char(68) NOT NULL,
  `active` tinyint(1) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


INSERT INTO `members`
VALUES
('john','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1),
('mary','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1),
('susan','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1);


DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
  `user_id` varchar(50) NOT NULL,
  `role` varchar(50) NOT NULL,
  UNIQUE KEY `roles_idx_1` (`user_id`,`role`),
  CONSTRAINT `roles_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `members` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


INSERT INTO `roles`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_ADMIN');
之后修改JDBC中的连接,将数据库名修改为spring_security_demo_custom(代码省略),然后在配置类内,在配置JDBC使用数据库的那条语句后边追加链式调用的语句:
@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        User.UserBuilder users = User.withDefaultPasswordEncoder();

        auth.jdbcAuthentication().dataSource(securityDataSource)
            .usersByUsernameQuery("SELECT user_id, pw, active from members where user_id=?")
            .authoritiesByUsernameQuery("SELECT user_id, role from roles where user_id=?");
    }
这两个语句的意思是告诉Spring Security使用用户名作为查询语句的参数,如何到数据库里查出来用户数据和对应的角色。Spring Security会自动将依次查出来的字段作为用户名,密码和激活与否。然后也会拿着同一个用户名去查询对应的角色表,将查出的第二列数据作为角色。 这样就完成了自定义数据表的功能。
LICENSED UNDER CC BY-NC-SA 4.0
Comment