百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

精通Spring Boot 3 : 9. Spring Boot 安全 (2)

ccwgpt 2025-01-14 11:16 41 浏览 0 评论

用户应用的安全性测试

Spring Security 提供了多种测试代码的方法。我们将从集成测试开始,使用在前几章中熟悉的 TestRestTemplate 类。

创建或打开 UsersHttpRequestTests 类。请参阅列表 9-3。

package com.apress.users;
import com.apress.users.model.User;
import com.apress.users.model.UserRole;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import java.util.Arrays;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UsersHttpRequestTests {
    private final String USERS_PATH = "/users";
    @Autowired
    private TestRestTemplate restTemplate;
    @Test
    public void indexPageShouldReturnHeaderOneContent() throws Exception {
        String html = this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").getForObject("/", String.class);
        assertThat(html).contains("Simple Users Rest Application");
    }
    @Test
    public void usersEndPointShouldReturnCollectionWithTwoUsers() throws Exception {
        Collection<User> response = this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").
                getForObject(USERS_PATH, Collection.class);
        assertThat(response.size()).isGreaterThan(1);
    }
    @Test
    public void userEndPointPostNewUserShouldReturnUser() throws Exception {
        User user =  new User("dummy@email.com","Dummy",
                "https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar",
                "SomeOtherAw2s0meR!", Arrays.asList(UserRole.USER),true);
        User response =  this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").postForObject(USERS_PATH,user, User.class);
        assertThat(response).isNotNull();
        assertThat(response.getEmail()).isEqualTo(user.getEmail());
        Collection<User> users = this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").
                getForObject(USERS_PATH, Collection.class);
        assertThat(users.size()).isGreaterThanOrEqualTo(2);
    }
    @Test
    public void userEndPointDeleteUserShouldReturnVoid() throws Exception {
        this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").delete(USERS_PATH + "/norma@email.com");
        Collection<User> users = this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").
                getForObject(USERS_PATH, Collection.class);
        assertThat(users.size()).isLessThanOrEqualTo(2);
    }
    @Test
    public void userEndPointFindUserShouldReturnUser() throws Exception{
        User user = this.restTemplate.withBasicAuth("manager@email.com","aw2s0meR!").getForObject(USERS_PATH + "/ximena@email.com",User.class);
        assertThat(user).isNotNull();
        assertThat(user.getEmail()).isEqualTo("ximena@email.com");
    }
}

9-3 src/test/java/com/apress/users/UsersHttpRequestTests.java

列表 9-3 展示了在您的应用程序中添加安全性时的集成测试。与前几章唯一的不同之处在于,我们现在使用带有基本身份验证的 TestRestTemplate,该方法接受用户名和密码作为参数。由于用户应用程序已配置为使用基本身份验证,因此这个方法正是我们所需要的。如果您运行测试,它们将会通过。

安全测试模拟

模拟是一种测试安全性的方法,当我们需要隔离特定对象的行为时。通过这种方式,我们可以模拟真实对象的行为(在这里是安全性),并专注于我们业务逻辑的某一部分。

创建或打开 UserMockMvcTests 类,详见列表 9-4。

package com.apress.users;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@WithMockUser
@AutoConfigureMockMvc
public class UserMockMvcTests {
    @Autowired
    MockMvc mockMvc;
    @Test
    void createUserTests() throws Exception {
        String location = mockMvc.perform(post("/users")
                        .contentType("application/json")
                        .content("""
                        {
                            "email": "dummy@email.com",
                            "name": "Dummy",
                            "password": "aw2s0meR!",
                            "gravatarUrl": "https://www.gravatar.com/avatar/fb651279f4712e209991e05610dfb03a?d=wavatar",
                            "userRole": ["USER"],
                            "active": true
                        }
                       """))
                .andExpect(status().isCreated())
                .andExpect(header().exists("Location"))
                .andReturn().getResponse().getHeader("Location");
        mockMvc.perform(get(location))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").exists())
                .andExpect(jsonPath("$.active").value(true));
    }
    @Test
    void getAllUsersTests() throws Exception {
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].name").value("Ximena"))
                .andExpect(jsonPath("$..active").value(hasItem(true)));
    }
}

