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
也可以使用构建一个UserDetailsService
Bean进行配置,这将会覆盖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);
}
}