Spring Security 5.7总结
Spring Security 5.7总结
概述
Spring Security是一个提供身份认证,授权和常见攻击防御框架,为保护式和反应式应用程序保护提供一流的支持,它是保护基于Spring应用程序的事实上的标准。
架构
Spring Security是一个过滤器链,每个Filter执行特定的功能。引用官方图,过滤器链通过DelegatingFilterProxy类与Servlet容器桥接建立连接,DelegatingFilterProxy封装一个FilterChainProxy类,FilterChainProxy是Spring Security提供的一个特殊过滤器,允许通过SecurityFilterChain将其委托给许多过滤器实例。

也就是说FilterChainProxy类含有一个SecurityFilterChain集合属性,通过阅读FilterChainProxy类,部分源码如下图:

FilterChainProxy执行它的doFilter方法会执行其私有方法doFilterInternal,接着会执行getFilters方法。getFilters方法会根据请求路径来匹配指定的SecurityFilterChain并返回其拥有的Filter类集合。也就是说,如果多个SecurityFilterChain对于request请求的条件相同的话,只会选择第一个匹配成功的SecurityFilterChain。- 注意: 默认实现是
AnyRequestMatcher,即无论什么请求打过来,都会返回true,所以如果多个SecurityFilterChain实例存在,都只会返回第一个SecurityFilterChain实例,要想不同请求执行不同的SecurityFilterChain,就需要对所有的SecurityFilterChain中的RequestMatcher提供指定的实现类如RegexRequestMatcher - 接下来,创建一个有指定Filter类集合的
VirtualFilterChain实例,由该实例执行这些Filter。VirtualFilterChain类是FilterChainProxy类的私有静态内部最终类,其核心方法如下图: 

以上是一个过滤链执行的部分流程,在使用Spring Security的过程中,主要是对SecurityFilterChain进行配置。
SecurityFilterChain中的Filter的排序顺序是确定的(自定义的除外),以下是官方给出的Filter排列顺序图:

以上的的Filter会根据SecurityFilterChain的配置参数来创建对应的Filter。
关于SecurityFilterChain
默认情况下的SecurityFilterChain也就是导入下方依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
而不做其他配置所创建的Filter如下图所示:

默认HttpSecurity的配置在HttpSecurityConfiguration中如下图:

从图中可知CsrfFilter过滤器默认是开启的,接下来有以下配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
        throws Exception {
    httpSecurity.formLogin().loginPage("sliver-gravel-login.html")
            .and().
            authorizeRequests().antMatchers("/**").authenticated();
    // MyAuthenticationEntryPoint 为 AuthenticationEntryPoint实现类
    httpSecurity.exceptionHandling()
            .authenticationEntryPoint(new MyAuthenticationEntryPoint());
    return httpSecurity.build();
}
formLogin方法会创建一个UsernamePasswordAuthenticationFilter过滤器,formLogin().disable()将不会创建该过滤器;如果不配置loginPage("sliver-gravel-login.html")或者不配置authenticationEntryPoint,只是仅仅配置formLogin,那么将会创建DefaultLoginPageGeneratingFilter和
DefaultLogoutPageGeneratingFilter过滤器。其中DefaultLoginPageGeneratingFilter中的generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess)会生成登录页面的内容也就是如下图:

不配置上述Bean,只使用默认配置,在启动Demo时候,如果不配置用户密码的情况下,默认用户是user,密码为应用启动后控制台输出的密码如下图:

可以在application.yml进行配置:
spring:
  security:
    user:
      name: DawnSilverGravel
      password: DawnSilverGravel
      roles: 
        - DawnSilverGravel
        - admin
也可以使用构建一个UserDetailsServiceBean进行配置,这将会覆盖application.yml中的内容:
@Bean
public UserDetailsService userDetailsService() {
    // withDefaultPasswordEncoder() 在生成环境应禁止使用,因为不安全
    // 在演示和入门阶段是可以接受的。
    // 出于生产目的,请确保密码是外部编码的
    // 但目前还没有要删除该方法的决定
    UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
    System.out.println(user.getPassword());
    return new InMemoryUserDetailsManager(user);
}
UserDetailsService相关实现类如图:

关于ExceptionTranslationFilter、FilterSecurityInterceptor以及AuthorizationFilter
ExceptionTranslationFilter类位于Spring Security过滤器职责链的尾端,用于
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
其通过捕获后续过滤器抛出的异常,然后对其进行处理如下图:

