什么是SpringSecurity

Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。

Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。而我们最常用的两个功能就是认证和鉴权,因此作为入门文章本文也只介绍这两个功能的使用。

Spring Security可以用于Servlet应用和Reactive应用,本文主要介绍基于Servlet应用的场景

如需更详细的使用方式请参考官方文档:https://spring.io/projects/spring-security

架构


上图是Spring Security官方提供的架构图。我们先看图的左边部分,就是一个典型Servlet Filter (过滤器)处理流程,我们依次讲解流程涉及的组件。

FilterChain

FilterChain :过滤器链,是Servlet容器在接收到客户端发送的请求时创建的,一个FilterChain可以包含多个Filter和一个ServletServlet容器根据请求URI的路径来处理HttpServletRequest

在Spring MVC中,Servlet 就是 DispatcherServlet实例。一个 Servlet 最多只能处理一个 HttpServletRequest 和 HttpServletResponse。然而,可以使用多个 Filter 来完成如下工作。

  • 防止下游的 Filter 或 Servlet 被调用。在这种阻断请求的情况下,Filter 通常会使用 HttpServletResponse 对客户端写入响应内容。
  • 修改下游的 Filter 和 Servlet 所使用的 HttpServletRequest 或 HttpServletResponse

DelegatingFilterProxy

DelegatingFilterProxy :Spring Security 对 Servlet 的支持是基于Servlet Filter的,而DelegatingFilterProxy 就是Spring Security的Filter实现。

DelegatingFilterProxy允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet容器允许通过使用自己的标准来注册 Filter 实例,但Servlet容器不知道 Spring 定义的 Bean。因此大多数情况下我们通过标准的Servlet容器机制来注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter 的Spring Bean。

Spring Security会自动向Servlet容器机制注册 DelegatingFilterProxy ,无需我们手动去注册

FilterChainProxy

FilterChainProxy :是 Spring Security 提供的一个特殊的 Filter,允许通过 SecurityFilterChain 委托给许多 Filter 实例。由于 FilterChainProxy 是一个Spring Bean,因此它被包含在 DelegatingFilterProxy 中。

SecurityFilterChain

SecurityFilterChain :是FilterChainProxy用来确定当前请求应该调用哪些Spring Security Filter 实例的过滤器链。

SecurityFilterChain 中的 Security Filter 一般都是Spring Bean,但这些Security Filter是用 FilterChainProxy 进行注册,而不是通过DelegatingFilterProxy注册。与直接向Servlet容器或 DelegatingFilterProxy 注册相比,FilterChainProxy 有很多优势。

  • 首先,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以处理一些必须要做的事情。 例如:

    • 清除 SecurityContext 以避免内存泄漏。
    • 应用Spring Security的 HttpFirewall 来保护应用程序免受某些类型的攻击。
  • 其次,它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在Servlet容器中,Filter 实例仅基于URL被调用。 然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。

图的右边部分是存在多个SecurityFilterChain, FilterChainProxy 的匹配策略则是匹配第一个满足的 SecurityFilterChain

