news 2026/5/1 9:29:52

初探 Spring Security

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
初探 Spring Security

前言

在當今瞬息萬變的 Web 環境中,應用程式安全比以往任何時候都更加重要。為保護服務、資料等各項資源,不被任意存取。Spring 提供了 Spring Security 驗證框架,它能幫助我們開發有關認證與授權等有關安全管理的功能。下面讓我們透過簡單的例子初窺如何運用。

專案實作

註: 基於 初探 Vue 與 Spring boot 的對話之Backend (SpringBoot-Backend)文章 專案延生

1. 新增 相關 Dependencies

Pom.xml

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>

備註:

spring-security-test 官方提供的測試套件,用來在 單元測試 與整合測試 中方便地測試與 Spring Security 相關的功能

2.增修相關代碼

增修 Web 安全性, 網路安全配置類別 WebSecurityConfig

/* Web 安全性配置, 網路安全配置 */ @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**", "/login").permitAll() .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/api/users/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/api/user").hasRole("ADMIN") .requestMatchers(HttpMethod.DELETE, "/api/users/*") .hasRole("ADMIN") .anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .formLogin(Customizer.withDefaults()) .build(); } /** * 使用 InMemoryUserDetailsManager,建立帳號與密碼並儲存於記憶體中 * 用於測試,定義帶有不同權限的用戶。 */ @Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { // ADMIN 用戶:擁有 ADMIN 角色 UserDetails admin = User .withUsername("admin") .password(passwordEncoder.encode("password")) .roles("ADMIN") .build(); // USER 用戶:擁有 USER 角色 UserDetails normalUser = User .withUsername("user") .password(passwordEncoder.encode("password")) .roles("USER") .build(); // GUEST 用戶:沒有任何角色 UserDetails guest = User .withUsername("guest") .password(passwordEncoder.encode("password")) .roles("GUEST") .build(); return new InMemoryUserDetailsManager(admin, normalUser, guest); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

備註:

/api/users 僅匹配 完全相同 的路徑。

例子:

/api/users 匹配

/api/users/ 不匹配

/api/users/123 不匹配

/api/users/** 匹配以 /api/users/ 開頭的 所有路徑,無論子路徑有多少層級。

例子:

/api/users 匹配

/api/users/ 匹配

/api/users/123 匹配

/api/users/data/1 匹配

增修 Entity

增修 Entity @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username", nullable = false) private String username; @Column(name = "password", nullable = false) private String password; @Column(name = "first_name", nullable = false) private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "email", nullable = false, unique = true) private String email; public User(String username, String password, String firstName, String lastName, String email) { this.username = username; this.password = password; this.firstName = firstName; this.lastName = lastName; this.email = email; } }

增修UserRepository

@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Boolean existsByUsername(String username); Boolean existsByEmail(String email); }

增修UserSerice

@Slf4j @Service public class UserService { @Autowired private UserRepository userRepository; @Transactional @PreAuthorize("hasAnyRole('ADMIN', 'USER')") public User saveUser(User user) { log.info("Saving user: " + user.getUsername()); if (user == null) { throw new IllegalArgumentException("User must not be null"); } return userRepository.save(user); } /* * @PreAuthorize: 在方法執行之前,決定是否允許訪問 */ @PreAuthorize("hasAuthority('ADMIN')") public List<User> getUsers() { List<User> users = null; try { users = userRepository.findAll(); log.debug("Number of users fetched: " + users.size()); } catch (Exception e) { e.printStackTrace(); } return users; } public User getUserById(Long uid) { if (uid == null) { throw new UserNotFoundException(null); } User user = userRepository.findById(uid) .orElseThrow(() -> new UserNotFoundException(uid)); return user; } public User updateUser(@RequestBody User newUser, @PathVariable Long id) { log.info("Updating user with id: " + id); return userRepository.findById(id) .map(user -> { user.setUsername(null == newUser.getUsername() ? user.getUsername() : newUser.getUsername()); user.setPassword(null == newUser.getPassword() ? user.getPassword() : newUser.getPassword()); user.setFirstName(null == newUser.getFirstName() ? user.getFirstName() : newUser.getFirstName()); user.setLastName(null == newUser.getLastName() ? user.getLastName() : newUser.getLastName()); user.setEmail(null == newUser.getEmail() ? user.getEmail() : newUser.getEmail()); return userRepository.save(user); }) .orElseGet(() -> { return userRepository.save(newUser); }); } public void deleteUser(Long uid) { if (uid == null) { throw new UserNotFoundException(null); } userRepository.deleteById(uid); } }

