Spring Security 实战:从认证到授权,30 分钟搞定接口安全防护

Spring Security 实战:从认证到授权,30 分钟搞定接口安全防护

作为互联网软件开发人员,你是否曾在项目中遇到过这些问题?用户密码明文存储导致数据泄露,不同角色的用户能随意访问敏感接口,跨域请求时认证信息失效…… 这些安全隐患不仅会影响用户体验,更可能给项目带来严重的安全风险。而 Spring Security,作为 Java 生态中最主流的安全框架,正是解决这些问题的 “利器”。今天这篇文章,我们就从实战角度出发,手把手教你搞定 Spring Security 的认证与授权,让你的接口防护能力直接拉满。

为什么必须掌握 Spring Security?先看 3 个真实开发场景

在开始技术讲解前,我们先聊聊为什么 Spring Security 是后端开发者的 “必修课”。我曾接触过三个典型的开发案例,或许能让你更直观地理解它的重大性。

第一个案例是某电商平台的后台管理系统。初期开发时,团队为了赶进度,只简单做了 “用户名 + 密码” 的登录验证,没有做角色权限控制。结果上线后,运营人员通过普通账号竟能访问到订单支付接口,还能修改用户的支付金额 —— 这就是典型的 “授权缺失” 导致的安全漏洞。最后团队紧急引入 Spring Security,花了 3 天时间重构权限体系,才避免了更大的损失。

第二个案例是某 SaaS 系统的 API 开发。开发者在设计认证机制时,自己手写了 Token 生成和验证逻辑,却忽略了 Token 的过期刷新、签名加密等细节。上线后不久,就出现了 Token 被伪造的情况,导致多个企业用户的数据被非法获取。后来改用 Spring Security 的 OAuth2.0 组件,仅用半天就实现了安全合规的认证流程。

第三个案例更常见:许多开发者在集成 Spring Security 时,由于不了解其核心过滤器链的工作原理,盲目自定义配置,导致出现 “登录成功后仍无法访问接口”“权限注解不生效” 等问题,排查了一整天才发现是过滤器顺序配置错误。

实则这些问题的根源,都是对 Spring Security 的认证与授权核心逻辑理解不透彻。接下来,我们就从基础概念入手,一步步搭建实战案例,帮你彻底搞懂这套框架。

认证:搞定 “你是谁” 的核心流程(附代码实战)

第一要明确:认证(Authentication)是确认用户身份的过程,简单说就是回答 “你是谁” 的问题。Spring Security 的认证流程围绕 “Authentication 对象” 和 “AuthenticationManager 接口” 展开,我们先拆解核心组件,再写代码。

2.1 3 个核心组件,搞懂认证底层逻辑

Authentication 对象:存储认证相关信息的载体,包含 3 个关键属性:

  • principal:用户身份信息(如用户名、用户实体类对象);
  • credentials:用户凭证(如密码,认证成功后会被清空,避免泄露);
  • authorities:用户拥有的权限(认证阶段暂不关注,授权阶段会用到)。

认证前,我们会创建一个 “未认证” 的 Authentication 对象,传入用户名和密码;认证成功后,Spring Security 会返回一个 “已认证” 的对象,填充用户的完整信息。

AuthenticationManager:认证的核心接口,只有一个authenticate()方法,负责执行认证逻辑。实际开发中,我们常用它的实现类ProviderManager,它可以管理多个AuthenticationProvider(不同的认证方式,如用户名密码认证、短信验证码认证)。

UserDetailsService:加载用户信息的接口,Spring Security 默认会调用它的loadUserByUsername()方法,根据用户名查询数据库中的用户信息(如密码、角色)。这是我们自定义认证逻辑时最常扩展的接口。

简单理解:认证流程就是 “前端传入用户名密码 → 构建未认证 Authentication 对象 → AuthenticationManager 调用 UserDetailsService 查询用户 → 比对密码是否正确 → 返回已认证对象”。