比如,请求的URL是 /api/messages/,它首先与 /api/** 的 SecurityFilterChain 0 模式匹配,所以只有 SecurityFilterChain0 被调用;虽然它也与 SecurityFilterChain n 匹配。

如果请求的URL是 /messages/,它与 /api/** 的 SecurityFilterChain 0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。如果没有其他 SecurityFilterChain 实例相匹配,则调用 SecurityFilterChain n

SecurityFilter

SecurityFilter:是指通过SecurityFilterChain 插入 FilterChainProxy 中的 Filter
这些 Filter 可以用于许多不同的目的,如 认证、 授权、 漏洞保护等。Filter 是按照特定的顺序执行的,以保证它们在正确的时间被调用。

例如,执行认证的 Filter 应该在执行授权的 Filter 之前被调用。如果想要知道 Spring Security 的 Filter 的顺序,可以查看 FilterOrderRegistration 源码。

如果想查看你应用中注册了哪些SecurityFilter 的话可以将org.springframework.security的日志级别调到info,这样在你应用启动的时候就会在控制台打印出当前应用注册的所有SecurityFilter 。效果如下:

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

至此,Spring Security官方架构图中涉及的组件就基本介绍完了,大家先对整体架构和执行流程有一个了解,只有先了解了整体架构,才方便接下来我们去理解Spring Security是如何去实现认证和授权的。

常用Spring Security开启的SecurityFilter

  • CsrfFilter:防止Csrf攻击的SecurityFilter
  • AuthorizationFilter:授权SecurityFilter
  • ExceptionTranslationFilter:处理认证和授权异常的SecurityFilter

异常处理

Spring Security中有一个ExceptionTranslationFilter ,ExceptionTranslationFilter 作为 Security Filter 之一被插入到 FilterChainProxy 中。

ExceptionTranslationFilter可以处理AuthenticationException或AccessDeniedException,其逻辑大概是这样:

try {
 filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
 if (!authenticated || ex instanceof AuthenticationException) {
  startAuthentication();
 } else {
  accessDenied();
 }
}

这段代码的逻辑大致就是,拦截AccessDeniedException 或 AuthenticationException,如果不是这两个异常则不处理。

ExceptionTranslationFilter流程如下:


因此如果我们想自己处理AuthenticationException或者AccessDeniedException,分别实现AuthenticationEntryPoint或者AccessDeniedHandler 即可

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("AccessDeniedException 请求URI:{}", request.getRequestURI(), accessDeniedException);
        response.setCharacterEncoding("UTF-8");
        HashMap<String, String> result = new HashMap();
        result.put("code", "401");
        result.put("message", "权限不足");
        // 处理没有权限的错误错误
        response.getWriter().write(JsonUtil.toString(result));
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error("AccessDeniedException 请求URI:{}", request.getRequestURI(), authException);
        response.setCharacterEncoding("UTF-8");
        // 处理认证失败的错误
        HashMap<String, String> result = new HashMap();
        result.put("code", "500");
        result.put("message", "用户名或密码错误");
        // 处理没有权限的错误错误
        response.getWriter().write(JsonUtil.toString(result));
    }
}
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              AuthenticationEntryPoint authenticationEntryPoint,
                                              AccessDeniedHandler accessDeniedHandler) throws Exception {
        // 配置异常处理
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        return httpSecurity.build();
    }
 }

上面是Spring Security对于认证和鉴权异常的处理机制,但是如果我们自定义了一个Filter。如果这个Filter抛出异常,Spring的全局异常处理机制是无法处理的(原因自行搜索)。所以我们还需要自己做一个Filter异常的处理流程。

首先,我们自定义一个Filter,要在FilterChain中的位置比较靠前,没有其他逻辑就是拦截后面filter抛出的异常,然后转发到指定Controller去处理,然后再用全局异常去处理Filter抛出的异常。

@Component
public class ExceptionFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try{
            filterChain.doFilter(request, response);
        }catch (Exception e){
            // 将异常信息写入请求
            request.setAttribute("filterException", e);
            // 重定向到处理异常的controller
            request.getRequestDispatcher("/exception").forward(request, response);
        }
    }
}
@RestController
public class ExceptionController {

    @RequestMapping("/exception")
    public void handleException(HttpServletRequest request) throws Exception {
        Object attribute = request.getAttribute("filterException");
        if (ObjectUtil.isNotEmpty(attribute) && attribute instanceof Exception e){
            throw e;
        }
    }
}
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              ExceptionFilter exceptionFilter) throws Exception {
        // 配置异常处理过滤器 
        // 这里我们配置在ExceptionTranslationFilter之前 让ExceptionTranslationFilter先处理AuthenticationException或者AccessDeniedException
     // 剩下的其他Exception再交由我们自定义的ExceptionFilter处理
        httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
        return httpSecurity.build();
    }
 }

认证

上面我们已经把Spring Security整体流程讲完了,接下来我们就看一下具体认证的流程是怎样的。Spring Security有提供一套基于标准页面的流程,但是不适用于基于前后端分离的开发模式。地址给大家贴这,有需要的可自行去看看:认证 :: Spring Security Reference (springdoc.cn)

接下来介绍基于前后端分离的流程:


  1. 先配置AuthenticationManager(认证管理器),其中最常用的AuthenticationManager实现是ProviderManager
@Configuration
public class SecurityConfig {

   /**
     * 配置AuthenticationManager(认证管理器)
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
       // ProviderManager 是 AuthenticationManager 最常用的实现
        return new ProviderManager(authenticationProvider);
    }
}
  1. 配置AuthenticationProvider,这里我们的认证方案是使用数据库存储的密码和登录请求的密码进行匹配验证,因此我们选择DaoAuthenticationProvider 。DaoAuthenticationProvider 是一个 AuthenticationProvider 的实现,它使用 UserDetailsService 和 PasswordEncoder 来验证一个用户名和密码。
@Configuration
public class SecurityConfig {

   /**
     * 配置PasswordEncoder(密码编码器)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置AuthenticationProvider
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, 
                                   PasswordEncoder passwordEncoder){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return daoAuthenticationProvider;
    }
}
  1. 配置UserDetails 和UserDetailsService
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Setter
@EqualsAndHashCode
@ToString
public class SecurityUserDetail implements UserDetails {

    @Getter
    private Long userId;

    private String userName;

    private String password;

    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

import lombok.RequiredArgsConstructor;
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;

@Service
@RequiredArgsConstructor
public class AuthUserDetailsService implements UserDetailsService {

    private final UserService userService ;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user= userService .getValidUser(username);
        Assert.notEmpty(user, "用户名或密码错误");
        SecurityUserDetail userDetail = new SecurityUserDetail();
        userDetail.setUserId(user.getId());
        userDetail.setUserName(user.getUserName());
        userDetail.setPassword(user.getPassword());
        return userDetail;
    }
}

  1. LoginController提供登录接口,伪代码如下:
@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController{

    private final AuthenticationManager authenticationManager;

    public Response<LoginVO> login(LoginAO loginAo) {
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginAo.getKey(), loginAo.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        Assert.notEmpty(authenticate, "用户名或密码错误");
        if (authenticate.getPrincipal() instanceof SecurityUserDetail userDetail){
            User user = User.builder().id(userDetail.getUserId()).userName(userDetail.getUsername()).build();
            String token = JwtUtil.generateToken(user);
            // 设置上下文
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, null);
            // 设置子线程支持从父线程获取用户上下文 注意使用ForkJoinPool无法生效
            // SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return Response.success(LoginVO.builder().token(token).build());
        }else {
            log.error("登录异常,从上下文获取用户信息失败,authenticate:{}", JsonUtil.toString(authenticate));
            return Response.fail(null);
        }
    }
}

授权

本文这里分享两种主流的Spring Security授权方式,一种是基于注解的方式,一种是基于配置的方式。

基于注解的授权校验

基于注解的方式校验授权,是通过Spring aop实现的,其流程如下:


首先,我们要开启注解鉴权的功能

@Configuration
/**
 * 开启基于注解的方式控制权限
 */
