Spring Security 入门与实战指南(第二版)

注:完整笔记可在 https://github.com/WuSangui571/SpringSecurity 中的 README.md 文件浏览,此处发表的是经由 GPT 润色过的精简版。

第 4 章 验证码登录

在第 3 章自定义登录页的基础上,增加验证码校验,确保用户必须提供正确验证码才能登录。

4.1 前端页面调整


<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录title>head>
<body>
 <form th:action="@{/user/login}" method="post">
   <label>账号:<input type="text" name="username"/>label><br/>
   <label>密码:<input type="password" name="password"/>label><br/>
   <label>验证码:<input type="text" name="captcha"/>label>
   <img th:src="@{/common/captcha}" alt="验证码"/><br/>
   <input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
   <button type="submit">登录button>
 form>
body>
html>

4.2 后端验证码生成

引入 Hutool Captcha:

<dependency>
 <groupId>cn.hutoolgroupId>
 <artifactId>hutool-captchaartifactId>
 <version>5.8.38version>
dependency>

编写 CaptchaController

@RestController
public class CaptchaController {
 @GetMapping("/common/captcha")
 public void generateCaptcha(HttpServletRequest req,
                             HttpServletResponse res) throws IOException {
   res.setContentType("image/gif");
   ICaptcha captcha = CaptchaUtil.createGifCaptcha(90, 30, 4, 50);
   req.getSession().setAttribute("captcha", captcha.getCode());
   captcha.write(res.getOutputStream());
}
}

建议将验证码逻辑进一步抽象到 Service 层,Controller 仅负责路由。

4.3 验证过滤器实现

自定义 CaptchaFilter,在 UsernamePasswordAuthenticationFilter 之前进行校验:

@Component
public class CaptchaFilter extends OncePerRequestFilter {
 @Override
 protected void doFilterInternal(HttpServletRequest req,
                                 HttpServletResponse res,
                                 FilterChain chain)
     throws ServletException, IOException {
   if ("/user/login".equals(req.getRequestURI())) {
     String code = (String) req.getSession().getAttribute("captcha");
     String input = req.getParameter("captcha");
     if (!StringUtils.hasText(input) || !input.equalsIgnoreCase(code)) {
       res.sendRedirect("/toLogin");
       return;
    }
  }
   chain.doFilter(req, res);
}
}

4.4 安全链配置

SecurityFilterChain 中放行验证码接口,并注册过滤器:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 return http
  .formLogin(form -> form
    .loginPage("/toLogin")
    .loginProcessingUrl("/user/login")
    .defaultSuccessUrl("/", true)
  )
  .authorizeHttpRequests(auth -> auth
    .requestMatchers("/toLogin", "/common/captcha").permitAll()
    .anyRequest().authenticated()
  )
  .addFilterBefore(captchaFilter,
     UsernamePasswordAuthenticationFilter.class)
  .csrf().and()
  .build();
}

4.5 验证码登录流程

  1. 访问 /hello,未登录跳转 /toLogin

  2. 用户提交账号、密码、验证码。

  3. CaptchaFilter 校验验证码。通过则继续,失败重定向。

  4. UsernamePasswordAuthenticationFilter 验证用户名和密码。

  5. 登录成功,保存 AuthenticationSecurityContext

第 5 章 BCrypt 密码加密与匹配原理

5.1 密码安全存储

  • 数据库仅存密文,不可逆。避免泄漏后被直接利用。

  • Spring Security 默认使用 BCrypt,具有内置盐值机制。

5.2 使用 BCryptPasswordEncoder

@Bean
public PasswordEncoder passwordEncoder() {
 return new BCryptPasswordEncoder();
}

常用方法:

  • encode(raw):加密明文。

  • matches(raw, encoded):匹配明文与密文。

String hash1 = encoder.encode("123");
String hash2 = encoder.encode("123");
assert !hash1.equals(hash2);
assert encoder.matches("123", hash1);

5.3 算法结构

BCrypt 密文格式:$版本$强度$盐值+散列值

  • 版本:前缀,如 $2a$10$

  • 强度:工作因子。

  • 盐值:22 字符随机值。

  • 散列值:根据明文和盐值生成。

匹配时,提取密文中的盐值,重新加密明文并比对散列值。

第 6 章 获取当前登录用户信息

6.1 注入 Principal

@GetMapping("/userInfo")
@ResponseBody
public Principal userInfo(Principal principal) {
 return principal;
}

返回示例(简化):

{
 "name": "admin",
 "principal": { "username": "admin" }
}

仅包含框架默认字段,不足以展示业务字段。

6.2 自定义 UserDetails

让实体 TUser 实现 UserDetails

@Data
public class TUser implements UserDetails {
 private Integer id;
 private String loginAct;
 private String loginPwd;
 private String name;
 // … 其他字段 …

 @Override @JsonIgnore
 public Collection extends GrantedAuthority> getAuthorities() { return List.of(); }
 @Override @JsonIgnore public String getPassword() { return loginPwd; }
 @Override @JsonIgnore public String getUsername() { return loginAct; }
 // 其余 isXXX 方法同理
}

loadUserByUsername 返回 TUser,并对不想输出的属性或方法加 @JsonIgnore

6.3 注入 Authentication

@GetMapping("/userInfo2")
@ResponseBody
public Authentication userInfo2(Authentication auth) {
 return auth;
}

6.4 常用工具方法

public class LoginInfoUtil {
 public static TUser getCurrentUser() {
   Authentication auth = SecurityContextHolder
    .getContext().getAuthentication();
   return (TUser) auth.getPrincipal();
}
}

在 Controller 中直接调用,返回业务实体。

  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录

    • avatar

      你真牛逼