2.2 实战:10 行代码实现数据库认证(Spring Boot 整合)

接下来我们用 Spring Boot 整合 Spring Security,实现 “从数据库查询用户信息” 的认证功能。假设你已经搭建好 Spring Boot 项目,且有一个sys_user表(包含 id、username、password、role 字段)。

步骤 1:引入依赖(pom.xml)

<!-- Spring Security核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis-Plus(用于操作数据库,也可用JPA) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

步骤 2:实现 UserDetailsService 接口(查询用户信息)

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper; // 自定义的Mapper接口

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询数据库
        SysUser user = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        // 2. 转换为Spring Security需要的UserDetails对象
        // 注意:密码必须是加密后的(这里假设数据库中存储的是BCrypt加密后的密码)
        Collection<? extends GrantedAuthority> authorities = 
            Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

步骤 3:配置 Spring Security(核心配置类)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    // 密码加密器(使用BCrypt算法)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
        // 关联UserDetailsService和密码加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        return auth.build();
    }

    // 配置接口访问规则、登录逻辑等
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 关闭CSRF(前后端分离项目常用,若为传统项目需开启)
            .csrf(csrf -> csrf.disable())
            // 配置接口访问权限
            .authorizeHttpRequests(auth -> auth
                // 放行登录接口(不需要认证就能访问)
                .requestMatchers("/api/login").permitAll()
                // 其他所有接口都需要认证
                .anyRequest().authenticated()
            )
            // 配置表单登录(前后端分离可改用JSON登录,这里先演示默认表单)
            .formLogin(form -> form
                // 自定义登录接口地址(默认是/login)
                .loginProcessingUrl("/api/login")
                // 登录成功后的返回格式(默认是跳转页面,这里改为返回JSON)
                .successHandler((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("code", 200);
                    result.put("message", "登录成功");
                    result.put("data", authentication.getPrincipal());
                    response.getWriter().write(new ObjectMapper().writeValueAsString(result));
                })
                // 登录失败后的返回格式
                .failureHandler((request, response, exception) -> {
                    response.setContentType("application/json;charset=utf-8");
                    Map<String, Object> result = new HashMap<>();
                    result.put("code", 401);
                    result.put("message", "登录失败:" + exception.getMessage());
                    response.getWriter().write(new ObjectMapper().writeValueAsString(result));
                })
            );
        return http.build();
    }
}

步骤 4:测试认证功能

此时启动项目,发送 POST 请求到/api/login,传入参数username和password(注意:前端传入的是明文密码,Spring Security 会自动用 BCrypt 加密后与数据库中的加密密码比对)。若用户存在且密码正确,会返回如下 JSON:

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "username": "admin",
    "authorities": [{"authority": "ROLE_ADMIN"}],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true
  }
}

至此,我们就完成了最基础的数据库认证功能。但这还不够 —— 如果所有认证通过的用户都能访问所有接口,那管理员和普通用户就没有区别了,这就需要 “授权” 来解决。

授权:控制 “你能做什么” 的 3 种实战方案

授权(Authorization)是确认用户拥有哪些权限的过程,也就是回答 “你能做什么” 的问题。Spring Security 的授权核心是 “基于角色的访问控制(RBAC)”,常用的实现方式有 3 种:接口级授权、方法级授权、动态权限授权。我们逐一讲解实战用法。

3.1 方案 1:接口级授权(在 SecurityConfig 中配置)

这种方式最直接,在securityFilterChain()方法中,通过requestMatchers()指定接口路径,并搭配hasRole()或hasAuthority()设置访问权限。