@EnableMethodSecurity
public class SecurityConfig {

}

然后在需要鉴权的方法上添加权限注解

@RestController
public class UserController{

  private final UserService userService;

    @PreAuthorize("hasAuthority('sys:user:page')")
    public Response<IPage<UserVO>> page(QueryUserPageParam param) {
        return Response.success(userService.page(param));
    }
}

Spring Security的常用权限注解有:

  • @PreAuthorize:前置校验权限,在方法执行之前校验权限,支持Spel表达式
  • @PostAuthorize:后置权限校验,在方法执行结束以后进行校验,可以对返回结果进行校验,支持Spel表达式
  • @PreFilter:对方法参数进行过滤
  • @PostFilter:对方法结果进行过滤

具体每个权限注解的使用方式可以自行去官网学习,这里就不具体介绍了。

下面就以最常用的@PreAuthorize注解为例,介绍一下Spring Security基于注解鉴权的流程与原理:

  1. AuthorizationManagerBeforeMethodInterceptor (授权管理前置方法拦截器),会将权限注解与AuthorizationManager (授权管理器)进行关联及初始化


  2. AuthorizationManagerBeforeMethodInterceptor拦截器拦截到请求后,会根据权限注解@PostAuthorize调用匹配的PreAuthorizeAuthorizationManager#check 方法,并从SecurityContextHolder上下文中获取Authentication对象,将Supplier<Authentication> 和 MethodInvocation作为参数传递给PreAuthorizeAuthorizationManager#check 方法。




  1. AuthorizationManager授权管理器使用 MethodSecurityExpressionHandler 解析@PostAuthorize注解的 SpEL 表达式,并从包含 Supplier<Authentication> 和 MethodInvocation 的 MethodSecurityExpressionRoot 构建相应的 EvaluationContext


  2. 然后从 Supplier 读取Authentication,并检查其权限集合中是否有 sys:user:page


  3. 如果校验通过,将继续调用业务方法。如果校验不通过,会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedExceptionExceptionTranslationFilter 会捕获并处理。