FilterSecurityInterceptor、AuthorizationFilter都是用于控制访问权限的过滤器,它们是位于ExceptionTranslationFilter的下一个过滤器。Spring Security根据配置条件来决定使用其中一个过滤器。下方有以下配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
        throws Exception {
    httpSecurity.authorizeHttpRequests()
            .antMatchers("/authorizationFilter/**").authenticated();
    SecurityFilterChain securityFilterChain = httpSecurity.build();
    securityFilterChain.getFilters().forEach(
            filter -> System.out.println(filter.getClass().getSimpleName())
    );
    return securityFilterChain;
}
@Bean
public SecurityFilterChain securityFilterChain1(HttpSecurity httpSecurity)
        throws Exception {
    httpSecurity.authorizeRequests()
            .antMatchers("/filterSecurityInterceptor/**").authenticated();
    SecurityFilterChain securityFilterChain = httpSecurity.build();
    securityFilterChain.getFilters().forEach(
            filter -> System.out.println(filter.getClass().getSimpleName())
    );
    return securityFilterChain;
}
其中securityFilterChain方法生成的是AuthorizationFilter,securityFilterChain1方法生成的是FilterSecurityInterceptor。区别在于authorizeHttpRequests(带Http)、authorizeRequests(不带Http)。
在Spring Security 6.1.1版本中已经将FilterSecurityInterceptor替换为AuthorizationFilter,官方之后更推荐使用AuthorizationFilter。
关于Authentication 身份验证
| 类、接口 | 描述 | 
|---|---|
SecurityContextHolder | Spring Security存储身份验证详细信息地方 | 
SecurityContext | 从Securitycontexholder中获得包含当前认证用户的身份验证。 | 
Authentication | 从SecurityContext获取当前的身份认证或者由AuthenticationManager输入 | 
GrantedAuthority | 在身份验证上授予主体权限 | 
AuthenticationManager | 定义Spring Security的过滤器如何执行Authentication | 
ProviderManager | AuthenticationManager最常见的一种实现 | 
AuthenticationProvider | 由ProviderManager执行特定类型的Authentication | 
AuthenticationEntryPoint | 身份验证入口点,在身份验证不成功之后,执行指定操作,如重定向等 | 
AbstractAuthenticationProcessingFilter | 用于身份验证基本过滤器,UsernamePasswordAuthenticationFilter是其子类 | 
在编写自定义Filter进行自定义的验证中,核心点是以下代码:
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
        UsernamePasswordAuthenticationToken.authenticated(userDetails, 
                userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().
        setAuthentication(usernamePasswordAuthenticationToken);
如果验证成功,则给当前的SecurityContext填充一个Authentication。UsernamePasswordAuthenticationToken是Authentication其中一个实现类。
Authentication的实现类如下图:

Authentication包含以下内容:
| 方法 | 描述 | 
|---|---|
principal | 用户在使用用户名/密码的情况下,通常是一个UserDetails实例。 | 
credentials | 通常是密码。在许多情况下,这将在用户进行身份验证后被清除,以确保它不会泄漏。 | 
authorities | 用户被授予的高级权限。 | 
关于Authorization 授权
授权的方式的也有好几种,以下有两种常用的配置选项:
- 在
SecurityFilterChain上配置路径设置权限验证 - 在指定方法上加上
@PreAuthorize注解 
SecurityFilterChain配置权限示例如下:
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity)
        throws Exception {
    httpSecurity.formLogin().and().authorizeRequests()
            // /permit 路径下的资源可以直接访问
            .antMatchers("/permit/**").permitAll()
            // /user 路径下的资源需要USER角色
            .antMatchers("/user/**").hasRole("USER")
            // /admin 路径下的资源需要 MY_PREFIX_ADMIN、MY_PREFIX_ADMIN1其中一个权限
            .antMatchers("/admin/**").hasAnyAuthority("MY_PREFIX_ADMIN", "MY_PREFIX_ADMIN1")
            // 其他的路径需要验证
            .anyRequest().authenticated();
    return httpSecurity.build();
}
要使@PreAuthorize注解生效,需要在配置类上加上@EnableMethodSecurity注解或者是 @EnableGlobalMethodSecurity(prePostEnabled = true)注解。其中@EnableMethodSecurity注解是Spring Security 5.5版本才有的,是对@EnableGlobalMethodSecurity的一种增强拓展,所以直接使用新版的即可。@PreAuthorize示例如下:
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER')")
public String getUser() {
    return "user";
}
两种方法权限可以相互叠加,这是一种与关系,只有配置权限都匹配了才能访问指定路径下的内容。
hasRole、hasAnyRole、hasAuthority、hasAnyAuthority方法的区别在与:前两个方法会在指定名称加上前缀,如 USER 会变成 ROLE_USER,ROLE_ 是默认的前缀。如果想自定义前缀,可以在配置类中加上以下Bean:
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("MY_PREFIX_");
}
使用static的目的是确保Spring在初始化 Spring Security 的方法Security@Configuration 类之前发布它。
注意: SecurityFilterChain中的authorizeRequests().antMatchers("/user/**").hasRole("USER")的hasRole的实现类如下:
所以如果自定义权限前缀请不要使用这个方法,或者重写底层access中的
Spring Security配置用户与权限,需要实现UserDetails接口,org.springframework.security.core.userdetails.User是Spring Security提供的其中一个实现类。
使用示例如下:
@Bean
public UserDetailsService userDetailsService() {
    UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("password"))
            // roles和authorities两者不可同时使用,最后一个方法会覆盖前面的方法
            // roles 会在名称加上前缀ROlE_,USER->ROLE_USER
            .roles("USER")
            .authorities("MY_PREFIX_USER", "MY_PREFIX_USER1")
            .build();
    return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