例如,我们希望:

  • /api/admin/**接口只能由 “ADMIN” 角色访问;
  • /api/user/**接口只能由 “USER” 角色访问;
  • /api/public/**接口无需认证即可访问。

修改 SecurityConfig 中的authorizeHttpRequests()配置:

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/public/**").permitAll() // 放行公开接口
    .requestMatchers("/api/admin/**").hasRole("ADMIN") // ADMIN角色可访问
    .requestMatchers("/api/user/**").hasRole("USER") // USER角色可访问
    .anyRequest().authenticated()
)

注意:hasRole()会自动给角色名加上 “ROLE_” 前缀,所以如果数据库中存储的角色是 “ROLE_ADMIN”,这里用hasRole(“ADMIN”)即可;若数据库中是 “ADMIN”,则需要用hasAuthority(“ADMIN”)(hasAuthority()不会加前缀)。

3.2 方案 2:方法级授权(用注解控制)

如果接口较多,在配置类中逐个配置会很繁琐,此时可以用注解实现 “方法级授权”。只需两步:

步骤 1:在 SecurityConfig 上添加@EnableMethodSecurity注解

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 开启方法级授权注解
public class SecurityConfig {
    // 其他配置不变
}

步骤 2:在 Controller 方法上添加@PreAuthorize注解

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    // 只有ADMIN角色能访问该方法
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/user/list")
    public String getUserList() {
        return "管理员查看用户列表";
    }

    // 只有同时拥有ADMIN角色和MANAGE_USER权限的用户能访问
    @PreAuthorize("hasRole('ADMIN') and hasAuthority('MANAGE_USER')")
    @PostMapping("/user/add")
    public String addUser() {
        return "管理员添加用户";
    }
}

这种方式的优势是 “权限与方法绑定”,代码更直观,后期维护也更方便。

3.3 方案 3:动态权限授权(从数据库加载权限)

前面两种方案的权限都是 “写死” 在代码中的,如果需要动态修改权限(列如在后台管理系统中,管理员可以随时调整角色的权限),就需要 “从数据库加载权限”。实现思路是:自定义Filter,在请求到达时,从数据库查询当前用户的权限,再与请求的接口进行匹配。

核心步骤:

设计权限表结构:新增sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户 – 角色关联表)、sys_role_permission(角色 – 权限关联表);

自定义权限过滤器:继承OncePerRequestFilter,在doFilterInternal()方法中:

  • 获取当前登录用户;
  • 从数据库查询用户拥有的所有权限(如/api/admin/user/list);
  • 构建SecurityContext,将权限设置到 Authentication 对象中;

将过滤器加入 Spring Security 的过滤器链

这里给出关键代码(自定义过滤器):

@Component
public class DynamicPermissionFilter extends OncePerRequestFilter {

    @Autowired
    private SysPermissionMapper permissionMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 获取当前登录用户
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            // 2. 获取用户名
            String username = authentication.getName();
            // 3. 从数据库查询用户拥有的权限(这里需要关联用户-角色-权限表)
            List<String> permissions = permissionMapper.getPermissionsByUsername(username);
            // 4. 构建权限集合
            Collection<GrantedAuthority> authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
            // 5. 重新构建Authentication对象,设置动态权限
            Authentication newAuth = new UsernamePasswordAuthenticationToken(
                authentication.getPrincipal(),
                authentication.getCredentials(),
                authorities
            );
            // 6. 更新SecurityContext
            SecurityContextHolder.getContext().setAuthentication(newAuth);
        }
        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }
}

然后在 SecurityConfig 中,将这个过滤器加入过滤器链:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, DynamicPermissionFilter dynamicPermissionFilter) throws Exception {
    http
        // 其他配置不变
        .addFilterBefore(dynamicPermissionFilter, UsernamePasswordAuthenticationFilter.class); // 在用户名密码过滤器前执行
    return http.build();
}

这样一来,用户的权限就会从数据库动态加载,后续修改权限时,只需更新数据库即可,无需修改代码。

避坑指南:5 个开发中最容易踩的坑

在集成 Spring Security 的过程中,许多开发者会由于对细节不熟悉而踩坑。我整理了 5 个最常见的问题,帮你少走弯路。

坑 1:密码加密方式不匹配

问题表现:明明输入的密码正确,却始终提示 “Bad credentials”(密码错误)。

缘由:数据库中存储的密码是明文,而 Spring Security 默认使用 BCrypt 加密器,会将前端传入的明文密码加密后与数据库中的明文比对,自然不匹配。

解决方案:确保数据库中存储的是加密后的密码。可以用PasswordEncoder的encode()方法生成加密密码,再存入数据库:

public static void main(String[] args) {
    PasswordEncoder encoder = new BCryptPasswordEncoder();
    String encodedPassword = encoder.encode("123456"); // 加密明文密码“123456”
    System.out.println(encodedPassword); // 输出加密后的密码,如$2a$10$...
}

坑 2:CSRF 保护导致接口无法访问

问题表现:前后端分离项目中,发送 POST 请求时,提示 “403 Forbidden”,且响应信息包含 “CSRF token”。

缘由:Spring Security 默认开启 CSRF 保护,会要求请求中携带 CSRF Token,而前后端分离项目中,前端一般不会处理这个 Token。

解决方案:前后端分离项目中,直接关闭 CSRF 保护(如前面的配置中csrf(csrf -> csrf.disable()));若为传统服务器渲染项目(如 JSP),则需要在表单中添加 CSRF Token:

<form action="/login" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <!-- 其他表单元素 -->
</form>

坑 3:过滤器顺序配置错误

问题表现:自定义的过滤器不生效,或者权限判断逻辑出现异常。

缘由:Spring Security 的过滤器链有严格的执行顺序,若自定义过滤器的位置放错,会导致逻辑混乱。例如,将动态权限过滤器放在
UsernamePasswordAuthenticationFilter之后,会导致权限判断时,用户还未完成认证。

解决方案:牢记核心过滤器的执行顺序(从先到后):

  1. CsrfFilter(CSRF 保护过滤器)
  2. UsernamePasswordAuthenticationFilter(用户名密码认证过滤器)
  3. RememberMeAuthenticationFilter(记住我过滤器)
  4. FilterSecurityInterceptor(权限判断过滤器)

自定义过滤器时,需根据功能确定位置:列如动态权限过滤器需要在 “认证后、权限判断前” 执行,所以放在
UsernamePasswordAuthenticationFilter之后、FilterSecurityInterceptor之前;而验证码过滤器则需要在
UsernamePasswordAuthenticationFilter之前执行(先验证验证码,再进行账号密码认证)。

坑 4:@PreAuthorize 注解不生效

问题表现:在 Controller 方法上添加了@PreAuthorize(“hasRole(‘ADMIN’)”),但普通用户仍能访问该接口。

缘由:常见有 3 种缘由:

未在 SecurityConfig 上添加@EnableMethodSecurity(prePostEnabled = true)注解,导致方法级授权注解未开启;

注解中的表达式错误,列如角色名大小写不匹配(hasRole(‘admin’)与数据库中的 “ADMIN” 不匹配);

Spring Security 版本问题:Spring Boot 3.x 后,默认使用@EnableMethodSecurity,而 Spring Boot 2.x 使用@
EnableGlobalMethodSecurity,若版本与注解不匹配会导致失效。

解决方案

确认注解开启:Spring Boot 3.x 用@EnableMethodSecurity(prePostEnabled = true),Spring Boot 2.x 用@
EnableGlobalMethodSecurity(prePostEnabled = true);

检查表达式:角色名严格区分大小写,若用hasRole()需确认数据库中角色是否带 “ROLE_” 前缀;

排除版本冲突:在 pom.xml 中指定 Spring Security 的版本,与 Spring Boot 版本匹配(如 Spring Boot 3.2.x 对应 Spring Security 6.2.x)。

坑 5:跨域请求时认证信息丢失

问题表现:前后端分离项目中,前端发送跨域请求(如 Vue 项目部署在localhost:8080,后端在localhost:8081),登录成功后,后续请求仍提示 “未认证”。

缘由:跨域请求默认不会携带 Cookie,而 Spring Security 的 Session 认证依赖 Cookie 中的 JSESSIONID,导致后续请求无法识别用户身份;若用 Token 认证(如 JWT),则可能是前端未在请求头中携带 Token,或后端未配置跨域允许携带请求头。

解决方案

若用 Session 认证:

  • 后端配置跨域允许携带 Cookie:
@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("http://localhost:8080") // 允许的前端域名
                    .allowedMethods("GET", "POST", "PUT", "DELETE")
                    .allowCredentials(true) // 允许携带Cookie
                    .maxAge(3600);
        }
    };
}
  • 前端请求时开启 withCredentials:
// Axios示例
axios.post("http://localhost:8081/api/user/info", {}, {
    withCredentials: true // 携带Cookie
});

若用 Token 认证:

  • 前端在请求头中携带 Token:
axios.post("http://localhost:8081/api/user/info", {}, {
    headers: {
        "Authorization": "Bearer " + localStorage.getItem("token") // Bearer + Token格式
    }
});
  • 后端配置跨域允许请求头:

在addCorsMappings()中添加.allowedHeaders(“Authorization”),允许携带 Authorization 头。

实战扩展:Token 认证(JWT)与记住我功能

前面我们讲的是基于 Session 的认证,但在分布式系统中(如多台后端服务器部署),Session 认证会出现 “Session 共享” 问题,此时更推荐用 Token 认证(如 JWT)。下面我们补充 JWT 认证的实战实现,以及常用的 “记住我” 功能。

5.1 JWT 认证:实现无状态的身份验证

JWT(JSON Web Token)是一种无状态的认证方式,核心是将用户信息加密到 Token 中,后端无需存储 Session,只需验证 Token 的有效性即可。

步骤 1:引入 JWT 依赖

<!-- JJWT(JWT的Java实现库) -->
<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>

步骤 2:编写 JWT 工具类(生成 Token、验证 Token)

@Component
public class JwtUtils {

    // 密钥(实际项目中需放在配置文件,避免硬编码)
    @Value("${jwt.secret}")
    private String secret;

    // Token过期时间(1小时,单位:毫秒)
    @Value("${jwt.expiration}")
    private long expiration;

    // 生成Token
    public String generateToken(String username) {
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setSubject(username) // 设置用户名
                .setIssuedAt(now) // 签发时间
                .setExpiration(expireDate) // 过期时间
                .signWith(SignatureAlgorithm.HS512, secret) // 签名算法
                .compact();
    }

    // 从Token中获取用户名
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    // 验证Token是否有效(未过期、签名正确)
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // Token过期、签名错误等都会抛出异常
            return false;
        }
    }
}

步骤 3:自定义 JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 从请求头中获取Token
        String token = getTokenFromRequest(request);

        // 2. 验证Token有效性
        if (token != null && jwtUtils.validateToken(token)) {
            // 3. 从Token中获取用户名
            String username = jwtUtils.getUsernameFromToken(token);
            // 4. 加载用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            // 5. 构建Authentication对象,设置到SecurityContext
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }

    // 从Authorization头中获取Token(格式:Bearer <token>)
    private String getTokenFromRequest(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7); // 截取“Bearer ”后的Token
        }
        return null;
    }
}

步骤 4:更新 SecurityConfig,集成 JWT 过滤器

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    // 省略其他注入...

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            // 关闭Session(JWT是无状态认证,无需Session)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/login").permitAll()
                .anyRequest().authenticated()
            )
            // 登录成功后生成Token并返回
            .formLogin(form -> form
                .loginProcessingUrl("/api/login")
                .successHandler((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    Map<String, Object> result = new HashMap<>();
                    String username = authentication.getName();
                    String token = jwtUtils.generateToken(username); // 生成JWT Token
                    result.put("code", 200);
                    result.put("message", "登录成功");
                    result.put("data", token); // 返回Token给前端
                    response.getWriter().write(new ObjectMapper().writeValueAsString(result));
                })
                .failureHandler(/* 省略失败处理,同之前 */)
            )
            // 添加JWT过滤器(在用户名密码过滤器之前执行)
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