基于配置的授权校验

基于配置的授权校验是通过AuthorizationFilter 实现的,首先我们需要配置授权校验规则:

@Configuration
public class SecurityConfig {

  /**
     * 不需要校验权限的资源
     */
    public static final String[] PERMIT_URL = new String[]{
            // knife4j 资源
            "/doc.html",
            "/favicon.ico",
            "/swagger-resources",
            "/v3/**",
            "/webjars/**",
            // 监控接口
            "/actuator/**",
            // 登录接口
           "/login",
            // 注册接口
           "/register",
    };

    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
                                              TokenFilter tokenFilter,
                                              AuthenticationEntryPoint authenticationEntryPoint,
                                              ExceptionFilter exceptionFilter,
                                              AccessDeniedHandler accessDeniedHandler) throws Exception {
        // 配置token校验过滤器
        httpSecurity.addFilterBefore(tokenFilter, AuthorizationFilter.class);
        // 配置异常处理过滤器
        httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
        // 配置异常处理
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        // 配置授权拦截规则
        httpSecurity.authorizeHttpRequests()
             // 配置放行的规则 这样配置只能在AuthorizationManager#check方法时返回true 如果不经过AuthorizationManager则不生效(比如自定义Filter)
             // 可以通过自定义WebSecurityCustomizer 来达到SecurityFilterChain中的Filter忽略处理 参考下方自定义WebSecurityCustomizer 
             .antMatchers(PERMIT_URL).permitAll()
                .antMatchers("/sys/user/page").hasAuthority("sys:user:page")
                .anyRequest().authenticated();
        // 配置组件 基于JWT认证 因此禁用csrf
        httpSecurity.csrf().disable();
        // 基于JWT认证 因此禁用session
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用缓存
        httpSecurity.headers().cacheControl().disable();
        // 允许跨域
        httpSecurity.cors();
        return httpSecurity.build();
    }

    /**
    自定义WebSecurityCustomizer忽略指定路径 但是Spring Security不建议这么做 建议通过antMatchers(PERMIT_URL).permitAll()实现 
    如果想使用此方式 只需要将其注册为Spring Bean即可
    */
    // @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        // 配置放行规则
        return customizer -> customizer.ignoring().antMatchers(PERMIT_URL);
    }
}

这里咱们还是以前置校验是否拥有某个权限为例:

// 配置授权拦截规则 校验
httpSecurity.authorizeHttpRequests()
                .antMatchers("/sys/user/page").hasAuthority("sys:user:page")
                .anyRequest().authenticated();


