Spring Security 概述

Spring Security 是 Spring 生态中专注于身份认证(Authentication)和授权(Authorization)的安全框架,广泛应用于 Java 企业级应用中,提供了全面的安全解决方案。它不仅能处理传统的用户名密码登录,还支持 OAuth2、JWT、LDAP 等多种认证方式,同时具备细粒度的授权控制、CSRF 防护、会话管理等功能。

核心组件:

  1. SecurityContextHolder:用于存储当前认证用户的安全上下文(SecurityContext),是线程安全的。通过它可以在应用任意位置获取当前用户信息
1
2
3
4
// 获取当前用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName(); // 获取用户名
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 获取权限
  1. Authentication:表示当前用户的认证信息
  2. AuthenticationManager:认证的核心接口,负责验证 Authentication 对象。
1
2
// 核心方法
Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • 若认证成功,返回一个包含用户权限的 Authentication 对象(isAuthenticated=true)
  • 若认证失败,抛出 AuthenticationException 异常
  1. ProviderManager:AuthenticationManager 的默认实现,委托一组 AuthenticationProvider 进行认证
  2. AuthenticationProvider
    具体执行认证逻辑的接口
  3. UserDetailsService:用于加载用户信息
  4. UserDetails:封装用户信息的接口
  5. PasswordEncoder:密码加密器

实现

Spring Security 进行认证和鉴权的时候,利用的一系列的 Filter 来进行拦截的
20250807224632
一个请求想要访问到 API 就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的 API。

重点UsernamePasswordAuthenticationFilter负责登录认证
FilterSecurityInterceptor负责权限授权。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

引入依赖产生的基础作用:

  • 要求经过身份验证的用户才能与应用程序进行交互
  • 创建好了默认登录表单
  • 生成用户名为 user 的随机密码并打印在控制台上
  • CSRF 攻击防护、Session Fixation 攻击防护

认证

流程:

  • 用户提交用户名密码(如表单登录)。
  • Spring Security 将用户名密码封装为 UsernamePasswordAuthenticationToken(Authentication 的实现)。
  • AuthenticationManager 委托 DaoAuthenticationProvider 进行认证。
  • DaoAuthenticationProvider 调用 UserDetailsService 加载用户信息(UserDetails)。
  • 验证用户状态(是否启用、未过期等),并通过 PasswordEncoder 匹配输入密码与存储的加密密码。
  • 认证成功:生成包含用户权限的 Authentication 对象,通过 SecurityContextHolder 存储到 SecurityContext 中。
  • 认证失败:抛出 AuthenticationException,跳转至登录失败页面。

  1. Authentication
    Authentication,它存储了认证信息,代表当前登录用户。我们需要通过 SecurityContext 来获取 Authentication,SecurityContext 就是我们的上下文对象,这个上下文对象则是交由 SecurityContextHolder 进行管理,我们可以在程序任何地方使用它来获取用户信息。
1
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  1. 用户认证
    AuthenticationManager 就是 Spring Security 用于执行身份验证的组件,只需要调用它的 authenticate 方法即可完成认证。Spring Security 默认的认证方式就是在 UsernamePasswordAuthenticationFilter 这个过滤器中进行认证的,该过滤器负责认证逻辑
1
2
3
4
5
6
// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

认证流程:
根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。

用户对象数据可以存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由UserDetialsService 处理,该接口只有一个方法 loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。

加密

PasswordEncoder,采取 MD5 加密
自定义加密处理组件:CustomMd5PasswordEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.util.Arrays;

/**
* 自定义security密码校验
* @author 尹稳健~
* @version 1.0
* @time 2023/1/31
*/
public class CustomMd5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 进行一个md5加密
return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()));
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 通过md5校验
return encodedPassword.equals(Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())));
}
}

用户对象 UserDetails

它提供了用户的一些通用属性
源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface UserDetails extends Serializable {
/**
* 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 用户密码
*/
String getPassword();
/**
* 用户名
*/
String getUsername();
/**
* 用户没过期返回true,反之则false
*/
boolean isAccountNonExpired();
/**
* 用户没锁定返回true,反之则false
*/
boolean isAccountNonLocked();
/**
* 用户凭据(通常为密码)没过期返回true,反之则false
*/
boolean isCredentialsNonExpired();
/**
* 用户是启用状态返回true,反之则false
*/
boolean isEnabled();
}

实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承 Spring Security 提供的 org.springframework.security.core.userdetails.User 类,该类实现了 UserDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.sky.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
* 自定义对象
* @author 尹稳健~
* @version 1.0
* @time 2023/1/31
*/
public class CustomUser extends User {
private SysUser sysUser;

public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}

public SysUser getSysUser() {
return sysUser;
}

public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
}


业务对象 UserDetailsService

该接口很简单只有一个方法:

1
2
3
4
5
6
public interface UserDetailsService {
/**
* 根据用户名获取用户对象(获取不到直接抛异常)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们需要实现该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.sky.model.system.SysUser;
import com.sky.system.custom.CustomUser;
import com.sky.system.service.SysUserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.Objects;

/**
* 实现UserDetailsService接口,重写方法
* @author 尹稳健~
* @version 1.0
* @time 2023/1/31
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Resource
private SysUserService sysUserService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.queryByUsername(username);
if (Objects.isNull(sysUser)){
throw new UsernameNotFoundException("用户名不存在!");
}

if(sysUser.getStatus() == 0) {
throw new RuntimeException("账号已停用");
}
return new CustomUser(sysUser, Collections.emptyList());
}
}

配置 SecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.sky.system.config;

import com.sky.system.custom.CustomMd5PasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

/**
* Security配置类
* @author 尹稳健~
* @version 1.0
* @time 2023/1/31
*/
@Configuration
/**
* @EnableWebSecurity是开启SpringSecurity的默认行为
*/
@EnableWebSecurity
public class SecurityConfig {

/**
* 密码明文加密方式配置
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new CustomMd5PasswordEncoder();
}

/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 基于 token,不需要 csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登录接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/doc.html").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
// 基于 token,不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// cors security 解决方案
.cors().configurationSource(corsConfigurationSource())
.and()
.build();
}

/**
* 配置跨源访问(CORS)
* @return
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

}

使用

业务实现层调用以下方法来获取用户

1
2
3
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
// authenticate方法会调用loadUserByUsername
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

认证过滤器

这个过滤器会去获取请求头中的 token,对 token 进行解析取出其中的信息,获取对应的 LoginUser 对象。然后封装 Authentication 对象存入 SecurityContextHolder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.kob.backend.config.filter;

import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");

if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

token = token.substring(7);

String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}

User user = userMapper.selectById(Integer.parseInt(userid));

if (user == null) {
throw new RuntimeException("用户名未登录");
}

UserDetailImpl loginUser = new UserDetailImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(request, response);
}
}

授权

在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。
SpringSecurity 中的 Authentication 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Authentication extends Principal, Serializable {
//权限数据列表
Collection<? extends GrantedAuthority> getAuthorities();

Object getCredentials();

Object getDetails();

Object getPrincipal();

boolean isAuthenticated();

void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

前面登录时执行 loadUserByUsername 方法时,return new CustomUser(sysUser, Collections.emptyList());后面的空数据对接就是返回给 Spring Security 的权限数据。

在 TokenAuthenticationFilter 中怎么获取权限数据呢?登录时我们把权限数据保存到 redis 中(用户名为 key,权限数据为 value 即可),这样通过 token 获取用户名即可拿到权限数据,这样就可构成出完整的 Authentication 对象。

修改 loadUserByUsername 接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@Autowired
private SysMenuService sysMenuService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if(null == sysUser) {
throw new UsernameNotFoundException("用户名不存在!");
}

if(sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("账号已停用");
}
List<String> userPermsList = sysMenuService.findUserPermsList(sysUser.getId());
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String perm : userPermsList) {
authorities.add(new SimpleGrantedAuthority(perm.trim()));
}
return new CustomUser(sysUser, authorities);
}

修改 SecurityConfig 类

配置类添加注解:

开启基于方法的安全认证机制,也就是说在 web 层的 controller 启用注解机制的安全确认

1
@EnableGlobalMethodSecurity(prePostEnabled = true)

Spring Security 默认是禁用注解的,要想开启注解,需要在继承 WebSecurityConfigurerAdapter 的类上加@EnableGlobalMethodSecurity 注解,来判断用户对某个控制层的方法是否具有访问权限

控制 controller 层接口权限

通过@PreAuthorize 标签控制 controller 层接口权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SysRoleController {

@Autowired
private SysRoleService sysRoleService;

@PreAuthorize("hasAuthority('bnt.sysRole.list')")
@ApiOperation(value = "获取分页列表")
@GetMapping("/{page}/{limit}")
public Result index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,

@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit,

@ApiParam(name = "roleQueryVo", value = "查询对象", required = false)
SysRoleQueryVo roleQueryVo) {
Page<SysRole> pageParam = new Page<>(page, limit);
IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo);
return Result.ok(pageModel);
}


...
}

实际应用中的典型流程:JWT + Spring Security 协作

在前后端分离项目中,两者的协作流程通常是:

认证阶段:用户登录成功后,服务器生成包含用户 ID、角色、权限的 JWT 令牌,返回给客户端;
请求阶段:客户端每次请求时,在请求头携带 JWT(如 Authorization: Bearer );
JWT 验证:Spring Security 的自定义过滤器(如 JwtAuthenticationFilter)拦截请求,验证 JWT 签名和过期时间,解析出用户信息和权限;
授权判断:Spring Security 基于解析出的权限,通过 URL 配置或方法注解判断是否允许访问该资源;
响应处理:授权通过则访问资源,失败则返回 403 错误。