步骤 5:测试 JWT 认证

  1. 发送 POST 请求到/api/login,获取返回的 Token(如eyJhbGciOiJIUzUxMiJ9…);
  2. 发送后续请求(如/api/user/info)时,在请求头中添加Authorization: Bearer eyJhbGciOiJIUzUxMiJ9…;
  3. 后端会通过 JWT 过滤器验证 Token,若有效则允许访问接口。

5.2 “记住我” 功能:实现自动登录

“记住我” 功能允许用户在关闭浏览器后,再次访问时无需重新登录。Spring Security 默认基于数据库存储 “记住我” 令牌,实现步骤如下:

步骤 1:创建 “记住我” 令牌表(MySQL)

CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

步骤 2:配置 “记住我” 功能

在 SecurityConfig 中添加rememberMe()配置:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 省略其他配置...
        .rememberMe(rememberMe -> rememberMe
            .tokenRepository(jdbcTokenRepository()) // 基于数据库存储令牌
            .tokenValiditySeconds(60 * 60 * 24 * 7) // 令牌有效期(7天)
            .userDetailsService(userDetailsService) // 加载用户信息
        );

    return http.build();
}

// 配置JDBC令牌仓库
@Bean
public JdbcTokenRepositoryImpl jdbcTokenRepository() {
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource); // 注入数据源
    // 首次启动时自动创建persistent_logins表(若已手动创建,注释掉这行)
    // tokenRepository.setCreateTableOnStartup(true);
    return tokenRepository;
}

