SpringSecurity

这里一篇我对Spring Security的笔记,多谢了下面这位博主的教程,也推荐他的视频

1
2
3
4
5
6
作者: 夜泊1990
企鹅: 1611756908
Q 群: 948233848
邮箱: hd1611756908@163.com
博客: https://hs-an-yue.github.io/
B 站: https://space.bilibili.com/514155929/

理解

这就是一个过滤器Filter

配置好依赖之后,启动就会自动创建一个动态password,默认用户名为user
运行原理图

客户端向服务器发起请求,被身份认证Filter拦截
如果没有授权,让客户端访问/login路由页面
客户端访问/login路由页面,则服务器返回login.html


深入

常用内置核心接口

1
2
3
InMemoryUserDetailsManager: 内存账户信息管理类,是UserDetailsService的子类  
UserDetailsService: SpringSecurity用户信息管理类的核心接口,管理用户信息来源(数据库还是内存以及其他...)
UserDetails: SpringSecurity封装用户信息的核心接口,给SpringSecurity送用户信息时SpringSecurity只认UserDetails

image.png

修改用户名和密码

1
2
3
4
5
6
spring:  
# 更改默认账号和密码
security:
user:
name: admin
password: admin

改完之后就不会在日志里面生成密码,这就是In-Memory Authentication

改成自定义的验证方式

使用数据库Dao层验证用户
创建一个UserDetailsServiceImpl实现UserDetailsService接口,并且启用事务

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Transactional
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}}

然后实现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
31
32
33
34
35
36
37
38
39
40
41
42
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.userdetails.UserDetails;
import top.nodaoli.pojo.User;

import java.util.ArrayList;
import java.util.Collection;

@Datac
public class LoginUserDetails implements UserDetails {

User user;
public LoginUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getPhone();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}}

UserDetailsImpl实现的loadUserByUsername方法返回LoginUserDetail
要做一个判断,如果没有查询到,抛出异常

1
2
3
4
5
6
7
@Override  
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("phone", username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
} return new LoginUserDetails(user);
}

⚠️配置默认加密方式

如果没有配置默认的加密方式,是无法验证的

密码前面加上{noop}就可以跳过密码加密

在配置类中启用

1
2
3
4
5
6
@Configuration  
class SecurityConfigura {
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}}

前后端分离

要更改自己的验证方式,前后端是用表单校验
放置在配置类里面

配置认证管理器 AuthenticationManager

配置类
1
2
3
4
5
6
7
/**
* SpringSecurity 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

配置 SecurityFilterChain

前端请求的数据,第一关过滤器,分流请求数据
也就是访问地址的时候返回的登录页面

配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**  
* SpringSecurity过滤器
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())//防止跨站请求伪造
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 禁用会话,无状态模式
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/test1").permitAll()//登陆和未登录的人都可以访问访问
.anyRequest().authenticated());// 允许所有请求,以及匹配已经登录认证的用户
return http.build();
return http.build();
}

防止跨站请求,比如说小程序的使用app端的
既然都是前后端分离模式,那么就用不上会话管理了

登录控制器中

使用UsernamePasswordAuthenticationToken的构造器,把账号密码封装成security专用的上下文对象

1
2
3
// 封装用户名和密码为security的上下文专用对象  
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(phone, password);

上面已经配置了AuthenticationManager,自动注入之后,那么接下来就调用它的方法,如果为null则表明认证失败

1
2
@Resource  
private AuthenticationManager authenticationManager;
1
2
3
4
5
6
7
try {  
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (Objects.isNull(authenticate)) {
return Result.error("用户名或密码错误");
}} catch (AuthenticationException e) {
return Result.error("用户名或密码错误");
}

配置异常处理

创建未认证处理逻辑

需要添加json处理,我用的是fastjson

https://springdoc.cn/spring-security/servlet/authentication/architecture.html#servlet-authentication-authenticationentrypoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**  
* 自定义一个 自己的未认证访问资源的异常
* 实现AuthenticationEntryPoint接口
* 作用其实就是对未携带认证的请求重定向执行操作
*/
@Component
class LoginNoAuthHandler implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");

Result error = Result.error("用户未认证或登录已过期,请重新登录后再访问");
//将消息json化
String json = JSON.toJSONString(error);
//送到客户端
response.getWriter().print(json);
}}

把处理逻辑注册到security

1
2
@Resource  
private AuthenticationManager authenticationManager;
1
http.exceptionHandling(e -> e.authenticationEntryPoint(loginNoAuthHandler))

调用httpexceptionHandling()方法,使用Lambda表达式


权限异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 如果登录成功,使用当前用户进行访问,发现权限不够,报权限错误,定义处理器进行处理

/**
* 权限不足处理器
* 用户登录成功,访问某一个资源时因为权限不足,报异常
*/
@Component
public class LoginUnAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.error("权限不足,请重新授权。");
//将消息json化
String json = JSONUtil.toJsonStr(result);
//送到客户端
response.getWriter().print(json);
}
}

//自定义权限不足处理器后,需要进行注册,注册到SecurityConfig配置文件中
// http.exceptionHandling(e -> e.accessDeniedHandler(loginUnAccessDeniedHandler))

配置令牌

自定义一个每次请求都会先经过的过滤器,继承OncePerRequestFilter接口

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
@Component  
class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtils redisUtils;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头
String token = request.getHeader("token");
if (StringUtils.hasText(token)) {
String key = "login:"+token;
String json = redisUtils.get(key).toString();
if(StringUtils.hasLength(json)){
//反序列化
LoginUserDetails user = JSON.parseObject(json, LoginUserDetails.class);
if(Objects.nonNull(user)){
//封装用户信息,送到下一个过滤器
UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
//将Redis数据库中的信息送到SpringSecurity上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}else {
SecurityContextHolder.getContext().setAuthentication(null);
} } }
//继续执行下一个过滤器
filterChain.doFilter(request,response);}}

