[spring] rest api security
之前的 rest api CRUD 都没有实现验证(authentication)和授权(Authorization),这里使用 Spring security 进行补全
spring security 是一个非常灵活、可延伸的实现方式,比较简单的可以通过注解(declarative)的方式实现,想要更具体的,也可以通过编程式(programmatic)实现。
整体流程大概如下:
POM 更新
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一旦添加了 dependency、rebuild 之后,spring boot 就会默认开启验证功能,默认情况下提供的密码:
效果如下:
修改默认密码的方式是修改 properties 文件:
# Spring Security Property
spring.security.user.name=admin
spring.security.user.password=1234
效果如下:
基础实现
用户验证
下面是一个基础的用户权限管理:
User ID | Password | Roles |
---|---|---|
worker | pass1234 | employee |
boss | pwdSecure | employee, manager |
admin | 789password | employee, manager, admin |
这里可以通过手写的方式写入用户名和权限,完成基础设定:
@Configuration
public class DemoSecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails worker = User.builder().username("worker").password("{noop}pass1234").roles("employee").build();
UserDetails boss = User.builder().username("boss").password("{noop}pass1234").roles("employee", "manager")
.build();
UserDetails admin = User.builder().username("admin").password("{noop}pass1234")
.roles("employee", "manager", "admin").build();
return new InMemoryUserDetailsManager(worker, boss, admin);
}
}
这个情况下 spring 会选择代码中写入的用户名和密码,而不是 properties 文件中,效果如下:
权限设定
权限的部分则是通过对 role 的授权实现,例如说 employee 只有读的权利,manager 有写的权利,admin 有删的权限:
HTTP Method | Endpoint | CRUD Action | Role |
---|---|---|---|
GET | /api/employees | Read All | employee |
GET | /api/employees/{id} | Read Single | employee |
POST | /api/employees | Create | manager |
PUT | /api/employees/{id} | Update | manager |
DELETE | /api/employees/{id} | Delete | admin |
这个部分可以通过添加 requestMatchers
实现,如:
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(configurer ->
configurer
.requestMatchers(HttpMethod.GET, "/api/employees").hasRole("employee")
.requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("employee")
.requestMatchers(HttpMethod.POST, "/api/employees").hasRole("manager")
.requestMatchers(HttpMethod.PATCH, "/api/employees").hasRole("manager")
.requestMatchers(HttpMethod.PUT, "/api/employees").hasRole("manager")
.requestMatchers(HttpMethod.PUT, "/api/employees/**").hasRole("manager")
.requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("admin")
);
// use http basic auth
httpSecurity.httpBasic(Customizer.withDefaults());
// disable CSRF
// in general, not required for stateless REST APIs
httpSecurity.csrf(AbstractHttpConfigurer::disable);
return httpSecurity.build();
}
效果如下:
role | CRUD | 结果 |
---|---|---|
employee | delete | ❌ |
employee | create | ❌ |
employee | get | ✅ |
boss | create | |
boss | delete | |
admin | create | |
admin | delete |
使用 JDBC 链接
这里使用数据库代替硬代码去实现
明文密码
这个 demo 使用明文密码进行实现,这样比较直观
sql 配置
USE `employee_directory`;
DROP TABLE IF EXISTS `authorities`;
DROP TABLE IF EXISTS `users`;
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Inserting data for table `users`
--
INSERT INTO `users`
VALUES
('john','{noop}test123',1),
('mary','{noop}test123',1),
('susan','{noop}test123',1);
--
-- Table structure for table `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;
--
-- Inserting data for table `authorities`
--
INSERT INTO `authorities`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');
这里的表名是 spring security 默认的名称,后面会说怎么配置,从而可以不用默认的表名。另外就是 {noop}test123
,这个语法表示 no-op,即不对用户名加密。结果如下:
mysql> show tables from employee_directory;
+------------------------------+
| Tables_in_employee_directory |
+------------------------------+
| authorities |
| employee |
| users |
+------------------------------+
3 rows in set (0.00 sec)
mysql> select * from employee_directory.authorities;
+----------+---------------+
| username | authority |
+----------+---------------+
| john | ROLE_EMPLOYEE |
| mary | ROLE_EMPLOYEE |
| mary | ROLE_MANAGER |
| susan | ROLE_ADMIN |
| susan | ROLE_EMPLOYEE |
| susan | ROLE_MANAGER |
+----------+---------------+
6 rows in set (0.00 sec)
mysql> select * from employee_directory.users;;
+----------+---------------+---------+
| username | password | enabled |
+----------+---------------+---------+
| john | {noop}test123 | 1 |
| mary | {noop}test123 | 1 |
| susan | {noop}test123 | 1 |
+----------+---------------+---------+
3 rows in set (0.00 sec)
后面会有 dbeaver 而不是命令行显示数据,这里是 dbeaver 生成的 ER Diagram:
修改 POM 和配置文件
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
这个是添加对 mysql 的支持,下面是 properties 文件的修改:
# JDBC properties
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
修改 java 代码
这个只需要修改 userDetailsManager
里的 dataSource 即可,这部分 srping 也会自动进行依赖注入:
// add support for JDBC
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager((dataSource));
}
最终效果如下:
⚠️:employee
, admin
, manager
在数据库里是大写的,之前实现是小写,所以需要修改
bcrypt 加密
bcrypt 是一个单方向的加密方式,无法通过已经 hash 的值去解密,这也是 spring security 支持的密码加密方式。
这里网上随便找了一个 bcrypt 加密的网站显示一下 string 对比的结果:
⚠️:就算是同一个 string,每次加密后获得的 hash 值都不会完全一致
sql 配置
USE `employee_directory`;
DROP TABLE IF EXISTS `authorities`;
DROP TABLE IF EXISTS `users`;
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` char(68) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Inserting data for table `users`
--
-- NOTE: The passwords are encrypted using BCrypt
--
INSERT INTO `users`
VALUES
('john','{bcrypt}$2y$08$I9qqnyR8fMLO/WFQWLjzfe7TCz6357dM/CaXgppCReDdSMktqUIPW',1),
('mary','{bcrypt}$2y$08$I9qqnyR8fMLO/WFQWLjzfe7TCz6357dM/CaXgppCReDdSMktqUIPW',1),
('susan','{bcrypt}$2y$08$I9qqnyR8fMLO/WFQWLjzfe7TCz6357dM/CaXgppCReDdSMktqUIPW',1);
--
-- Table structure for table `authorities`
--
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `authorities4_idx_1` (`username`,`authority`),
CONSTRAINT `authorities4_ibfk_1` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Inserting data for table `authorities`
--
INSERT INTO `authorities`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');
查看数据库,确定密码已经从明文更新为 bcrypt:
⚠️:bcrypt 的密码是 test1234
这个时候运行一下 postman,原本的 123 验证会失败,但是 1234 会通过:
自定义表名
sql 配置
USE `employee_directory`;
DROP TABLE IF EXISTS `roles`;
DROP TABLE IF EXISTS `members`;
--
-- Table structure for table `members`
--
CREATE TABLE `members` (
`user_id` varchar(50) NOT NULL,
`pw` char(68) NOT NULL,
`active` tinyint NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Inserting data for table `members`
--
-- NOTE: The passwords are encrypted using BCrypt
--
INSERT INTO `members`
VALUES
('john','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('mary','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('susan','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1);
--
-- Table structure for table `authorities`
--
CREATE TABLE `roles` (
`user_id` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE KEY `authorities5_idx_1` (`user_id`,`role`),
CONSTRAINT `authorities5_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `members` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Inserting data for table `roles`
--
INSERT INTO `roles`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');
这里会创建两个新的表去建立关联:
其中 role 等同于 auth,members 等同于 user
⚠️:这里新修改的密码是 fun123
java 更新
这里更新的地方要让 jdbcUserDetailsManager 能够找到用户和权限的表,就需要新写一下 query,让 spring security 通过新的 query 去找到 user 和 role:
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager((dataSource));
// define query to retrieve a user by username
jdbcUserDetailsManager.setUsersByUsernameQuery(
"select user_id, pw, active from members where user_id=?"
);
// define query to retrieve the roles by username
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
"select user_id, role from roles where user_id=?"
);
return jdbcUserDetailsManager;
}
效果如下: