news 2026/2/9 4:54:34

小明的Spring Security入门到深入实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
小明的Spring Security入门到深入实战

** 第 一 步 : 搭 建 Spring Boot 项 目 , 引 入 Spring Security **

** 小 明 的 需 求 **

“ 先 让 网 站 有 个 最 基 础 的 登 录 功 能 , 能 拦 住 未 登 录 的 用 户 。 ”

** 实 操 步 骤 **

** 创 建 Spring Boot 项 目 ** : 用 https://start.spring.io/ , 选 Spring Web ( 搭 Web 服 务 ) 、 Spring Security ( 安 全 框 架 ) 、 Spring Data JPA ( 操 作 数 据 库 ) 、 MySQL Driver ( 数 据 库 驱 动 ) 。

** 添 加 依 赖 ** ( pom.xml 核 心 部 分 ) :

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId> <!-- Spring Security 依 赖 -->

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-jpa</artifactId>

</dependency>

<dependency>

<groupId>com.mysql</groupId>

<artifactId>mysql-connector-j</artifactId>

</dependency>

</dependencies>

** 运 行 项 目 ** : 启 动 Spring Boot , 访 问 http://localhost:8080 , 会 自 动 跳 转 到 Spring Security 的 默 认 登 录 页 。

** 现 象 与 原 理 **

** 默 认 用 户 ** : 控 制 台 会 打 印 一 行 密 码 , 格 式 如 Using generated security password: abcdef12-3456-7890-abcd-ef1234567890 , 用 户 名 固 定 为 user 。

** 原 理 ** : Spring Security 自 动 配 置 了 默 认 的 InMemoryUserDetailsManager ( 内 存 用 户 存 储 ) , 生 成 随 机 密 码 。 所 有 接 口 默 认 需 要 USER 角 色 才 能 访 问 。

💡 小 明 的 疑 问 : “ 这 密 码 每 次 启 动 都 变 , 而 且 用 户 不 能 自 己 注 册 , 得 改 ! ”

** 第 二 步 : 自 定 义 用 户 存 储 —— 从 内 存 到 数 据 库 **

** 小 明 的 需 求 **

“ 用 户 信 息 存 在 自 己 的 数 据 库 里 , 支 持 注 册 、 登 录 , 密 码 用 BCrypt 加 密 。 ”

** 核 心 概 念 **

** UserDetailsService ** : Spring Security 的 “ 用 户 查 询 接 口 ” , 开 发 者 实 现 它 , 告 诉 框 架 “ 怎 么 从 数 据 库 查 用 户 ” 。

** UserDetails ** : “ 用 户 信 息 封 装 类 ” , 包 含 用 户 名 、 密 码 、 角 色 、 账 户 是 否 过 期 等 信 息 。

** PasswordEncoder ** : “ 密 码 加 密 器 ” , 用 BCrypt 算 法 把 明 文 密 码 变 成 哈 希 值 存 储 。

** 实 操 步 骤 **

** 建 用 户 表 ** ( MySQL ) :

CREATE TABLE users (

id BIGINT AUTO_INCREMENT PRIMARY KEY,

username VARCHAR(50) UNIQUE NOT NULL, -- 用 户 名

password VARCHAR(100) NOT NULL, -- BCrypt 哈 希 后 的 密 码

role VARCHAR(20) NOT NULL -- 角 色 : ROLE_VISITOR / ROLE_EDITOR / ROLE_ADMIN

);

** 实 现 UserDetailsService ** ( 查 数 据 库 用 户 ) :

@Service

public class CustomUserDetailsService implements UserDetailsService {

@Autowired private UserRepository userRepo; // JPA 操 作 users 表

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 1. 查 数 据 库 用 户

User user = userRepo.findByUsername(username)

.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));

// 2. 封 装 成 UserDetails 对 象 ( Spring Security 认 识 的 格 式 )

return org.springframework.security.core.userdetails.User.builder()

.username(user.getUsername())

.password(user.getPassword()) // 存 的 是 BCrypt 哈 希 值

.roles(user.getRole().replace("ROLE_", "")) // 角 色 去 掉 ROLE_ 前 缀

.build();

}

}

** 配 置 PasswordEncoder ** ( 用 BCrypt 加 密 ) :

@Configuration

public class SecurityConfig {

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder(); // BCrypt 加 密 器 , 自 带 随 机 盐

}

}

** 注 册 用 户 ** ( 比 如 小 明 自 己 注 册 为 管 理 员 ) :