在 Spring Security 中使用 JWT(JSON Web Token)进行认证时,你看到的这段代码:

1
2
3
4
5
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

它的作用是手动创建一个已认证(authenticated)的 Authentication 对象,并将它设置到 Spring Security 的上下文中(通常是 SecurityContextHolder),从而让当前请求被识别为“已登录”状态。


逐项解释参数含义:

  1. 第一个参数:userDetails

    • 通常是实现了 UserDetails 接口的对象(如 org.springframework.security.core.userdetails.User)。
    • 它包含了用户的主体信息(如用户名、密码、权限等)。
    • 在 JWT 场景中,这个对象通常是从数据库或缓存中根据 JWT 中的用户名(claim)加载出来的。
  2. 第二个参数:null(密码)

    • 在传统的表单登录中,这里会传入用户提交的原始密码,用于认证。
    • 但在 JWT 认证流程中,认证已经通过验证 JWT 签名完成,不再需要密码。
    • 而且出于安全考虑,不应该在内存中保留用户明文或加密后的密码
    • 所以这里传 null 是合理的,因为:
      • 认证已通过(JWT 验签成功);
      • 密码在此阶段无用,且避免泄露风险。
  3. 第三个参数:userDetails.getAuthorities()

    • 代表用户拥有的权限(如 ROLE_USER, SCOPE_read 等)。
    • Spring Security 后续的权限控制(如 @PreAuthorizehasRole())依赖这些 GrantedAuthority
    • 必须传入,否则用户会被视为“无权限”。

为什么这样创建 UsernamePasswordAuthenticationToken

  • UsernamePasswordAuthenticationToken 有两个常用构造函数:
    • (Object principal, Object credentials) → 用于未认证状态(authenticated = false
    • (Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) → 用于已认证状态(authenticated = true

一旦传入 authorities(哪怕为空集合),该 token 就被视为 已认证(authenticated = true)

所以在 JWT 过滤器中(比如 JwtAuthenticationFilter),验证 JWT 有效后,你会:

  1. 从 JWT 中解析出用户名(如 sub claim);
  2. 根据用户名加载 UserDetails(包含权限);
  3. 创建如上所示的 UsernamePasswordAuthenticationToken
  4. 调用 SecurityContextHolder.getContext().setAuthentication(authenticationToken);

这样,后续的 Spring Security 组件(如方法安全、URL 拦截)就能识别当前用户及其权限。


补充说明:为什么不使用其他 Authentication 实现?

虽然也可以自定义 Authentication 实现类,但 UsernamePasswordAuthenticationToken 是最通用且被广泛支持的。即使没有密码,只要标记为已认证,它就能正常工作。


典型使用场景(JWT 过滤器片段):

1
2
3
4
5
6
7
8
9
10
11
12
String token = jwtUtils.extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authToken);
}

配置中启用

记得要先自动注入一下自定义的过滤器

1
2
//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面  
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

注销

创建一个注销成功类实现LogoutSuccessHandler接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component  
class LogoutStatusSuccessHandler implements LogoutSuccessHandler {
@Resource
private RedisUtils redisUtils;

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
if(StringUtils.hasText(token)){
redisUtils.del("login:"+token);
} response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
Result result = Result.ok("注销成功");
//将消息json化
String json = JSON.toJSONString(result);
//送到客户端
response.getWriter().print(json);
}}

注册到配置类中

1
http.logout(l -> l.logoutSuccessHandler(logoutStatusSuccessHandler))

关于权限列表

无论如何都不能返回null,至少也要new一个空数组返回
授权架构 :: Spring Security Reference

存储权限的数组,需要把权限封装进一个GrantedAuthority对象里面,然后再把GrantedAuthority对象放入数组里面
image.png
LoginUserDetails中修改,用到了一个SimpleGrantedAuthority(String role),只能传String类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override  
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

// 判断角色和权限列表是否为空
if (!CollectionUtils.isEmpty(roleNames)) {
for (String roleName : roleNames) {
// add prefix "ROLE_"
roleName = "ROLE_" + roleName;
grantedAuthorities.add(new SimpleGrantedAuthority(roleName));
}
}
return grantedAuthorities;
}

使用方法级别安全设置

Spring Boot Starter Security 默认不激活方法级授权。

在任何 @Configuration 类中注解 @EnableMethodSecurity 或在任何 XML 配置文件中添加 <method-security> 来激活:

1
@EnableMethodSecurity(securedEnabled = true)

然后,你就可以立即用 @PreAuthorize@PostAuthorize@PreFilter 和 @PostFilter 注解任何 Spring 管理的类或方法,以授权方法调用,包括参数和返回值。
方法安全(Method Security) :: Spring Security Reference

  • permitAll - 该方法无需授权即可调用;请注意,在这种情况下,将不会从 session 中获取 Authentication 信息。
  • denyAll - 在任何情况下都不允许使用该方法;请注意,在这种情况下,将永远不会从 session 中检索 Authentication
  • hasAuthority - 该方法要求 Authentication 的 GrantedAuthority 符合给定值。
  • hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。
  • hasAnyAuthority - 该方法要求 Authentication 的 GrantedAuthority 符合任何给定值。
  • hasAnyRole - hasAnyAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。
  • hasPermission - 用于对象级授权的 PermissionEvaluator 实例的钩子。