likes
comments
collection
share

Spring Security 5.7总结

作者站长头像
站长
· 阅读数 14

Spring Security 5.7总结

概述

Spring Security是一个提供身份认证,授权和常见攻击防御框架,为保护式和反应式应用程序保护提供一流的支持,它是保护基于Spring应用程序的事实上的标准。

架构

Spring Security是一个过滤器链,每个Filter执行特定的功能。引用官方图,过滤器链通过DelegatingFilterProxy类与Servlet容器桥接建立连接,DelegatingFilterProxy封装一个FilterChainProxy类,FilterChainProxy是Spring Security提供的一个特殊过滤器,允许通过SecurityFilterChain将其委托给许多过滤器实例。

Spring Security 5.7总结

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

Spring Security 5.7总结

  • 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 5.7总结

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

以上的的Filter会根据SecurityFilterChain的配置参数来创建对应的Filter。

关于SecurityFilterChain

默认情况下的SecurityFilterChain也就是导入下方依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

而不做其他配置所创建的Filter如下图所示:

Spring Security 5.7总结

默认HttpSecurity的配置在HttpSecurityConfiguration中如下图: Spring Security 5.7总结

从图中可知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,那么将会创建DefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilter过滤器。其中DefaultLoginPageGeneratingFilter中的generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess)会生成登录页面的内容也就是如下图:

Spring Security 5.7总结

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

Spring Security 5.7总结

可以在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相关实现类如图: Spring Security 5.7总结

关于ExceptionTranslationFilterFilterSecurityInterceptor以及AuthorizationFilter

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

Spring Security 5.7总结

FilterSecurityInterceptorAuthorizationFilter都是用于控制访问权限的过滤器,它们是位于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 身份验证

类、接口描述
SecurityContextHolderSpring Security存储身份验证详细信息地方
SecurityContextSecuritycontexholder中获得包含当前认证用户的身份验证。
AuthenticationSecurityContext获取当前的身份认证或者由AuthenticationManager输入
GrantedAuthority在身份验证上授予主体权限
AuthenticationManager定义Spring Security的过滤器如何执行Authentication
ProviderManagerAuthenticationManager最常见的一种实现
AuthenticationProviderProviderManager执行特定类型的Authentication
AuthenticationEntryPoint身份验证入口点,在身份验证不成功之后,执行指定操作,如重定向等
AbstractAuthenticationProcessingFilter用于身份验证基本过滤器,UsernamePasswordAuthenticationFilter是其子类

在编写自定义Filter进行自定义的验证中,核心点是以下代码:

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
        UsernamePasswordAuthenticationToken.authenticated(userDetails, 
                userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().
        setAuthentication(usernamePasswordAuthenticationToken);

如果验证成功,则给当前的SecurityContext填充一个AuthenticationUsernamePasswordAuthenticationTokenAuthentication其中一个实现类。 Authentication的实现类如下图:

Spring Security 5.7总结

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";
}

两种方法权限可以相互叠加,这是一种与关系,只有配置权限都匹配了才能访问指定路径下的内容。

hasRolehasAnyRolehasAuthorityhasAnyAuthority方法的区别在与:前两个方法会在指定名称加上前缀,如 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的实现类如下:

Spring Security 5.7总结 所以如果自定义权限前缀请不要使用这个方法,或者重写底层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();
}

rolesauthorities底层都是调用该方法,所以重复调用之前配置的都会被覆盖掉,如果使用了自定义的前缀 ,不要使用roles方法,它只能加上ROLE_前缀

Spring Security 5.7总结

集成使用

项目结构

Spring Security 5.7总结

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);
    }
}

参考文档

Spring Security 5.7官方文档