增修Controller

@Slf4j @RestController @RequestMapping("/api") public class UserController { @Autowired private UserService userService; @Autowired private UserRepository userRepository; @GetMapping("/public") public String publicApi() { return "public OK"; } @PostMapping("/user") public ResponseEntity<?> createUser(@RequestBody User newUser) { User user = userService.saveUser(newUser); return ResponseEntity.ok(user); } @GetMapping("/user/{uid}") public User getUserById(@PathVariable Long uid) { return userService.getUserById(uid); } @GetMapping("/users") public List<User> getAllUsers() { List<User> users = userRepository.findAll(); return users; } @PutMapping("/users/{uid}") User replaceUser(@RequestBody User newUser, @PathVariable Long uid) { return userService.updateUser(newUser, uid); } @DeleteMapping("/users/{uid}") @PreAuthorize("hasAuthority('delete')") void deleteUser(@PathVariable Long uid) { userService.deleteUser(uid); } }

建立SpringBootTestMockMvc測試範例

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.dannyyu.backend.model.User; import com.fasterxml.jackson.databind.ObjectMapper; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.http.MediaType; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class SecurityTest { @Autowired private WebApplicationContext context; private MockMvc mockMvc; static Long uid = 0L; @BeforeEach public void setup() throws Exception { mockMvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); } @Test @Order(7) @WithMockUser(username = "admin", authorities = { "delete", "ROLE_ADMIN" }) void testDeleteUser() throws Exception { mockMvc.perform(delete("/api/users/" + uid)) .andExpect(status().isOk()); } @Test @Order(1) @WithMockUser(username = "admin", roles = { "ADMIN" }) public void testCreateUser() throws Exception { MvcResult result = null; User createdUser = null; String json = ""; User user = new User("test", "123456", "test", "wu", "test@example.com"); String jsoString = asJsonString(user); result = mockMvc.perform( MockMvcRequestBuilders .post("/api/user") .content( jsoString) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); json = result.getResponse().getContentAsString(); createdUser = new ObjectMapper().readValue(json, User.class); uid = createdUser.getId(); } // Public API 無需登入 @Test @Order(2) @WithMockUser(roles = { "USER" }) void testPublicApi() throws Exception { mockMvc.perform(get("/api/public")) .andExpect(status().isOk()); } // 測試 USER角色可存取 /user @Test @Order(3) @WithMockUser(username = "user", roles = { "USER" }) void testUserApi() throws Exception { mockMvc.perform(get("/api/user/" + uid)) .andExpect(status().isOk()); } // 測試 USER 不能存取 /admin @Test @Order(4) @WithMockUser(roles = { "USER" }) void testAdminDenied() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isForbidden()); } // ADMIN 可存取 /users @Test @Order(5) @WithMockUser(roles = { "ADMIN" }) void testAdminApi() throws Exception { mockMvc.perform(get("/api/users")) .andExpect(status().isOk()); } @Test @Order(6) void testUserApiWithRequestPostProcessor() throws Exception { String responseBody = mockMvc.perform( get("/api/user/" + uid).with( user("admin").roles("ADMIN"))) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); System.out.println("****** 取得角色: ******"); System.out.println(responseBody); } public static String asJsonString(final Object obj) { try { return new ObjectMapper().writeValueAsString(obj); } catch (Exception e) { throw new RuntimeException(e); } } }

執行測試案例,成功

% mvn test

. . .

[INFO] -------------------------------------------------------

[INFO] T E S T S

[INFO] -------------------------------------------------------