@RestController

public class RegisterController {

@Autowired private UserRepository userRepo;

@Autowired private PasswordEncoder passwordEncoder;

@PostMapping("/register")

public String register(String username, String rawPassword, String role) {

User user = new User();

user.setUsername(username);

user.setPassword(passwordEncoder.encode(rawPassword)); // 明 文 密 码 → BCrypt 哈 希

user.setRole("ROLE_" + role.toUpperCase()); // 角 色 加 ROLE_ 前 缀 ( Spring Security 约 定 )

userRepo.save(user);

return "注册成功!";

}

}

** 效 果 **

小 明 用 POST /register?username=xiaoming&rawPassword=123456&role=admin 注 册 后 , 数 据 库 users 表 会 存 一 条 记 录 , password 字 段 是 BCrypt 哈 希 值 ( 如 $2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx )。 登 录 时 , 框 架 会 用 BCrypt 验 证 输 入 密 码 是 否 匹 配 。

💡 小 明 的 体 会 : “ UserDetailsService 就 像 图 书 馆 的 ‘ 查 阅 员 ’ , 框 架 告 诉 他 ‘ 找 小 明 ’ , 他 就 去 数 据 库 把 小 明 的 书 ( 用 户 信 息 ) 拿 回 来 。 ”

** 第 三 步 : 配 置 权 限 控 制 —— 谁 能 干 啥 ? **

** 小 明 的 需 求 **

“ 游 客 只 能 看 照 片 , 编 辑 能 上 传 , 管 理 员 能 删 除 。 ”

** 核 心 概 念 **

** HttpSecurity ** : Spring Security 的 “ Web 安 全 配 置 类 ” , 用 来 设 置 URL 访 问 权 限 、 登 录 / 注 销 页 、 CSRF 等 。

** @PreAuthorize ** : “ 方 法 级 权 限 注 解 ” , 直 接 在 Controller 方 法 上 标 注 需 要 的 角 色 。

** 实 操 步 骤 **

** 配 置 URL 访 问 权 限 ** ( 在 SecurityConfig 中 ) :

@Configuration

@EnableWebSecurity // 开 启 Web 安 全 配 置

public class SecurityConfig {

@Autowired private CustomUserDetailsService userDetailsService;

@Autowired private PasswordEncoder passwordEncoder;

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http

.authorizeHttpRequests(auth -> auth

.requestMatchers("/photos/**").permitAll() // 公 开 : 所 有 人 能 看 照 片

.requestMatchers("/upload/**").hasRole("EDITOR") // 上 传 : 需 EDITOR 角 色

.requestMatchers("/delete/**").hasRole("ADMIN") // 删 除 : 需 ADMIN 角 色

.anyRequest().authenticated() // 其 他 接 口 需 登 录

)

.formLogin(form -> form // 配 置 登 录 页

.loginPage("/my-login") // 自 定 义 登 录 页 ( 下 节 讲 )

.defaultSuccessUrl("/home") // 登 录 成 功 跳 转

)

.logout(logout -> logout // 配 置 注 销

.logoutUrl("/logout")

.logoutSuccessUrl("/photos")

);

return http.build();

}

// 配 置 认 证 管 理 器 ( 用 自 定 义 UserDetailsService 和 PasswordEncoder )

@Bean

public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {

return config.getAuthenticationManager();

}

}

** 用 @PreAuthorize 做 方 法 级 权 限 ** ( 更 精 细 控 制 ) :

@RestController

public class PhotoController {

// 游 客 能 看

@GetMapping("/photos")

public List<Photo> listPhotos() { ... }

// 编 辑 能 上 传 ( @PreAuthorize 需 开 启 @EnableMethodSecurity )

@PostMapping("/upload")

@PreAuthorize("hasRole('EDITOR')") // 直 接 标 注 需 EDITOR 角 色

public String uploadPhoto(MultipartFile file) { ... }

// 管 理 员 能 删 除

@DeleteMapping("/delete/{id}")

@PreAuthorize("hasRole('ADMIN') or hasRole('EDITOR')") // 管 理 员 或 编 辑 都 能 删

public String deletePhoto(@PathVariable Long id) { ... }

}

** 开 启 @PreAuthorize 支 持 ** : 在 SecurityConfig 上 加 @EnableMethodSecurity 。

** 效 果 **

