玩转Spring Boot——简单登录认证全指南
登录认证是Web应用的安全基石。无论你开发的是传统MVC应用还是前后端分离的API服务,身份验证(Who are you?)和授权(What can you do?)都是绕不开的核心功能。Spring Boot作为Java生态中最流行的快速开发框架,通过整合Spring Security为我们提供了开箱即用的登录认证解决方案,同时支持高度定制化。
本文将从基础表单登录入手,逐步讲解自定义登录页面、密码加密、记住我功能,最终延伸到前后端分离场景下的JWT认证。我们会结合最佳实践和真实代码示例,帮助你彻底掌握Spring Boot中的登录认证实现。
目录#
- 引言
- 前置知识准备
- 快速搭建基础登录认证(基于表单)
- 3.1 添加依赖
- 3.2 配置Spring Security
- 3.3 实现用户详情服务(UserDetailsService)
- 3.4 测试基础登录功能
- 进阶:自定义登录页面与错误处理
- 4.1 自定义登录页面
- 4.2 处理登录错误
- 4.3 配置登录成功后的跳转逻辑
- 密码安全:加密与存储
- 5.1 为什么需要密码加密?
- 5.2 Spring Security支持的加密算法
- 5.3 实战:使用BCrypt加密密码
- 记住我(Remember Me)功能实现
- 6.1 原理简介
- 6.2 配置记住我功能
- 前后端分离场景下的登录认证(JWT)
- 7.1 为什么选择JWT?
- 7.2 JWT的结构与原理
- 7.3 实战:整合JWT实现无状态登录
- 常见问题与最佳实践
- 8.1 避免常见安全漏洞
- 8.2 性能优化建议
- 8.3 日志与监控
- 总结
- 参考资料
前置知识准备#
在开始前,请确保你具备以下基础:
- Spring Boot基础:了解如何创建Spring Boot项目、使用Starter依赖。
- Maven/Gradle:能配置项目依赖。
- RESTful API概念:理解HTTP方法、请求/响应结构。
- Spring Security基础(可选):知道
UserDetails、PasswordEncoder等核心接口。
工具准备:
- IDE:IntelliJ IDEA或Eclipse
- 接口测试工具:Postman
- 数据库(可选):MySQL或H2(用于持久化用户数据)
快速搭建基础登录认证(基于表单)#
Spring Security的表单登录是最经典的认证方式,适用于传统MVC应用。我们将从最简化的配置开始,快速实现一个可运行的登录功能。
3.1 添加依赖#
首先,在pom.xml(Maven)或build.gradle(Gradle)中添加Spring Security Starter:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>// Gradle
implementation 'org.springframework.boot:spring-boot-starter-security'3.2 配置Spring Security#
Spring Security的核心是安全过滤链(Security Filter Chain),它负责拦截请求并处理认证逻辑。自Spring Boot 2.7起,WebSecurityConfigurerAdapter被废弃,推荐使用组件式配置(通过@Bean定义过滤链)。
3.2.1 基础配置(Spring Boot ≥ 2.7)#
创建SecurityConfig类,配置安全过滤链和用户信息:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig {
// 1. 密码编码器:推荐BCrypt
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 2. 用户详情服务:这里使用内存用户(快速测试用)
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
// 创建一个测试用户:用户名user,密码password,角色USER
var user = User.builder()
.username("user")
.password(passwordEncoder.encode("password")) // 加密密码
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user); // 内存存储用户
}
// 3. 安全过滤链:定义请求授权规则
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权:所有请求需认证
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
// 启用表单登录(默认生成登录页)
.formLogin(formLogin -> formLogin
.defaultSuccessUrl("/home", true) // 登录成功跳转/home
)
// 启用HTTP Basic认证(可选,用于API测试)
.httpBasic();
return http.build();
}
}3.2.2 旧版配置(Spring Boot < 2.7)#
如果你的项目仍在使用旧版本(如2.6.x),可以通过WebSecurityConfigurerAdapter配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("password"))
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/home", true)
.and()
.httpBasic();
}
}3.3 实现用户详情服务(UserDetailsService)#
上面的示例使用内存用户(InMemoryUserDetailsManager),仅适用于测试。真实项目中,我们需要从数据库加载用户,这就需要自定义UserDetailsService。
3.3.1 数据库准备(可选)#
假设我们使用MySQL存储用户,先创建users表:
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL
);3.3.2 实体类与Repository#
创建User实体类:
import jakarta.persistence.*; // Spring Boot 3+用jakarta,2.x用javax
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role; // 如"USER"、"ADMIN"
// Getter、Setter省略
}创建JPA Repository:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// 根据用户名查询用户(登录核心方法)
Optional<User> findByUsername(String username);
}3.3.3 自定义UserDetailsService#
实现UserDetailsService接口,从数据库加载用户:
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.core.authority.SimpleGrantedAuthority;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户
var user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));
// 2. 转换为Spring Security的UserDetails(包含权限信息)
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
// 角色转换:ROLE_前缀是Spring Security的默认要求
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))
);
}
}3.4 测试基础登录功能#
- 启动Spring Boot应用。
- 访问
http://localhost:8080,会自动跳转至Spring Security默认登录页(/login)。 - 输入用户名
user、密码password,点击登录。 - 登录成功后,会跳转至
/home(需自己实现/home接口,例如一个简单的Controller):
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home"; // 需创建src/main/resources/templates/home.html(Thymeleaf模板)
}
}进阶:自定义登录页面与错误处理#
默认登录页虽然能用,但样式和体验较差。我们可以自定义登录页面,并处理登录错误(如密码错误、账号锁定)。
4.1 自定义登录页面#
4.1.1 添加Thymeleaf依赖#
自定义登录页通常使用模板引擎(如Thymeleaf),需添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>4.1.2 创建登录页模板#
在src/main/resources/templates下创建custom-login.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>自定义登录页</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>用户登录</h1>
<!-- 登录错误提示:当URL带?error参数时显示 -->
<div class="error" th:if="${param.error}">
用户名或密码错误!
</div>
<!-- 登出成功提示:当URL带?logout参数时显示 -->
<div th:if="${param.logout}">
已成功登出!
</div>
<!-- 登录表单:提交至Spring Security的/login端点 -->
<form th:action="@{/login}" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required/>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required/>
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>4.1.3 配置自定义登录页#
修改SecurityConfig,指定登录页URL并允许匿名访问:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/custom-login").permitAll() // 允许匿名访问登录页
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage("/custom-login") // 自定义登录页URL
.defaultSuccessUrl("/home", true) // 登录成功跳转
.failureUrl("/custom-login?error") // 登录失败跳转(带error参数)
)
.logout(logout -> logout
.logoutSuccessUrl("/custom-login?logout") // 登出成功跳转
);
return http.build();
}4.1.4 添加登录页Controller#
创建LoginController,处理登录页的GET请求:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
// 访问/custom-login时返回自定义登录页
@GetMapping("/custom-login")
public String login() {
return "custom-login";
}
}4.2 处理登录错误#
登录错误包括:
BadCredentialsException:密码错误DisabledException:账号禁用LockedException:账号锁定UsernameNotFoundException:用户不存在
Spring Security会将错误信息通过error参数传递给登录页。我们可以在模板中根据错误类型显示更友好的提示:
<!-- 改进后的错误提示 -->
<div class="error" th:if="${param.error}">
<th:block th:switch="${SPRING_SECURITY_LAST_EXCEPTION.class.simpleName}">
<p th:case="'BadCredentialsException'">用户名或密码错误!</p>
<p th:case="'DisabledException'">账号已禁用!</p>
<p th:case="'LockedException'">账号已锁定!</p>
<p th:case="*">登录失败,请联系管理员!</p>
</th:block>
</div>4.3 配置登录成功后的跳转逻辑#
除了defaultSuccessUrl,还可以通过successHandler实现更灵活的跳转(如根据用户角色跳转不同页面):
.formLogin(formLogin -> formLogin
.loginPage("/custom-login")
.successHandler((request, response, authentication) -> {
// authentication包含当前登录用户的信息
var user = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
String targetUrl;
// 根据角色跳转:ADMIN到/admin,USER到/home
if (user.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
targetUrl = "/admin";
} else {
targetUrl = "/home";
}
response.sendRedirect(targetUrl);
})
)密码安全:加密与存储#
永远不要存储明文密码! 这是Web安全的基本准则。Spring Security提供了PasswordEncoder接口,支持多种加密算法。
5.1 为什么需要密码加密?#
- 防泄露:即使数据库被黑客窃取,加密后的密码也无法直接使用。
- 合规要求:GDPR、等保2.0等法规要求必须加密存储敏感数据。
5.2 Spring Security支持的加密算法#
| 算法 | 描述 | 推荐度 |
|---|---|---|
| BCrypt | 自适应哈希函数,通过cost因子调整计算复杂度(默认10) | ✅ 推荐 |
| PBKDF2 | 基于密码的密钥派生函数,需指定迭代次数和盐值 | ✅ 推荐 |
| SCrypt | 内存硬哈希函数,适合对抗GPU/ASIC破解 | ✅ 推荐 |
| NoOpPasswordEncoder | 不加密(仅测试用,已废弃) | ❌ 禁止 |
5.3 实战:使用BCrypt加密密码#
5.3.1 加密密码#
在用户注册时,使用PasswordEncoder加密密码:
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
// 注册用户:密码加密后存储
public User register(String username, String password, String role) {
var user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); // 加密密码
user.setRole(role);
return userRepository.save(user);
}
}5.3.2 验证密码#
Spring Security会自动使用PasswordEncoder验证密码(无需手动调用)。例如,登录时:
- 用户输入密码
password。 - Spring Security调用
UserDetailsService加载用户(获取加密后的密码)。 - 使用
PasswordEncoder.matches(明文密码, 加密密码)验证正确性。
记住我(Remember Me)功能实现#
“记住我”功能允许用户在关闭浏览器后,下次访问时无需重新登录。Spring Security通过持久化token实现该功能。
6.1 原理简介#
- 用户登录时勾选“记住我”,Spring Security生成一个持久化token(包含系列号和token值)。
- Token存储在两个地方:
- 浏览器的
remember-mecookie(有效期可配置)。 - 服务器端数据库(或内存)。
- 浏览器的
- 下次访问时,浏览器携带
remember-mecookie,服务器验证token有效性,自动登录。
6.2 配置记住我功能#
6.2.1 内存存储(快速测试)#
修改SecurityConfig,启用记住我:
.http
.rememberMe(rememberMe -> rememberMe
.key("mySecretKey123!") // 签名密钥(需唯一、保密)
.tokenValiditySeconds(86400) // 有效期:1天(86400秒)
)6.2.2 数据库存储(生产推荐)#
生产环境中,推荐将token存储在数据库(避免内存丢失):
- 添加数据库依赖(如MySQL):
<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>- 配置数据源(
application.properties):
spring.datasource.url=jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.hibernate.ddl-auto=update # 自动建表(仅测试用)- 添加
PersistentTokenRepositoryBean:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.context.annotation.Bean;
@Configuration
public class RememberMeConfig {
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
var tokenRepo = new JdbcTokenRepositoryImpl();
tokenRepo.setJdbcTemplate(jdbcTemplate);
// 自动创建persistent_logins表(首次运行时启用,之后注释)
// tokenRepo.setCreateTableOnStartup(true);
return tokenRepo;
}
}- 修改
SecurityConfig,使用数据库存储:
.http
.rememberMe(rememberMe -> rememberMe
.key("mySecretKey123!")
.tokenValiditySeconds(86400)
.tokenRepository(persistentTokenRepository()) // 数据库存储
)6.2.3 添加“记住我” checkbox#
在登录页添加checkbox:
<div>
<label><input type="checkbox" name="remember-me"/> 记住我</label>
</div>前后端分离场景下的登录认证(JWT)#
传统表单登录依赖Session,不适合前后端分离的SPA(单页应用)或移动端应用。**JWT(JSON Web Token)**是一种无状态认证方案,更适合现代架构。
7.1 为什么选择JWT?#
- 无状态:服务器不需要存储Session,减轻集群压力。
- 跨域支持:JWT通过HTTP Header传递,支持跨域请求。
- 自包含:Token包含用户信息和权限,减少数据库查询。
7.2 JWT的结构与原理#
JWT由三部分组成,用.分隔:
- Header(头部):包含算法(如HS256)和类型(JWT)。
- Payload(负载):包含用户信息(如
sub=用户名、exp=过期时间)。 - Signature(签名):用密钥对Header和Payload进行签名,确保不被篡改。
示例JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzE3NjU4MDAwLCJleHAiOjE3MTc3NDQ0MDB9.
5Z7Z3e9X6Y8W2Q4R1T0U7S3V2B1N5M8K9L0P
7.3 实战:整合JWT实现无状态登录#
我们将实现以下流程:
- 客户端发送POST请求
/api/auth/login,携带用户名和密码。 - 服务器验证身份,生成JWT返回给客户端。
- 客户端后续请求携带JWT(放在
AuthorizationHeader)。 - 服务器验证JWT有效性,允许访问资源。
7.3.1 添加JWT依赖#
使用jjwt库处理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>7.3.2 实现JWT工具类#
创建JwtTokenProvider,负责生成、验证JWT:
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
// JWT密钥(从配置文件读取,避免硬编码)
@Value("${jwt.secret}")
private String jwtSecret;
// JWT有效期(毫秒):1天
@Value("${jwt.expiration}")
private long jwtExpirationMs;
// 生成JWT
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setSubject(userDetails.getUsername()) // 用户名(sub字段)
.setIssuedAt(now) // 签发时间
.setExpiration(expiryDate) // 过期时间
.signWith(SignatureAlgorithm.HS512, jwtSecret) // 签名算法和密钥
.compact();
}
// 从JWT中获取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 验证JWT有效性
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
System.err.println("JWT签名无效:" + e.getMessage());
} catch (MalformedJwtException e) {
System.err.println("JWT格式错误:" + e.getMessage());
} catch (ExpiredJwtException e) {
System.err.println("JWT已过期:" + e.getMessage());
} catch (UnsupportedJwtException e) {
System.err.println("JWT不支持:" + e.getMessage());
} catch (IllegalArgumentException e) {
System.err.println("JWT参数为空:" + e.getMessage());
}
return false;
}
}7.3.3 配置application.properties#
添加JWT配置:
# JWT配置
jwt.secret=mySuperSecretKey123!@# // 密钥(需长且随机,推荐32位以上)
jwt.expiration=86400000 // 有效期1天(毫秒)7.3.4 实现登录接口#
创建AuthController,处理登录请求:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
// 登录接口:POST /api/auth/login
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// 1. 验证用户名和密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 2. 将认证信息存入SecurityContext(可选,无状态场景可省略)
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成JWT
var userDetails = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
String jwt = tokenProvider.generateToken(userDetails);
// 4. 返回JWT
return ResponseEntity.ok(new JwtResponse(jwt));
}
}
// 登录请求DTO(数据传输对象)
class LoginRequest {
private String username;
private String password;
// Getter、Setter省略
}
// JWT响应DTO
class JwtResponse {
private String token;
public JwtResponse(String token) {
this.token = token;
}
// Getter省略
}7.3.5 配置JWT安全过滤链#
创建JwtSecurityConfig,配置无状态认证:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
// 密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 认证管理器(用于登录时验证用户名密码)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
// JWT认证过滤器:拦截请求并验证JWT
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(tokenProvider, userDetailsService);
}
// 安全过滤链:无状态配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 禁用CSRF(无状态场景不需要)
.csrf(csrf -> csrf.disable())
// 2. 禁用Session(无状态)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 3. 配置请求授权
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**").permitAll() // 登录接口允许匿名访问
.anyRequest().authenticated() // 其他请求需认证
)
// 4. 添加JWT过滤器(在用户名密码过滤器之前执行)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}7.3.6 实现JWT认证过滤器#
创建JwtAuthenticationFilter,拦截请求并验证JWT:
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, CustomUserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求头获取JWT
String jwt = getJwtFromRequest(request);
// 2. 验证JWT有效性
if (jwt != null && tokenProvider.validateToken(jwt)) {
// 3. 从JWT获取用户名
String username = tokenProvider.getUsernameFromToken(jwt);
// 4. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 创建认证对象并存入SecurityContext
var authentication = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("JWT认证失败:" + ex.getMessage());
}
// 继续执行后续过滤器
filterChain.doFilter(request, response);
}
// 从Authorization头中获取JWT(格式:Bearer <token>)
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // 去掉Bearer前缀
}
return null;
}
}7.3.7 测试JWT认证#
-
登录:使用Postman发送POST请求
http://localhost:8080/api/auth/login,请求体:{ "username": "user", "password": "password" }响应会返回JWT:
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } -
访问受保护接口:发送GET请求
http://localhost:8080/api/user/profile,在请求头中添加:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...若JWT有效,会返回用户信息;否则返回401 Unauthorized。
常见问题与最佳实践#
8.1 避免常见安全漏洞#
- CSRF攻击:传统MVC应用需启用CSRF保护(
http.csrf().enable()),前后端分离应用需禁用。 - 弱密码:强制用户使用复杂密码(长度≥8,包含大小写、数字、符号)。
- JWT泄露:使用HTTPS传输JWT,避免在URL中传递JWT。
- Token过期:设置合理的过期时间(如1天),并提供刷新Token机制。
8.2 性能优化建议#
- 缓存用户信息:使用Redis缓存
UserDetails,减少数据库查询。 - JWT压缩:使用
jjwt的压缩功能(如GZIP),减少Token大小。 - 异步认证:对于高并发场景,使用异步方法加载用户信息(
@Async)。
8.3 日志与监控#
- 登录日志:记录登录成功/失败事件(用户名、IP、时间),用于审计。
- 异常监控:使用ELK Stack(Elasticsearch、Logstash、Kibana)或Prometheus+Grafana监控认证异常。
- Token监控:统计Token生成/验证次数,及时发现异常请求。
总结#
通过本文,你已经掌握了Spring Boot中登录认证的完整流程:
- 基础表单登录:快速搭建可运行的登录功能。
- 自定义登录页:提升用户体验。
- 密码加密:保障数据安全。
- 记住我功能:增强用户体验。
- JWT认证:适配前后端分离场景。
Spring Security的强大之处在于灵活性——你可以根据需求选择不同的认证方式,从简单的表单登录到复杂的OAuth2.0/OpenID Connect。
最后,记住安全是持续的过程:定期更新依赖、审计配置、学习最新安全知识,才能让你的应用更安全。
参考资料#
- Spring Security官方文档:https://docs.spring.io/spring-security/reference/index.html
- Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
- JWT官方文档:https://jwt.io/introduction
- BCrypt算法:https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/provos/provos.pdf
- OWASP Top Ten:https://owasp.org/Top10/
- JJWT库文档:https://github.com/jwtk/jjwt
希望本文对你有所帮助!如果有问题,欢迎在评论区交流。