[INFO] Running com.dannyyu.backend.controller.SecurityTest

. . .

. . .

Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from users u1_0 where u1_0.id=?

Hibernate: delete from users where id=?

[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.966 s -- in com.dannyyu.backend.controller.SecurityTest

[INFO]

[INFO] Results:

[INFO]

[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0

[INFO]

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS

. . .

使用 Browser 測試

測試 USER 不能存取 /users (只有admin才可以查看所有使用者)

使用 user 登入

沒處理登入後頁面, 出現 Error Page, 不用擔心,沒事

修改 URL http://localhost:8088/api/users , Enter (查看所有使用者)

權限不足導致請求失敗

有關HTTP 回應狀態碼可參看以下網址

https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Reference/Status

打http://localhost:8088/logout 登出

改使用 Admin 登入

修改 URL http://localhost:8088/api/users , Enter (查看所有使用者)

測試資料來源 users 資料表,不是 admin, user。因為,用於測試,使用 InMemoryUserDetailsManager,建立帳號與密碼並儲存於記憶體中。

查看 WebSecurityConfig

文章到此完結。希望都有所得。
謝謝!

祝妳好運!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 18:18:11

企业合规必备|2025 算法备案实操指南:避坑要点 + 未合规风险

某物流科技公司优化智能调度算法后&#xff0c;因未完成备案被要求整改&#xff0c;同时面临相应处罚&#xff1b;某资讯平台因备案流程滞后&#xff0c;推荐功能暂停服务数日&#xff0c;用户活跃度出现明显下降……​ 2025 年&#xff0c;算法备案已成为互联网相关企业的合规…

作者头像 李华
网站建设 2026/5/1 7:24:46

ModelEngine API与SDK深度解析与实战指南:从零构建AI应用的完整手册

ModelEngine API与SDK深度解析与实战指南&#xff1a;从零构建AI应用的完整手册 【免费下载链接】doc ModelEngine开源项目公共文档库 项目地址: https://gitcode.com/ModelEngine/doc 想要快速上手ModelEngine开发&#xff1f;别慌&#xff01;这篇指南将带你从基础概念…

作者头像 李华
网站建设 2026/5/1 4:05:50

《破局核心领域 “卡脖子”:国产数据库四十年进化史与 2025 新机遇》

本文聚焦2025年国产数据库行业核心动态&#xff0c;结合信创政策导向、最新技术突破及关键行业落地实践&#xff0c;系统梳理发展脉络、技术路线差异、头部产品竞争力及未来趋势&#xff0c;为企业选型与开发者技术深耕提供专业参考。全文约5000字&#xff0c;涵盖多维度深度分…

作者头像 李华
网站建设 2026/4/30 7:12:29

媒体观点丨Databricks与袋鼠云,两个故事、一个方向

以下文章来源于数据猿&#xff0c;作者月满西楼。“中国的DataAI平台&#xff0c;不仅仅是复制Databricks那么简单。过去两年&#xff0c;关于AI的叙事有一个明显的转折点。一开始&#xff0c;所有人都在看参数量、模型榜单和Demo效果——谁的模型更大、更“聪明”&#xff0c;…

作者头像 李华
网站建设 2026/4/23 15:35:28

国产数据库技术:DM数据库学习实践心得

目录引言&#xff1a;国产数据库的发展背景与学习意义DM数据库基础认知DM数据库安装与环境配置实践DM数据库实例管理与核心配置DM数据库备份与还原机制及实操DM数据库函数体系与应用场景SQL语句查询与优化实践DM SQL程序设计思路与实现步骤DM数据库实操问题解决案例分析学习总结…

作者头像 李华
网站建设 2026/4/18 6:32:50

Containerd指南:从Docker到K8s的容器运行时

引言 随着云原生技术的快速发展&#xff0c;容器运行时技术栈正在经历深刻变革。从Docker一家独大到Kubernetes生态下的多元化选择&#xff0c;Containerd作为新一代容器运行时标准&#xff0c;正在成为企业级容器平台的核心基石。本文将带你深入了解Containerd的技术演进、架…

作者头像 李华