SpringSecurity
发表于更新于
字数总计:2.8k阅读时长:11分钟阅读量: 广西
SpringBootSpringSecurity
nodaoli这里一篇我对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
|

修改用户名和密码
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
|
@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
|
@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
| 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
|
@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("用户未认证或登录已过期,请重新登录后再访问"); String json = JSON.toJSONString(error); response.getWriter().print(json); }}
|
把处理逻辑注册到security
1 2
| @Resource private AuthenticationManager authenticationManager;
|
1
| http.exceptionHandling(e -> e.authenticationEntryPoint(loginNoAuthHandler))
|
调用http的exceptionHandling()方法,使用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("权限不足,请重新授权。"); String json = JSONUtil.toJsonStr(result); response.getWriter().print(json); } }
|
配置令牌
自定义一个每次请求都会先经过的过滤器,继承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()); 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),从而让当前请求被识别为“已登录”状态。
逐项解释参数含义:
第一个参数:userDetails
- 通常是实现了
UserDetails 接口的对象(如 org.springframework.security.core.userdetails.User)。
- 它包含了用户的主体信息(如用户名、密码、权限等)。
- 在 JWT 场景中,这个对象通常是从数据库或缓存中根据 JWT 中的用户名(claim)加载出来的。
第二个参数:null(密码)
- 在传统的表单登录中,这里会传入用户提交的原始密码,用于认证。
- 但在 JWT 认证流程中,认证已经通过验证 JWT 签名完成,不再需要密码。
- 而且出于安全考虑,不应该在内存中保留用户明文或加密后的密码。
- 所以这里传
null 是合理的,因为:
- 认证已通过(JWT 验签成功);
- 密码在此阶段无用,且避免泄露风险。
第三个参数:userDetails.getAuthorities()
- 代表用户拥有的权限(如
ROLE_USER, SCOPE_read 等)。
- Spring Security 后续的权限控制(如
@PreAuthorize、hasRole())依赖这些 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 有效后,你会:
- 从 JWT 中解析出用户名(如
sub claim);
- 根据用户名加载
UserDetails(包含权限);
- 创建如上所示的
UsernamePasswordAuthenticationToken;
- 调用
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
| 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("注销成功"); String json = JSON.toJSONString(result); response.getWriter().print(json); }}
|
注册到配置类中
1
| http.logout(l -> l.logoutSuccessHandler(logoutStatusSuccessHandler))
|
关于权限列表
无论如何都不能返回null,至少也要new一个空数组返回
授权架构 :: Spring Security Reference
存储权限的数组,需要把权限封装进一个GrantedAuthority对象里面,然后再把GrantedAuthority对象放入数组里面

在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) { 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 实例的钩子。