玩转Spring Boot——简单登录认证全指南

登录认证是Web应用的安全基石。无论你开发的是传统MVC应用还是前后端分离的API服务,身份验证(Who are you?)和授权(What can you do?)都是绕不开的核心功能。Spring Boot作为Java生态中最流行的快速开发框架,通过整合Spring Security为我们提供了开箱即用的登录认证解决方案,同时支持高度定制化。

本文将从基础表单登录入手,逐步讲解自定义登录页面、密码加密、记住我功能,最终延伸到前后端分离场景下的JWT认证。我们会结合最佳实践和真实代码示例,帮助你彻底掌握Spring Boot中的登录认证实现。

目录#

  1. 引言
  2. 前置知识准备
  3. 快速搭建基础登录认证(基于表单)
    • 3.1 添加依赖
    • 3.2 配置Spring Security
    • 3.3 实现用户详情服务(UserDetailsService)
    • 3.4 测试基础登录功能
  4. 进阶:自定义登录页面与错误处理
    • 4.1 自定义登录页面
    • 4.2 处理登录错误
    • 4.3 配置登录成功后的跳转逻辑
  5. 密码安全:加密与存储
    • 5.1 为什么需要密码加密?
    • 5.2 Spring Security支持的加密算法
    • 5.3 实战:使用BCrypt加密密码
  6. 记住我(Remember Me)功能实现
    • 6.1 原理简介
    • 6.2 配置记住我功能
  7. 前后端分离场景下的登录认证(JWT)
    • 7.1 为什么选择JWT?
    • 7.2 JWT的结构与原理
    • 7.3 实战:整合JWT实现无状态登录
  8. 常见问题与最佳实践
    • 8.1 避免常见安全漏洞
    • 8.2 性能优化建议
    • 8.3 日志与监控
  9. 总结
  10. 参考资料

前置知识准备#

在开始前,请确保你具备以下基础:

  1. Spring Boot基础:了解如何创建Spring Boot项目、使用Starter依赖。
  2. Maven/Gradle:能配置项目依赖。
  3. RESTful API概念:理解HTTP方法、请求/响应结构。
  4. Spring Security基础(可选):知道UserDetailsPasswordEncoder等核心接口。

工具准备:

  • 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 测试基础登录功能#

  1. 启动Spring Boot应用。
  2. 访问http://localhost:8080,会自动跳转至Spring Security默认登录页/login)。
  3. 输入用户名user、密码password,点击登录。
  4. 登录成功后,会跳转至/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验证密码(无需手动调用)。例如,登录时:

  1. 用户输入密码password
  2. Spring Security调用UserDetailsService加载用户(获取加密后的密码)。
  3. 使用PasswordEncoder.matches(明文密码, 加密密码)验证正确性。

记住我(Remember Me)功能实现#

“记住我”功能允许用户在关闭浏览器后,下次访问时无需重新登录。Spring Security通过持久化token实现该功能。

6.1 原理简介#

  1. 用户登录时勾选“记住我”,Spring Security生成一个持久化token(包含系列号和token值)。
  2. Token存储在两个地方:
    • 浏览器的remember-me cookie(有效期可配置)。
    • 服务器端数据库(或内存)。
  3. 下次访问时,浏览器携带remember-me cookie,服务器验证token有效性,自动登录。

6.2 配置记住我功能#

6.2.1 内存存储(快速测试)#

修改SecurityConfig,启用记住我:

.http
    .rememberMe(rememberMe -> rememberMe
            .key("mySecretKey123!") // 签名密钥(需唯一、保密)
            .tokenValiditySeconds(86400) // 有效期:1天(86400秒)
    )

6.2.2 数据库存储(生产推荐)#

生产环境中,推荐将token存储在数据库(避免内存丢失):

  1. 添加数据库依赖(如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>
  1. 配置数据源(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 # 自动建表(仅测试用)
  1. 添加PersistentTokenRepository Bean:
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;
    }
}
  1. 修改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由三部分组成,用.分隔:

  1. Header(头部):包含算法(如HS256)和类型(JWT)。
  2. Payload(负载):包含用户信息(如sub=用户名、exp=过期时间)。
  3. Signature(签名):用密钥对Header和Payload进行签名,确保不被篡改。

示例JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzE3NjU4MDAwLCJleHAiOjE3MTc3NDQ0MDB9.
5Z7Z3e9X6Y8W2Q4R1T0U7S3V2B1N5M8K9L0P

7.3 实战:整合JWT实现无状态登录#

我们将实现以下流程:

  1. 客户端发送POST请求/api/auth/login,携带用户名和密码。
  2. 服务器验证身份,生成JWT返回给客户端。
  3. 客户端后续请求携带JWT(放在Authorization Header)。
  4. 服务器验证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认证#

  1. 登录:使用Postman发送POST请求http://localhost:8080/api/auth/login,请求体:

    {
        "username": "user",
        "password": "password"
    }

    响应会返回JWT:

    {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  2. 访问受保护接口:发送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中登录认证的完整流程:

  1. 基础表单登录:快速搭建可运行的登录功能。
  2. 自定义登录页:提升用户体验。
  3. 密码加密:保障数据安全。
  4. 记住我功能:增强用户体验。
  5. JWT认证:适配前后端分离场景。

Spring Security的强大之处在于灵活性——你可以根据需求选择不同的认证方式,从简单的表单登录到复杂的OAuth2.0/OpenID Connect。

最后,记住安全是持续的过程:定期更新依赖、审计配置、学习最新安全知识,才能让你的应用更安全。

参考资料#

  1. Spring Security官方文档:https://docs.spring.io/spring-security/reference/index.html
  2. Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
  3. JWT官方文档:https://jwt.io/introduction
  4. BCrypt算法:https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/provos/provos.pdf
  5. OWASP Top Ten:https://owasp.org/Top10/
  6. JJWT库文档:https://github.com/jwtk/jjwt

希望本文对你有所帮助!如果有问题,欢迎在评论区交流。