这里初始化设置了AuthorityAuthorizationManager 作为AuthorizationManager 的实现,不同的权限校验功能可能对应的AuthorizationManager实现会不一样,比如anyRequest().authenticated() 对应的AuthorizationManager 实现则是AuthenticatedAuthorizationManager


AuthorizationFilter 执行的时候,会根据配置的授权规则找到对应的AuthorizationManager 实现,然后执行check 方法,并从SecurityContextHolder上下文中获取Authentication对象,将Authentication 和 request作为参数传递给AuthorizationManager#check 方法。 这里根据上面的规则hasAuthority() 对应的AuthorizationManager 实现就是AuthorityAuthorizationManager




然后AuthorityAuthorizationManager 会根据SecurityContextHolder的Authentication 中获取所有权限和配置需要的权限进行对比,如果用户上下文SecurityContextHolder中存储的权限集合包含配置需要的权限则返回true通过,反之则返回false。

注意事项

  1. 需要特别说明的是,Spring Security存储角色和权限都是使用的GrantedAuthority 对象,因此Spring Security规定角色需要加上统一前缀方便与权限区分开

这个统一前缀默认为ROLE_ ,无论是基于配置还是基于注解的授权校验都是同样的规则。


当然你也可以自定义这个前缀,只需要将自定义的GrantedAuthorityDefaults 对象注册进Spring容器即可。

@Configuration
public class SecurityConfig {

    /**
     * 配置Role前缀
     * @return
     */
    @Bean
    static GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("ROLE_");
    }
}

  1. 细心的朋友可能已经发现了,无论是基于注解还是基于配置的授权校验,都是从用户上下文SecurityContextHolder中获取当前用户拥有的角色和权限,然后再和需要的权限去比较是否拥有权限。所以我们需要在授权校验之前需要往用户上下文SecurityContextHolder中设置当前用户所拥有的权限。这里就需要用到自定义Filter了。

自定义Filter

如果Spring Security中的SecurityFilter 不能满足你的业务需求,需要自定义SecurityFilter 。比如我们需要自定义一个Filter用于解析请求的Token,然后从Token中获取用户信息和权限。自定义SecurityFilter有两种方式:

  1. 自定义SecurityFilter 实现jakarta.servlet.Filter,在doFilter方法中实现自己的业务逻辑,参考案例:
public class TokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String token= request.getHeader("Authorization");
        boolean hasAccess = checkToken(token);
        if (hasAccess) {
            filterChain.doFilter(request, response);
            return;
        }
        // 注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理 如果是其他异常需要自己处理 Springboot全局异常无法处理
        throw new AuthenticationException("权限不足");
    }

}

然后将该SecurityFilter注册进SecurityFilterChain

@Configuration
public class SecurityConfig {

 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
     http.addFilterBefore(new TokenFilter(), AuthorizationFilter.class);
     return http.build();
 }
}

注意,如果想把jakarta.servlet.Filter的实现注册为Spring Bean,这可能会导致 filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,而且顺序不同。可以通过声明 FilterRegistrationBean Bean 并将其 enabled 属性设置为false来告诉 Spring Boot不要向容器注册它。配置如下:

@Bean
public FilterRegistrationBean<TokenFilter> tenantFilterRegistration(TokenFilter filter) {
    FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}
  1. 自定义SecurityFilter继承OncePerRequestFilter,这样能保证每个请求只会调用一次的filter(「推荐方式」),然后将该SecurityFilter注册进SecurityFilterChain