步骤 3:前端添加 “记住我” 复选框

在登录表单中添加复选框,参数名固定为remember-me:

<form action="/api/login" method="post">
    <input type="text" name="username" placeholder="用户名">
    <input type="password" name="password" placeholder="密码">
    <input type="checkbox" name="remember-me" value="true"> 记住我
    <button type="submit">登录</button>
</form>

若为前后端分离项目,前端需在登录请求中携带remember-me参数(布尔值)。

步骤 4:测试 “记住我” 功能

  1. 勾选 “记住我” 并登录;
  2. 关闭浏览器,再次打开并访问需要认证的接口(如/api/user/info);
  3. 无需重新登录即可正常访问,说明 “记住我” 功能生效。

总结:Spring Security 认证与授权核心流程

看到这里,信任你已经对 Spring Security 的认证与授权有了清晰的理解。最后我们用一张流程图,总结核心逻辑:

认证流程

前端传入认证信息(用户名密码 / Token)→ 后端过滤器(如
UsernamePasswordAuthenticationFilter/JwtAuthenticationFilter)接收请求 → 调用 AuthenticationManager 执行认证 → 通过 UserDetailsService 加载用户信息 → 比对认证信息(密码 / Token 有效性)→ 认证成功后,将 Authentication 对象存入 SecurityContext。

授权流程

请求到达 FilterSecurityInterceptor → 从 SecurityContext 获取 Authentication 对象 → 调用 AccessDecisionManager 判断用户权限是否满足接口要求 → 权限足够则允许访问,不足则返回 403。

掌握这套流程后,无论遇到简单的单体项目,还是复杂的分布式系统,你都能快速搭建安全可靠的接口防护体系。如果在实际开发中遇到具体问题,欢迎在评论区留言讨论,我们一起解决!

© 版权声明

相关文章

3 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    怪旅泊 读者

    向你学习👍

    无记录
  • 头像
    投行社 读者

    收藏了,感谢分享

    无记录
  • 头像
    炖鱼请放香菜 投稿者

    请受我一拜👍

    无记录