9-4 src/test/java/com/apress/users/UserMockMvcTests.java

UserMockMvcTests 类包含以下内容:

  • 正如您所知,这个注解用于配置 MockMvc 对象,以便测试 Web 应用。
  • MockMvc:正如您所知,这个对象可以模拟任何 HTTP 请求和响应,而无需使用真实的 Web 服务器。在这种情况下,我们使用 POST 和 GET 请求。
  • @WithMockUser:这个注解来自 spring-security-test 依赖,允许我们在安全上下文中模拟用户,这样我们就可以在不担心身份验证的情况下测试功能。当然,如果你删除这个注解,测试将会失败。@WithMockUser 注解可以接受参数;例如,你可以这样使用:@WithMockUser(username = "user", password = "password", roles = "USER"),这样可以确保用户已被认证或在需要时拥有相应权限。它可以作为类注解使用,影响所有测试方法,或者作为方法注解使用,以便在模拟时获得更多控制。

现在,你知道你可以选择进行安全测试,既可以进行单元测试,也可以进行集成测试。

在用户应用中使用持久性实现安全身份验证

我们刚刚看到,可以使用内存中的用户进行安全身份验证,但在实际场景中,用户存储在数据库中。幸运的是,Spring Security 提供了一种简单的方法,可以使用标准的 JDBC 实现 UserDetailsService。您需要提供用户名、密码、账户状态(启用或禁用)以及角色。列表 9-5 中的 SQL 架构遵循这一标准实现。

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(50) not null,
    enabled boolean not null
);
create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

Spring Security 的 SQL 模式(列表 9-5)

Spring Security 根据所使用的数据库引擎(如 PostgreSQL、Oracle、MySQL 等)提供了专门的数据库架构。您可以在此链接找到更多详细信息:https://docs.spring.io/spring-security/reference/servlet/appendix/database-schema.html。

如果您想使用默认的架构,只需在 UserSecurityConfig 类中将 UserDetailsManager 的返回类型更改为 JdbcUserDetailsManager。

打开 UserSecurityConfig 类,并将所有代码替换为列表 9-6 中显示的内容。

package com.apress.users.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.userdetails.User;
import javax.sql.DataSource;
import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.H2;
@Configuration
public class UserSecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests( auth -> auth.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
    @Bean
    DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(H2)
                .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
                .build();
    }
    @Bean
    UserDetailsManager userDetailsManager(PasswordEncoder passwordEncoder, DataSource dataSource){
        UserDetails admin = User
                .builder()
                .username("admin")
                .password(passwordEncoder.encode("admin"))
                .roles("ADMIN","USER")
                .build();
        UserDetails manager = User
                .builder()
                .username("manager@email.com")
                .password(passwordEncoder.encode("aw2s0meR!"))
                .roles("ADMIN","USER")
                .build();
        UserDetails user = User
                .builder()
                .username("user@email.com")
                .password(passwordEncoder.encode("aw2s0meR!"))
                .roles("USER")
                .build();
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        users.createUser(admin);
        users.createUser(manager);
        users.createUser(user);
        return users;
    }
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

9-6 src/main/java/com/apress/users/security/UserSecurityConfig.java

列表 9-6 展示了新的 UserSecurityConfig 类。请注意,我们的 SecurityFilterChain 没有变化,只有 DataSource 和 JdbcUserDetailsManager 发生了变化。

就这样。如果你运行相同的单元测试和集成测试,它们应该能够顺利通过。

使用自定义持久化提高您的身份验证安全性

在这一部分,我想向你展示如何使用自定义模型来为你的应用程序提供部分身份验证和授权。在这种情况下,我将展示用户应用程序已经包含 Spring Security 所需的信息。当然,我们的模型还提供了一些额外的信息,这些信息对我们的领域非常重要,但也可以用于安全(身份验证和授权)目的。