访 问 /upload 时 , 若 用 户 是 ROLE_VISITOR , 会 被 拦 截 并 返 回 403 错 误 ( 权 限 不 足 ) ;

用 @PreAuthorize 的 方 法 , 框 架 会 在 执 行 前 检 查 用 户 角 色 , 不 符 合 则 抛 异 常 。

💡 小 明 的 体 会 : “ HttpSecurity 像 ‘ 门 卫 ’ , 管 URL 级 的 大 门 ; @PreAuthorize 像 ‘ 房 间 锁 ’ , 管 方 法 级 的 小 门 , 双 保 险 ! ”

** 第 四 步 : 自 定 义 登 录 页 、 注 销 与 记 住 我 **

** 小 明 的 需 求 **

“ 默 认 登 录 页 太 丑 , 想 用 自 己 的 ; 加 个 ‘ 记 住 我 ’ 功 能 , 关 闭 浏 览 器 再 打 开 还 是 登 录 状 态 。 ”

** 实 操 步 骤 **

** 自 定 义 登 录 页 ** :

写 一 个 HTML 页 面 my-login.html ( 放 resources/templates 目 录 ) , 包 含 username 、 password 输 入 框 和 remember-me 复 选 框 ;

在 SecurityConfig 中 配 置 .loginPage("/my-login") , 并 指 定 处 理 登 录 的 URL :

.formLogin(form -> form

.loginPage("/my-login") // 自 定 义 登 录 页 路 径

.loginProcessingUrl("/do-login") // 处 理 登 录 的 URL ( 框 架 自 动 处 理 )

.defaultSuccessUrl("/home")

.failureUrl("/my-login?error") // 登 录 失 败 跳 转

)

** 开 启 “ 记 住 我 ” 功 能 ** :

在 SecurityConfig 中 配 置 .rememberMe() :

.rememberMe(remember -> remember

.key("xiaoming-secret-key") // 自 定 义 密 钥 ( 防 篡 改 )

.tokenValiditySeconds(7 * 24 * 3600) // 记 住 我 有 效 期 : 7 天

)

在 自 定 义 登 录 页 加 “ 记 住 我 ” 复 选 框 :

<input type="checkbox" name="remember-me"> 记住我

** 自 定 义 注 销 页 ** :

在 SecurityConfig 中 配 置 .logout() , 默 认 注 销 URL 是 /logout , 可 自 定 义 :

.logout(logout -> logout

.logoutUrl("/my-logout") // 自 定 义 注 销 URL

.logoutSuccessUrl("/photos") // 注 销 成 功 跳 转

.invalidateHttpSession(true) // 注 销 时 清 空 Session

.deleteCookies("JSESSIONID") // 删 除 Cookie

)

** 效 果 **

访 问 需 登 录 的 页 面 时 , 跳 转 到 小 明 自 己 写 的 my-login.html ;

勾 选 “ 记 住 我 ” 后 , 关 闭 浏 览 器 , 7 天 内 再 访 问 网 站 仍 是 登 录 状 态 ;

点 击 注 销 链 接 , 清 空 Session 和 Cookie , 跳 回 公 开 照 片 页 。

💡 小 明 的 体 会 : “ 自 定 义 登 录 页 让 网 站 更 好 看 , ‘ 记 住 我 ’ 像 ‘ 长 期 饭 卡 ’ , 不 用 每 次 都 刷 临 时 卡 。 ”

** 第 五 步 : 集 成 JWT —— 支 持 APP 与 微 服 务 **

** 小 明 的 需 求 **

“ 我 做 了 个 手 机 APP , 用 Session - Cookie 不 方 便 , 想 用 JWT Token 做 认 证 。 ”

** 核 心 概 念 **

** JWT ** : “ 自 带 信 息 的 身 份 证 ” , 服 务 器 不 存 Session , 用 户 登 录 后 拿 Token , 后 续 请 求 带 Token 即 可 。

** JwtAuthenticationFilter ** : 自 定 义 过 滤 器 , 从 Authorization 头 取 JWT , 验 证 后 设 置 Security Context 。

** 实 操 步 骤 **

** 添 加 JWT 依 赖 ** :

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-api</artifactId>

<version>0.11.5</version>

</dependency>

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-impl</artifactId>

<version>0.11.5</version>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-jackson</artifactId>

<version>0.11.5</version>

<scope>runtime</scope>

</dependency>

** 写 JWT 工 具 类 ** ( 生 成 / 验 证 Token ) :

@Component

