[spring] rest api security

时间:2024-04-19 07:05:17

[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;
    }

效果如下:

在这里插入图片描述