roles和authorities底层都是调用该方法,所以重复调用之前配置的都会被覆盖掉,如果使用了自定义的前缀 ,不要使用roles方法,它只能加上ROLE_前缀

集成使用
项目结构

pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.12</version>
</parent>
<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>
application.yml
spring:
  security:
    # 如果配置了 UserDetailsService Beam,该配置失效
    user:
      name: DawnSilverGravel
      password: DawnSilverGravel
      roles:
        - DawnSilverGravel
        - ADMIN
        - USER
SecurityConfiguration自定义配置
package com.example.config;
import com.example.filter.MyFilter;
import com.example.handler.MyAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/6/23
 */
@Configuration
public class SecurityConfiguration {
    public final static String ROLE_PREFIX = "DAWN_SILVER_GRAVEL_";
    @Bean
    public SecurityFilterChain dawnStarFilterChain(HttpSecurity httpSecurity)
            throws Exception {
        httpSecurity.formLogin()
                .and()
                // 匹配指定路径
                .regexMatcher("^/(dawn-star|login).*")
                // 使用FilterSecurityInterceptor过滤器
                .authorizeRequests()
                .antMatchers("/dawn-star/**")
                .authenticated();
        httpSecurity.exceptionHandling()
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType("application/json");
                    response.setCharacterEncoding("UTF-8");
                    response.getWriter().write("该账号没有权限访问资源!" + accessDeniedException.getMessage());
                    response.getWriter().close();
                });
        return httpSecurity.build();
    }
    @Bean
    public SecurityFilterChain silverGravelFilterChain(HttpSecurity httpSecurity)
            throws Exception {
        httpSecurity.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 匹配指定路径
                .regexMatcher("^/silver-gravel/.*")
                // 使用 AuthenticationFilter
                .authorizeHttpRequests()
                // 允许该路径下的资源直接访问
                .antMatchers("/silver-gravel/permit/**").permitAll()
                // 该路径下的资源需要 USER 角色才能访问
                .antMatchers("/silver-gravel/user/**").hasAuthority(ROLE_PREFIX + "USER")
                // 该路径下的资源需要 ADMIN角色或 ADMIN1 角色才能访问
                .antMatchers("/silver-gravel/admin/**")
                .hasAnyAuthority(ROLE_PREFIX + "ADMIN", ROLE_PREFIX + "ADMIN1")
                // 其他资源需要验证即可访问
                .anyRequest().authenticated();
        httpSecurity.addFilterBefore(myFilter(), UsernamePasswordAuthenticationFilter.class);
        httpSecurity.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("该账号没有权限访问资源!" + accessDeniedException.getMessage());
            response.getWriter().close();
        }).authenticationEntryPoint(new MyAuthenticationEntryPoint());
        return httpSecurity.build();
    }
    /**
     * 注入MyFilter
     */
    @Bean
    public MyFilter myFilter() {
        return new MyFilter();
    }
    /**
     * 注入指定编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 指定前缀
     */
    @Bean
    static GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(ROLE_PREFIX);
    }
    /**
     * 注入用户信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
                .authorities(ROLE_PREFIX + "USER")
                .password(passwordEncoder().encode("password"))
                .username("user")
                .build();
        UserDetails admin = User.builder()
                .authorities(ROLE_PREFIX + "ADMIN")
                .password(passwordEncoder().encode("password"))
                .username("admin")
                .build();
        UserDetails adminAll = User.builder()
                .authorities(ROLE_PREFIX + "ADMIN", ROLE_PREFIX + "ADMIN1")
                .password(passwordEncoder().encode("password"))
                .username("adminAll")
                .build();
        UserDetails adminAndUser = User.builder()
                .authorities(ROLE_PREFIX + "ADMIN", ROLE_PREFIX + "USER")
                .password(passwordEncoder().encode("password"))
                .username("adminAndUser")
                .build();
        return new InMemoryUserDetailsManager(user, admin, adminAll, adminAndUser);
    }
}
Controller配置
DawnStarController控制器
package com.example.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/6/26
 */