public class JwtUtil {

@Value("${jwt.secret}") private String secret; // 配 置 在 application.properties 中

@Value("${jwt.expiration}") private long expiration; // Token 有 效 期 ( 如 3600000 毫 秒 )

// 生 成 Token

public String generateToken(UserDetails userDetails) {

Map<String, Object> claims = new HashMap<>();

claims.put("roles", userDetails.getAuthorities().stream()

.map(GrantedAuthority::getAuthority).collect(Collectors.toList()));

return Jwts.builder()

.setClaims(claims)

.setSubject(userDetails.getUsername())

.setIssuedAt(new Date())

.setExpiration(new Date(System.currentTimeMillis() + expiration))

.signWith(SignatureAlgorithm.HS512, secret) // 用 HS512 算 法 签 名

.compact();

}

// 验 证 Token 并 取 用 户 名

public String getUsernameFromToken(String token) {

return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();

}

}

** 写 JWT 认 证 过 滤 器 ** :

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired private JwtUtil jwtUtil;

@Autowired private CustomUserDetailsService userDetailsService;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

// 1. 从 Header 取 Token

String token = request.getHeader("Authorization");

if (token != null && token.startsWith("Bearer ")) {

token = token.substring(7);

// 2. 验 证 Token , 取 用 户 名

String username = jwtUtil.getUsernameFromToken(token);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

// 3. 查 用 户 信 息 , 设 置 Security Context

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(

userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(auth);

}

}

chain.doFilter(request, response); // 继 续 处 理 请 求

}

}

** 修 改 SecurityConfig , 用 JWT 替 换 Session ** :

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http

.csrf(csrf -> csrf.disable()) // JWT 无 状 态 , 关 闭 CSRF

.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无 Session

.authorizeHttpRequests(auth -> auth

.requestMatchers("/api/photos/**").permitAll()

.requestMatchers("/api/upload/**").hasRole("EDITOR")

.anyRequest().authenticated()

)

.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 加 JWT 过 滤 器

.formLogin(form -> form.disable()) // 禁 用 默 认 表 单 登 录 ( APP 用 JWT )

.logout(logout -> logout.disable()); // 禁 用 默 认 注 销

return http.build();

}

** 效 果 **

APP 用 户 登 录 时 , 调 POST /api/login ( 自 定 义 接 口 , 验 证 用 户 名 密 码 后 返 JWT ) ;

后 续 APP 请 求 API 时 , 在 Authorization 头 带 Bearer <JWT> , 框 架 自 动 认 证 。

💡 小 明 的 体 会 : “ JWT 像 ‘ 电 子 通 行 证 ’ , APP 拿 着 它 就 能 畅 通 无 阻 , 服 务 器 不 用 记 住 谁 来 过 , 轻 松 支 持 多 端 ! ”

** 第 六 步 : 深 入 : OAuth2 第 三 方 登 录 与 安 全 防 护 **

** 小 明 的 需 求 **

“ 想 让 用 户 用 GitHub 账 号 直 接 登 录 , 还 要 防 CSRF 、 XSS 攻 击 。 ”

** 核 心 概 念 **

** OAuth2 ** : “ 第 三 方 登 录 协 议 ” , 用 户 授 权 GitHub 告 诉 小 明 的 网 站 “ 这 是 我 ” , 无 需 注 册 。

** Spring Security OAuth2 Client ** : 框 架 内 置 OAuth2 客 户 端 , 支 持 GitHub 、 Google 等 登 录 。

** 实 操 步 骤 **

** 配 置 GitHub OAuth2 登 录 ** :

在 GitHub 开 发 者 设 置 中 创 建 OAuth App , 获 取 client-id 和 client-secret ;

在 application.properties 中 配 置 :

spring.security.oauth2.client.registration.github.client-id=你的client-id

spring.security.oauth2.client.registration.github.client-secret=你的client-secret

spring.security.oauth2.client.registration.github.scope=user:email # 申 请 邮 箱 权 限

在 SecurityConfig 中 开 启 OAuth2 登 录 :

.oauth2Login(oauth2 -> oauth2

.loginPage("/my-login") // 自 定 义 登 录 页 加 “ GitHub 登 录 ” 按 钮

.defaultSuccessUrl("/home")

)

** 安 全 防 护 ** ( 框 架 默 认 开 启 , 无 需 额 外 配 置 ) :

** CSRF 防 护 ** : 默 认 开 启 , 框 架 自 动 生 成 CSRF 令 牌 , 表 单 提 交 时 验 证 ;