要使用我们的模型,我们需要将 UserSecurityConfig 类中的所有代码替换为清单 9-7 中所示的代码。

package com.apress.users.security;
import com.apress.users.repository.UserRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class UserSecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http,AuthenticationProvider authenticationProvider) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests( auth -> auth.anyRequest().authenticated())
                .authenticationProvider(authenticationProvider)
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
    @Bean
    public AuthenticationProvider authenticationProvider(UserRepository userRepository){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(new UserSecurityDetailsService(userRepository));
        return provider;
    }
}

9-7 src/main/java/com/apress/users/security/UserSecurityConfig.java

让我们来回顾一下修改后的 UserSecurityConfig 类

  • 认证提供者:在这个例子中,我们使用自己的数据库来提供认证和授权的数据,因此我们需要指定一个提供者来执行这种类型的认证。您可以提供多个由 ProviderManager 管理的 AuthenticationProvider 实例。实际上,Spring Security 已经包含了 DaoAuthenticationProvider、JwtAuthenticationProvider(用于处理 JWT 令牌)、LdapAuthenticationProvider、CasAuthenticationProvider 等。当然,您也可以创建自己的自定义认证提供者。
  • 使用 AuthenticationProvider 的主要好处之一是它已经实现了您所需的功能,您只需提供获取数据的逻辑,即用户名/密码和权限/角色。在这个例子中,我们正在设置 UserSecurityDetailsService,它使用存储我们数据的 UserRepository。
  • 在这个例子中,我们创建了一个新的 UserSecurityDetailsService 类的实例,该类依赖于 UserRepository。这个类实现了 UserDetailsService,用于存储身份验证过程中的数据,包括用户名、密码、权限或角色,以及在身份验证时可能有用的其他属性。

接下来,打开或创建 UserSecurityDetailsService 类。请参阅第 9-8 号列表。

package com.apress.users.security;
import com.apress.users.exception.UserNotFoundException;
import com.apress.users.model.User;
import com.apress.users.model.UserRole;
import com.apress.users.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@AllArgsConstructor
public class UserSecurityDetailsService implements UserDetailsService {
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findById(username)
                .orElseThrow(UserNotFoundException::new);
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        return org.springframework.security.core.userdetails.User
                .withUsername(username)
                .roles(user.getUserRole().stream().map(UserRole::toString).toArray(String[]::new))
                .password(passwordEncoder.encode(user.getPassword()))
                .accountExpired(!user.isActive())
                .build();
    }
}

9-8 src/main/java/com/apress/users/security/UserSecurityDetailsService.java

在 UserSecurityDetailsService 类中,我们实现了 UserDetailsService,以便检索用户名、密码和其他用于身份验证的有用属性。在这种情况下,我们使用 User.isActive 方法将 accountExpired 属性作为示例,并使用 UserRole 实例来收集角色列表。

现在你知道有多种方法可以配置 Spring Security 的身份验证,从默认的用户名“用户”和控制台中随机生成的密码,到内存和持久性存储(使用默认模式或自定义模式),甚至 LDAP。如果你实现了 UserDetails,你可以插入任何你想要用来提供身份验证和授权的内容。

相关推荐

定时任务工具,《此刻我要...》软件体验

之前果核给大家介绍过一款小众但实用的软件——小说规则下载器,可以把网页里的小说章节按照规则下载到本地,非常适合喜欢阅读小说的朋友。有意思的是,软件作者当时看到果核写的体验内容后,给反推荐到他的帖子里去...

前端定时任务的神库:Node-cron,让你的项目更高效!

在前端开发中,定时任务是一个常见的需求。无论是定时刷新数据、轮询接口,还是发送提醒,都需要一个可靠且灵活的定时任务解决方案。今天,我要向大家介绍一个强大的工具——Node-cron,它不仅能解决定时任...

Shutter Pro!一款多功能定时执行任务工具