@Component
public class TokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        // 如果时不需要授权的URI直接放行
        if (!AntPathUtil.match(requestURI, SecurityConfig.PERMIT_URL)){
          String token = request.getHeader("Authorization");
          if (StrUtil.isBlank(token)){
            // 注意AuthenticationException或AccessDeniedException会被ExceptionTranslationFilter处理 如果是其他异常需要自己处理 Springboot全局异常无法处理
            // AuthenticationCredentialsNotFoundException 是 AuthenticationException的子类
              throw new AuthenticationCredentialsNotFoundException("请先登录");
          }
          // 这里是伪代码 逻辑就是通过token解析出用户信息 然后查询出用户所有角色和权限
          User user = parse(token);
          List<GrantedAuthority> authorities = listUserAllPermissions(user.getId());
          PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(user, null, authorities);
          // 设置子线程支持从父线程获取用户上下文 注意使用ForkJoinPool无法生效 如果是线程池可能导致数据错误 谨慎使用
          SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
          // 设置上下文
          SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        // 放行
        filterChain.doFilter(request, response);
    }
}
public class AntPathUtil {

    private AntPathUtil(){}

    public static final AntPathMatcher MATCHER = createMatcher();

    public static boolean match(String path, String... pattern){
        if (ArrayUtil.isEmpty(pattern)){
            return true;
        }
        if (StrUtil.isBlank(path)){
            return false;
        }
        return Arrays.stream(pattern).filter(p -> MATCHER.match(p, path)).findAny().isPresent();
    }

    private static AntPathMatcher createMatcher(){
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        antPathMatcher.setCaseSensitive(false);
        return antPathMatcher;
    }
}

然后将该SecurityFilter注册进SecurityFilterChain

@Configuration
public class SecurityConfig {
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http, TokenFilter tokenFilter) throws Exception {
     http.addFilterBefore(tokenFilter, AuthorizationFilter.class);
     return http.build();
 }
}

总结

最后总结Spring Security的认证和授权的流程如下:


梳理一下上面的流程:

  1. 首先,用户携带用户名密码通过LoginController进行登录(认证流程),如果登录成功则返回token(推荐使用JWT作为token)
  2. 后续其他请求,携带通过登录获取得到的token,然后先被TokenFilter解析token获取用户信息,并将用户信息写入SecurityContextHolder
  3. 然后进行授权流程
  4. AuthorizationManager 通过从SecurityContextHolder获取到当前用户的authentication(权限集合),然后与需要的权限进行对比,从而校验当前用户是否有权限使用当前业务功能

本文使用 markdown.com.cn 排版

