Spring Security 配置和源码解析
Spring Security 配置和源码解析
背景:spring-boot-starter-security:2.3.9.RELEASE
在微服务中,整合Spring Security
功能。将系统模块(涵盖用户、菜单等功能的模块)与Spring Security
进行分离,使Spring Security
作为一个单独的依赖存在,使用自动配置的方式配置进使用该依赖的模块当中。
思路
处理流程
Spring Security
的原理是一个过滤器链,内部包含了提供各种功能的过滤器
可以从Ioc
中获取SecurityFilterChain
对象,调用getFilters
方法得到过滤器链(15个)
其中:
- UsernamePasswordAuthenticationFilter:负责处理在登陆页面的登陆请求
- ExceptionTranslationFilter:处理过滤器链中抛出的任何异常,然后经过一系列逻辑处理,抛出
AccessDeniedException
或AuthenticationException
(一般不会直接抛出在此之前过滤器中出现的异常),或是拒绝访问处理 - FilterSecurityInterceptor:负责权限校验的过滤器
认证流程
说明
-
……
-
attemptAuthentication
方法由UsernamePasswordAuthenticationFilter
实现 -
调用的是
AbstractAuthenticationProcessingFilter.authenticate
,目的是封装了一个Authentication
对象 -
调用的是
AuthenticationProvider.authenticate
,而AuthenticationProvider
的实现关系是:AuthenticationProvider
-AbstractUserDetailsAuthenticationProvider
-DaoAuthenticationProvider
-
调用的是
AbstractUserDetailsAuthenticationProvider.retrieveUser
。retrieveUser
是一个抽象方法,由DaoAuthenticationProvider
实现 -
调用的是
UserDetailsService.loadUserByUsername
-
……
整合 Spring Security
实现UserDetailsService
接口
重写loadUserByUsername
方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户信息
...
if (Objects.isNull(user)) {
throw new BadCredentialsException("用户不存在");
}
// 获取权限列表
...
return new LoginUser(user, new ArrayList<>(list));
}
}
实现UserDetails
接口
将用户对象
、用户权限
作为成员变量。其中GrantedAuthority
无法被序列化,所以需要使用额外的对象来存储权限信息:permissions
,并且重写getAuthorities
方法
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private UserEntity user;
private List<String> permissions;
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
public LoginUser(UserEntity user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getBcPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
...
}
完成上述操作,就已经可以在
Spring Security
的原本配置中,使用/login
接口进行登录验证了
自定义登录接口
登录成功之后将用户信息存入Redis
中
@Service
public class LoginServiceImpl implements LoginService {
@Override
public R login(UserEntity user) {
// 创建Authentication对象,然后进行认证操作
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),
user.getBcPassword());
Authentication authenticate = SpringUtil.getBean(AuthenticationManager.class).authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
// 获取用户Id,作为Redis的Key,User作为Value存入Redis中
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
...
}
@Override
public R logout() {
// 从上下文中获取用户Id,然后从Redis中移除
...
}
}
定义JWT
过滤器
使用继承OncePreRequestFilter
的方式。作用是获取Token
并且解析,然后进行用户是否登录的验证
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的JWT
...
// 解析JWT,获取用户Id,从Redis中获取用户信息和权限列表
...
// 完成认证,然后将认证信息存入上下文中,最后放行
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
Spring Security
核心配置
将登录接口进行放行。重写WebSecurityConfigurerAdapter
中的configure
方法
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityAutoConfiguration extends WebSecurityConfigurerAdapter {
@Resource
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
public DefaultAuthenticationEntryPoint authenticationEntryPoint;
@Resource
public DefaultAccessDeniedHandler accessDeniedHandler;
@Resource
public SecurityProperties properties;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
if (!CollectionUtils.isEmpty(properties.getAnonymous())) {
for (String anonymous : properties.getAnonymous()) {
http.authorizeRequests().antMatchers(anonymous).anonymous();
}
}
http.authorizeRequests().anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
认证过程中出现的异常
异常处理流程
因为在过滤器中抛出的异常一般都由
ExceptionTranslationFilter
来处理,所以异常抛出后执行的入口就是ExceptionTranslationFilter.doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...
try {
// 入口
chain.doFilter(request, response);
...
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
// 这里一般只会有:AuthenticationException、AccessDeniedException
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
...
// 最后执行到这里
handleSpringSecurityException(request, response, chain, ase);
} else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
// 分别单独处理两种认证异常:AuthenticationException、AccessDeniedException
if (exception instanceof AuthenticationException) {
...
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
...
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
...
// 其他异常走拒绝访问处理
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
...
// 关键出口
authenticationEntryPoint.commence(request, response, reason);
}
由源码可知:如果想要自定义认证异常的处理逻辑,可以从
authenticationEntryPoint.commence
入手;自定义其他异常的处理逻辑可以从accessDeniedHandler.handle
入手
自定义认证失败处理器
实现AuthenticationEntryPoint
接口
@Component
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
HttpUtil.response(response, 50000 + HttpStatus.UNAUTHORIZED.value(), "认证失败");
}
}
自定义拒绝访问处理器
实现AccessDeniedHandler
接口
@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
HttpUtil.response(response, 50000 + HttpStatus.FORBIDDEN.value(), accessDeniedException.getMessage());
}
}
跨域
除了在configure
中配置Spring Security
的跨域问题:cors
,还需要打开Servlet
的跨域:
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
或者使用注解:@CrossOrigin
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?