Spring Security 概述 Spring Security 是 Spring 生态中专注于身份认证(Authentication)和授权(Authorization)的安全框架,广泛应用于 Java 企业级应用中,提供了全面的安全解决方案。它不仅能处理传统的用户名密码登录,还支持 OAuth2、JWT、LDAP 等多种认证方式,同时具备细粒度的授权控制、CSRF 防护、会话管理等功能。
核心组件:
SecurityContextHolder:用于存储当前认证用户的安全上下文(SecurityContext),是线程安全的。通过它可以在应用任意位置获取当前用户信息
1 2 3 4 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();String username = authentication.getName(); Collection<? extends GrantedAuthority > authorities = authentication.getAuthorities();
Authentication:表示当前用户的认证信息
AuthenticationManager:认证的核心接口,负责验证 Authentication 对象。
1 2 Authentication authenticate (Authentication authentication) throws AuthenticationException;
若认证成功,返回一个包含用户权限的 Authentication 对象(isAuthenticated=true)
若认证失败,抛出 AuthenticationException 异常
ProviderManager:AuthenticationManager 的默认实现,委托一组 AuthenticationProvider 进行认证
AuthenticationProvider 具体执行认证逻辑的接口
UserDetailsService:用于加载用户信息
UserDetails:封装用户信息的接口
PasswordEncoder:密码加密器
实现 Spring Security 进行认证和鉴权的时候,利用的一系列的 Filter 来进行拦截的 一个请求想要访问到 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,跳转至登录失败页面。
Authentication Authentication,它存储了认证信息,代表当前登录用户。我们需要通过 SecurityContext 来获取 Authentication,SecurityContext 就是我们的上下文对象,这个上下文对象则是交由 SecurityContextHolder 进行管理,我们可以在程序任何地方使用它来获取用户信息。
1 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
用户认证 AuthenticationManager 就是 Spring Security 用于执行身份验证的组件,只需要调用它的 authenticate 方法即可完成认证。Spring Security 默认的认证方式就是在 UsernamePasswordAuthenticationFilter 这个过滤器中进行认证的,该过滤器负责认证逻辑
1 2 3 4 5 6 Authentication authenticationToken = new UsernamePasswordAuthenticationToken (username, passwrod);Authentication authentication = authenticationManager.authenticate(authenticationToken);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;public class CustomMd5PasswordEncoder implements PasswordEncoder { @Override public String encode (CharSequence rawPassword) { return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())); } @Override public boolean matches (CharSequence rawPassword, String encodedPassword) { 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 () ; boolean isAccountNonExpired () ; boolean isAccountNonLocked () ; boolean isCredentialsNonExpired () ; 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;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;@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;@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder () { return new CustomMd5PasswordEncoder (); } @Bean public AuthenticationManager authenticationManager (AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain (HttpSecurity http) throws Exception { return http .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() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .cors().configurationSource(corsConfigurationSource()) .and() .build(); } @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()); 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 错误。