SpringSecurity认证和授权流程详解的更多相关文章

  1. oauth2.0的授权流程详解

    授权模式 1)oauth2.0 提供了四种授权模式,开发者可以根据自己的业务情况自由选择. 授权码授权模式(Authorization Code Grant) 隐式授权模式(简化模式)(Implici ...

  2. Shrio授权验证详解

    所谓授权,就是控制你是否能访问某个资源,比如说,你可以方位page文件夹下的jsp页面,但是不可以访问page文件夹下的admin文件夹下的jsp页面. 在授权中,有三个核心元素:权限,角色,用户. ...

  3. 转 OAuth 2.0授权协议详解

    http://www.jb51.net/article/54948.htm 作者:阮一峰 字体:[增加 减小] 类型:转载 时间:2014-09-10我要评论 这篇文章主要介绍了OAuth 2.0授权 ...

  4. Linux启动流程详解【转载】

    在BIOS阶段,计算机的行为基本上被写死了,可以做的事情并不多:一般就是通电.BIOS.主引导记录.操作系统这四步.所以我们一般认为加载内核是linux启动流程的第一步. 第一步.加载内核 操作系统接 ...

  5. C++的性能C#的产能?! - .Net Native 系列《二》:.NET Native开发流程详解

    之前一文<c++的性能, c#的产能?!鱼和熊掌可以兼得,.NET NATIVE初窥> 获得很多朋友支持和鼓励,也更让我坚定做这项技术的推广者,希望能让更多的朋友了解这项技术,于是先从官方 ...

  6. [nRF51822] 5、 霸屏了——详解nRF51 SDK中的GPIOTE(从GPIO电平变化到产生中断事件的流程详解)

    :由于在大多数情况下GPIO的状态变化都会触发应用程序执行一些动作.为了方便nRF51官方把该流程封装成了GPIOTE,全称:The GPIO Tasks and Events (GPIOTE) . ...

  7. 迅为4412开发板Linux驱动教程——总线_设备_驱动注册流程详解

    本文转自:http://www.topeetboard.com 视频下载地址: 驱动注册:http://pan.baidu.com/s/1i34HcDB 设备注册:http://pan.baidu.c ...

  8. iOS 组件化流程详解(git创建流程)

    [链接]组件化流程详解(一)https://www.jianshu.com/p/2deca619ff7e

  9. git概念及工作流程详解

    git概念及工作流程详解 既然我们已经把gitlab安装完毕[当然这是非必要条件],我们就可以使用git来管理自己的项目了,前文也多多少少提及到git的基本命令,本文就先简单对比下SVN与git的区别 ...

  10. Lucene系列六:Lucene搜索详解(Lucene搜索流程详解、搜索核心API详解、基本查询详解、QueryParser详解)

    一.搜索流程详解 1. 先看一下Lucene的架构图 由图可知搜索的过程如下: 用户输入搜索的关键字.对关键字进行分词.根据分词结果去索引库里面找到对应的文章id.根据文章id找到对应的文章 2. L ...

随机推荐

  1. 搭建Windows环境下的多功能免费SSH客户端

    关于Windows下的SSH客户端工具,可以有许多选择,从开源免费到商业收费的,零零总总. 免费版: Putty就是最简单的SSH客户端,非常轻量级. Electerm是一个开源可免费使用的跨平台SS ...

  2. python模块imghdr-----推测图像类型

    官方文档 https://docs.python.org/zh-cn/3/library/imghdr.html#module-imghdr 用处 模块推测文件或字节流中的图像的类型 imghdr.w ...

  3. vue 项目npm run dev(启动)时报错The service was stopped

    vue项目yarn upgrade后vite build报错,如何项目也运行不起来了. 报错截图: 解决办法: 删除node_modules文件夹,然后执行yarn install重新生成心的node ...

  4. 【LeetCode二叉树#18】修剪二叉搜索树(涉及重构二叉树与递归回溯)

    修剪二叉搜索树 力扣题目链接(opens new window) 给定一个二叉搜索树,同时给定最小边界L 和最大边界 R.通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) .你 ...

  5. 框架和MVC架构

    网络框架及MVC架构 网络框架 所谓网络框架是指这样的一组Python包,它能够使开发者专注于网站应用业务逻辑的开发,而无须处理网络应用底层的协议.线程.进程等方面.这样能大大提高开发者的工作效率,同 ...

  6. 面试官:说说SSO单点登录的实现原理?

    单点登录(Single Sign-On, SSO)是一种让用户在多个应用系统之间只需登录一次就可以访问所有授权系统的机制.单点登录主要目的是为了提高用户体验并简化安全管理. 举个例子,您在一个大型企业 ...

  7. CSRF(Steam的链接不用随便点)

    漏洞详解 CSRF 漏洞原理: 攻击者会冒充或利用用户本人对web服务器发送请求,然而web服务器无法识别该请求是否为用户本人所发送,因此造成各种危害. 漏洞利用过程: 1)首先需要用户登录了上网站, ...

  8. RocketMQ(1) 基础介绍和单机-集群安装

    1. MQ简单介绍 1.1 应用场景 应用解耦 系统的耦合性越高,容错性就越低.以电商应用为例,用户创建订单后,如果耦合调用库存系统.物流系统.支付系统,任何一个子系统出了故障或者因为升级等原因暂时不 ...

  9. 学习ASP.NET Core Razor 编程系列文章目录

    学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二--添加一个实体 学习ASP.NET Core Razor 编程系列三--创建数据表及创建项目 ...

  10. 学习笔记-涛讲F#(基础 II)

    目录 处理一堆数 组织代码(命名空间.模块) 使用联合重命名类型 类必须显式转换成接口 对象表达式 递归函数 CPS解决堆栈溢出 扩展一个类型 静态解析的类型参数 ref变量的实现原理及应用 F#资源 ...