@RestController
@RequestMapping("/dawn-star")
public class DawnStarController {
    /**
     * 登录账号
     */
    @GetMapping("/star")
    public String star() {
        return "dawnStar SecurityFilterChain处理的资源";
    }
    /**
     * 登陆user账号
     * http://localhost:8080/login?logout 退出账号
     * 登录admin账号
     */
    @GetMapping("/user")
    @PreAuthorize("hasRole('USER')")
    public String user() {
        return "dawnStar SecurityFilterChain处理的资源,需要MY_PREFIX_USER权限";
    }
    
}
SilverGravelController控制器
package com.example.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/6/26 21:02
 */
@RestController
@RequestMapping("/silver-gravel")
public class SilverGravelController {
    /**
     * http://localhost:8080/silver-gravel/permit
     * @return
     */
    @GetMapping("/permit")
    public String permit() {
        return "permit 路径下该资源无需登录验证即可访问";
    }
    /**
     * http://localhost:8080/silver-gravel/other?username=user&password=password
     * @return
     */
    @GetMapping("/other")
    public String other() {
        return "其他路径下资源需要登录验证才能访问";
    }
    /**
     * http://localhost:8080/silver-gravel/user?username=user&password=password
     * http://localhost:8080/silver-gravel/user?username=admin&password=password
     * @return
     */
    @GetMapping("/user")
    public String user() {
        return "user 路径下资源需要 USER 权限才能访问";
    }
    /**
     * http://localhost:8080/silver-gravel/admin?username=admin&password=password
     * http://localhost:8080/silver-gravel/admin?username=user&password=password
     * @return
     */
    @GetMapping("/admin")
    public String admin() {
        return "admin 路径下资源需要 ADMIN 或 ADMIN1 权限才能访问";
    }
    /**
     * http://localhost:8080/silver-gravel/admin/user?username=admin&password=password
     * http://localhost:8080/silver-gravel/admin/user?username=adminAndUser&password=password
     * @return
     */
    @GetMapping("/admin/user")
    @PreAuthorize("hasAuthority('DAWN_SILVER_GRAVEL_USER')")
    public String adminUser() {
        return "adminUser 资源需要 (ADMIN 或 ADMIN1)且USER 权限才能访问";
    }
    /**
     * http://localhost:8080/silver-gravel/admin/admin1?username=adminAll&password=password
     * http://localhost:8080/silver-gravel/admin/admin1?username=adminAndUser&password=password
     * @return
     */
    @GetMapping("/admin/admin1")
    @PreAuthorize("hasRole('ADMIN1')")
    public String admin1() {
        return "adminUser 资源需要 ADMIN1  权限才能访问";
    }
}
其他配置
MyFilter过滤器
package com.example.filter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/6/26
 */
public class MyFilter extends OncePerRequestFilter {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder encoder;
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // jwt 相关实现差不多
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        if (username != null && password != null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails != null) {
                boolean matches = encoder.matches(password, userDetails.getPassword());
                if (matches) {
                    // 核心点还是这里,设置当前Authentication
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            UsernamePasswordAuthenticationToken.authenticated(username, password, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}
uthenticationEntryPoint
package com.example.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Description:
 *
 * @author DawnStar
 * Date: 2023/6/26
 */
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().write("错误:" + authException.getMessage());
        response.getWriter().close();
    }
}
启动类
@SpringBootApplication
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}
参考文档
转载自:https://juejin.cn/post/7248935299040051256