这是一款可以在电脑上定时执行多种任务的小工具,使用它可以根据时间,电量等来设定一些定时任务,像定时打开程序、打开文件,定时关机重启,以及定时弹窗提醒等都可以轻松做到。这是个即开即用的小工具,无需安装,...

深度解析 Redis 缓存击穿及解决方案

在当今互联网大厂的后端开发体系中,Redis缓存占据着极为关键的地位。其凭借高性能、丰富的数据类型以及原子性操作等显著优势,助力众多高并发系统从容应对海量用户的访问冲击,已然成为后端开发从业者不可或...

从零搭建体育比分网站完整步骤(比较好的体育比分软件)

搭建一个体育比分网站是一个涉及前端、后端、数据源、部署和维护的完整项目。以下是从零开始搭建的详细流程:一、明确项目需求1.功能需求:实时比分展示(如足球、篮球、网球等)支持多个联赛和赛事历史数据查询比...

告别复杂命令行:GoCron 图形界面让定时任务触手可及

如果你是运维人员或者经常接触一些定时任务的配置,那么你一定希望有一款图形界面来帮助你方便的轻松配置定时任务,而GoCron就是这样一款软件,让你的配置可视化。什么是GoCron从名字你就可以大概猜到,...

Java任务管理框架核心技术解析与分布式高并发实战指南

在当今数字化时代,Java任务管理框架在众多应用场景中发挥着关键作用。随着业务规模的不断扩大,面对分布式高并发的复杂环境,掌握其核心技术并进行实战显得尤为重要。Java任务管理框架的核心技术涵盖多个方...

链表和结构体实现:MCU软件定时器(链表在单片机中的应用)

在一般的嵌入式产品设计中,介于成本、功耗等,所选型的MCU基本都是资源受限的,而里面的定时器的数量更是有限。在我们软件设计中往往有多种定时需求,例如脉冲输出、按键检测、LCD切屏延时等等,我们不可能...

SpringBoot定时任务(springboot定时任务每小时执行一次)

前言在我们开发中,经常碰到在某个时间点去执行某些操作,而我们不能人为的干预执行,这个时候就需要我们使用定时任务去完成该任务,下面我们来介绍下载springBoot中定时任务实现的方式。定时任务实现方式...

定时任务新玩法!systemd timer 完整实战详解

原文链接:「链接」Hello,大家好啊!今天给大家带来一篇使用systemdtimer实现定时任务调度的详细实战文章。相比传统的crontab,systemdtimer更加现代化、结构清晰...

Celery与Django:打造高效DevOps的定时任务与异步处理神器

本文详细介绍了Celery这一强大的异步任务队列系统,以及如何在Django框架中应用它来实现定时任务和异步处理,从而提高运维开发(DevOps)的效率和应用性能。下面我们先认识一下Cele...

订单超时自动取消的7种方案,我用这种!

前言在电商、外卖、票务等系统中,订单超时未支付自动取消是一个常见的需求。这个功能乍一看很简单,甚至很多初学者会觉得:"不就是加个定时器么?"但真到了实际工作中,细节的复杂程度往往会超...

裸机下多任务框架设计与实现(gd32裸机配置lwip 网络ping不通)

在嵌入式系统中,特别是在没有操作系统支持的裸机环境下,实现多任务执行是一个常见的挑战。本文将详细介绍一种基于定时器的多任务框架设计,通过全局时钟和状态机机制,实现任务的非阻塞调度,确保任务执行中不会出...

亿级高性能通知系统构建,小白也能拿来即用

作者介绍赵培龙,采货侠JAVA开发工程师分享概要一、服务划分二、系统设计1、首次消息发送2、重试消息发送三、稳定性的保障1、流量突增2、问题服务的资源隔离3、第三方服务的保护4、中间件的容错5、完善...

运维实战:深度拆解Systemd定时任务原理,90%的人不知道的玩法

运维实战:深度拆解Systemd定时任务原理,90%的人不知道的高效玩法一、Systemd定时任务的核心原理Systemd定时任务是Linux系统中替代传统cron的现代化解决方案,通过...

取消回复欢迎 发表评论: