使用数据库存储用户信息和角色
观察我们的配置类可以发现,我们目前把所有的用户名,密码和角色信息都硬编码在配置类里。角色名一旦有变动,需要修改全部代码中对应的这个角色名。这显然不够好。更常见的做法是用数据库来存储相关的内容。每次进行验证的时候,到数据库中去存取数据。
Spring Security支持从数据库中读取用户信息,默认情况下,存放用户的数据表必须符合Spring Security预定义的格式,也可以自定义数据格式。
先来使用预定义的格式,把目前的权限验证弄到数据库里去。
使用默认数据表
开发步骤如下:
- 使用SQL脚本创建数据库
- 在Maven中配置数据库相关依赖包
- 创建JDBC的配置文件
- 在Spring里配置数据库相关的Bean
- 修改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;
}
}
这里有几个地方要解释:
private Environment environment;
由于是类配置,这里使用了注入方式装配一个域,实际上是装配了一个Bean(Spring官方不推荐这么做),当配置了@PropertySource
之后,Spring内部实际上会将这个配置文件载入到环境对象中。使用注入方式取得了环境的Bean对象,进而可以获取配置文件的具体内容。
public DataSource securityDataSource()
这个是组装DataSource的Bean
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如何获取用户名,密码和用户名对应的角色。开发步骤如下:
- 创建自定义的数据表
- 配置JDBC连接到新的数据表
- 修改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会自动将依次查出来的字段作为用户名,密码和激活与否。然后也会拿着同一个用户名去查询对应的角色表,将查出的第二列数据作为角色。
这样就完成了自定义数据表的功能。