** XSS 防 护 ** : Spring Boot 默 认 开 启 HTML 转 义 , 防 注 入 脚 本 ;

** 会 话 固 定 攻 击 防 护 ** : 登 录 后 自 动 更 换 SessionID 。

** 效 果 **

小 明 的 登 录 页 多 了 个 “ 用 GitHub 登 录 ” 按 钮 , 点 击 后 跳 转 GitHub 授 权 , 授 权 成 功 后 自 动 登 录 网 站 ;

框 架 自 动 挡 住 CSRF 攻 击 , 即 使 黑 客 诱 骗 小 明 点 链 接 , 也 无 法 操 作 。

💡 小 明 的 体 会 : “ OAuth2 像 ‘ 找 朋 友 作 证 ’ , 让 GitHub 帮 忙 证 明 ‘ 这 是 小 明 ’ ; 框 架 的 安 全 防 护 像 ‘ 隐 形 铠 甲 ’ , 平 时 感 觉 不 到 , 关 键 时 刻 能 挡 刀 。 ”

** 小 明 的 学 习 总 结 **

通 过 这 几 步 , 小 明 彻 底 掌 握 了 Spring Security 配 Spring Boot 的 核 心 用 法 :

** 从 默 认 配 置 入 门 ** , 观 察 框 架 自 动 生 成 的 登 录 / 权 限 逻 辑 ;

** 自 定 义 UserDetailsService 和 PasswordEncoder ** , 连 接 自 己 的 数 据 库 , 用 BCrypt 加 密 密 码 ;

** 用 HttpSecurity 和 @PreAuthorize 配 置 权 限 ** , 实 现 URL 级 和 方 法 级 控 制 ;

** 自 定 义 登 录 / 注 销 页 、 加 “ 记 住 我 ” ** , 优 化 用 户 体 验 ;

** 集 成 JWT ** , 支 持 APP 和 微 服 务 无 状 态 认 证 ;

** 用 OAuth2 实 现 第 三 方 登 录 ** , 开 启 框 架 默 认 安 全 防 护 。

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

aTeX 学习笔记:学术文档排版

在实际应用中&#xff0c;如果我们仅仅需要完成的是《[[LaTeX学习笔记&#xff1a;文档排版基础]]》中所介绍的那些纯文本排版工作&#xff0c;其实并不一定需要用到 LATEX这样复杂的排版系统。毕竟&#xff0c;LATEX的核心优势主要在于其对数学公式、图表、参考文献等复杂文档…

作者头像 李华
网站建设 2026/2/9 0:27:34

零基础教程:5分钟用AI创建你的第一个抖音录播工具

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个最简单的抖音直播录制工具demo&#xff0c;要求&#xff1a;1.极简实现(不超过200行代码) 2.只需核心录制功能 3.提供最基础的命令行界面 4.包含最简单的错误提示 5.有清晰…

作者头像 李华
网站建设 2026/2/6 18:27:24

传统开发vsAI生成:Yande入口开发效率对比

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 构建一个功能完整的Yande搜索引擎入口页面&#xff0c;包含&#xff1a;1) 响应式搜索框 2) 热门标签云 3) 图片搜索结果网格展示 4) 分页功能 5) 图片详情弹窗。使用React前端框架…

作者头像 李华
网站建设 2026/2/6 16:44:21

1小时打造MissAV智能推荐系统原型

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 快速开发一个MissAV内容推荐系统原型&#xff0c;功能包括&#xff1a;1. 用户偏好收集 2. 内容特征提取 3. 相似度计算 4. 推荐结果展示 5. 反馈机制。使用Sentence Transformers处…

作者头像 李华
网站建设 2026/2/4 22:09:18

零基础教程:5分钟创建你的第一个Yande搜索入口

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 为初学者设计一个简单的Yande搜索引擎入口页面教程项目。包含基础HTML结构、CSS样式和极简JavaScript搜索功能。逐步指导如何添加搜索框、搜索结果展示区域和基本交互效果。提供详细…

作者头像 李华
网站建设 2026/2/7 5:08:01

AI如何自动获取国内NTP服务器IP地址

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个Python脚本&#xff0c;使用AI模型自动检测国内可用的NTP时间服务器IP地址。要求&#xff1a;1) 实现NTP协议的基本通信功能&#xff1b;2) 使用AI算法评估服务器响应时间和…

作者头像 李华