Files
rikako-note/spring/Spring Security/Spring Security.md
2022-10-09 23:26:14 +08:00

338 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spring Security
## Spring Security简介
Spring Security作为一个安全框架向使用者提供了用户认证、授权、常规攻击保护等功能。
## Spring Security自动配置
默认情况下在引入Spring Security的启动器依赖之后Spring Boot自动配置会做如下工作
- 启用Spring Security的默认配置创建一个的bean对象bean对象名称为“springSecurityFilterChain”bean对象类型为SecurityFilterChain实现了Filter。该bean对象为负责应用中所有与安全相关的操作例如验证提交的username和password保护应用的url等
- 创建一个UserDetailsService的bean对象并且产生一个为”user“的username和一个随机产生的password随机产生的password会输出在console日志中
- 将名为springSecurityFilterChain的bean对象注册到servlet容器中用来对每个servlet请求进行处理
> Spring Security会通过Spring Security会通过BCtyptHash解密算法来对密码的存储进行保护
## Spring Security结构
### DelegatingFilterProxy
Spring提供了Filter的一个实现类DelegatingFilterProxy其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。
***DelegatingFilterProxy会通过标准的servlet容器机制被注册但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。
> DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象并且调用该bean对象的doFilter方法
> ```java
> public void doFilter(ServletRequest request,
> ServletResponse response, FilterChain chain) {
> // Lazily get Filter that was registered as a Spring Bean
> // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
> Filter delegate = getFilterBean(someBeanName);
> // delegate work to the Spring Bean
> delegate.doFilter(request, response);
> }
> ```
### FilterChainProxy
Spring Security支持FilterChainProxyFilterChainProxy是一个由Spring Security提供的特殊FilterFilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。
***FilterChainProxy是一个bean对象通过被包含在DelegatingFilterProxy中。***
### SecurityFilterChain
SecurityFilterChain通常被FilterChainProxy使用用来决定在该次请求中调用那些Spring Security Filters。
### SecurityFilters
Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。
### 处理Security异常
ExceptionTranslationFilter将认证异常和权限异常翻译为http response。
> ExceptionTranslationFilter被插入到FilterChainProxy中作为SecurityFilterChain中的一个。
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException那么ExceptionTranslationFilter并不会做任何事情。
```java
// ExceptionTranslationFilter的伪代码
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
```
## Spring Security身份认证的结构
### SecurityContextHolder
Spring Security身份认证的核心模型SecurityContextHolder包含有SecurityContext。
> Spring Security将被认证用户的详细信息details存储在SecurityContextHolder中。Spring Security不在乎该SecurityContextHolder是如何被填充的只要该SecurityContextHolder有值那么其将会被用作当前已经被认证的用户。
> ***将用户标识为已认证的最简单的方法是为该用户设置SecurityContextHolder。***
```java
// 设置SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
```
默认情况下SecurityContextHolder通过ThreadLocal来存储SecurityContext故而SecurityContext对于位于同一线程之下的方法来说都可以访问。
> 使用ThreadLocal来存储SecurityContext是相当安全的如果想要在该已认证主体的请求被处理完成之后清除SecurityContextSpring Security中的FilterChainProxy会保证该SecurityContext被清除。
### SecurityContext
SecurityContext从SecurityContextHolder中获得SecurityContext中含有Authentication对象。
### Authentication
Authentication在Spring Security中具有两个目的
- 作为AuthenticationManager的输入用于提供待认证用户的认证凭证。当用于该场景下时isAuthenticated()方法返回值应该为false
- 代表当前已经认证过的用户。当前的Authentication可以从SecurityContext中获取而默认情况下SecurityContext是存储在ThreadLocal中的
Authentication含有如下部分
- 主体principal用于标识用户当通过username/password进行认证时其通常是UserDetails类的实例
- 凭据credentials通常是password在许多场景下凭据会在用户认证成功之后被清空为了保证凭据不会被泄露
- 权限authorities该GrantedAuthority集合是用户被授予的高层次许可。许可通常是用户角色或者作用域范围。
### GrantedAuthority
GrantedAuthority是用户被授予的高层次许可譬如用户角色或者作用域范围。
GrantedAuthority可以通过Authentication.getAuthorities()方法来获得该方法会返回一个GrantedAuthentication的集合。每个GrantedAuthentication都是一项被授予该用户的权限。
### AuthenticationManager
AuthenticationManager的API定义了Security Filters如何来执行身份认证。对于身份认证返回的Authentication会被调用AuthenticationManager的controller设置到SecurityContextHolder中。
> AuthenticationManager的实现可以是任何类但是最通用的实现仍然是ProviderManager
### ProviderManager
ProviderManager是AuthenticationManager的最通用实现。ProviderManager将工作委托给一系列AuthenticationProvider。
> 对于每个ProviderManager都可以决定将该认证标识为成功、失败或者将认证工作委托给下游AuthenticationProvider。
> 如果所有的AuthenticationProvider都没有将该认证标识为成功或者失败那么整个认证流程失败并且抛出ProviderNotFoundException异常。
> ProviderNotFoundException是一个特殊的AuthenticationException该异常代表对传入Authentication的类型并没有配置该类型的ProviderManager
> 在实践中每个AuthenticationProvider知道如何处理一种特定类型的Authentication
默认情况下ProviderManager在认证请求成功后会尝试清除返回的Authentication对象中任何敏感的凭据信息这将会保证password等敏感信息保存时间尽可能地短减少泄露的风险。
### AuthenticationProvider
复数个AuthenticationProvider可以被注入到ProviderManager中每个AuthenticationProvider可以负责一种专门类型的认证例如DaoAuthenticationProvider负责username/password认证JwtAuthenticationProvider负责jwt token的认证。
### AuthenticationEntryPoint
AuthenticationEntryPoint用来发送一个Http Response用来向客户端请求认证凭据。
某些情况下客户端在请求资源时会主动在请求中包含凭据如username/password等。在这种情况下服务端并不需要再专门发送Http Response来向客户端请求认证凭据。
> AuthenticationEntryPoint用来向客户端请求认证凭据AuthenticationEntryPoint的实现可能会执行一个从定向操作将请求重定向到一个登录页面用于获取凭据并且返回一个WWW-Authentication Header。
### AbstractAuthenticationProcessingFilter
该类作为base filter用来对用户的凭据进行认证。再凭据被认证之前Spring Security通常会通过AuthenticationEntryPoint向客户端请求认证凭据。
之后AbstractAuthenticationProcessingFilter会对提交的任何认证请求进行认证。
#### 认证流程
1. 当用户提交认证凭据之后AbstractAuthenticationProcesingFilter会根据HttpServletRequest对象创建一个Authentication对象用于认证该Authentication的类型取决于AbstractAuthenticationProcessingFilter的子类类型。例如UsernamePasswordAuthenticationFilter会通过提交request中的username和password创建UsernamePasswordAuthenticationToken。
2. 然后将构建产生的Authentication对象传入到AuthenticationManager中进行认证
3. 如果认证失败那么会失败SecurityContextHolder会被清空RememberMeService.logFail方法将会被调用AuthenticationFailureHandler也会被调用
4. 如果认证成功那么SessionAuthenticationStrategy将会收到登录的通知
5. 该Authentication对象再认证成功之后将会被设置到SecurityContextHolder中
6. RemeberMeService.logSuccess将会被调用
7. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent.
8. AuthenticationSuccessHandler被调用
## 用户名/密码认证
### Form Login
Spring Security为以表单形式提供的用户名和密码认证提供支持。
#### 用户被重定向到登录页面的过程
1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的
2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException代表该未授权的请求被拒绝
3. 因为该用户没有经过认证故而ExceptionTransactionFilter发起了开始认证的过程并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint
4. 浏览器接下来会请求重定向到的登陆页面
当用户名和密码提交后UsernamePasswordAuthenticationFilter会对username和password进行认证。UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter。
#### 认证用户名和密码过程
1. 当用户提交了其用户名和密码之后UsernamePasswordAuthenticationFilter会创建一个UsernamePasswordAuthenticationToken
2. 创建的UsernamePasswordAuthenticationToken会传入AuthenticationManager中进行认证
3. 如果认证失败那么SecurityContextHolder会被清除RememberMeService.logFailure和AuthenticationFailureHandler会被调用
4. 如果认证成功那么SessionAuthenticationStrategy将会收到登录的通知RemeberMeService.logSuccess和AuthenticationSuccessHandler会被调用ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent
Spring Security Form Login默认情况下是开启的但是一旦任何基于servlet的配置被提供那么基于表单的login也必须要显式指定。
```java
// 显式指定form login的配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(withDefaults());
// ...
}
```
如果想要自定义login form page可以使用如下配置
```java
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
// ...
}
```
### 基本Authentication
#### 基本Authentication的认证流程
1. 用户向私有资源发送未认证请求,其中对私有资源的访问并没有被授权
2. SpringSecurity的FilterSecurityInterceptor表明该未认证的请求被拒绝访问抛出AccessDeniedException
3. 由于该请求没有经过身份认证故而ExceptionTranslationFilter启动身份认证被配置好的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint类的实例该实例会发送WWW-Authentication的header。RequestCache通常是一个NullRequestCache不会保存任何的http request请求因为客户端能够重新发送其原来发送过的请求。
4. 当客户端获取到WWW-Authentication的header客户端会知道其接下来会通过username和password重新尝试重新发送http请求。
默认情况下basic authentication是被开启的。但是如果有任何基于基于servlet的配置被提供那么必须通过如下方式显式开启basic authentication。
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
return http.build();
}
```
### Digest Authentication(摘要认证,***不安全***)
在目前不应该在现代应用程序中使用Digest Authentication因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储MD5已经被证实不安全。相对的应该使用单向的密码散列如bCrypt, PBKDF2, SCrypt来存储认证凭证但是这些都不被Digest Authentication所支持。
> 摘要认证主要用来解决Basic Authentication中存在的问题摘要认证确保了认证凭证在网络上不会以明文的方式传输。
> 如果想要使用非https的方式并且最大限度的加强认证过程那么可以考虑使用Digest Authentication。
#### 摘要认证中的随机数
摘要认证中的核心是随机数该随机数的值由服务端产生Spring Security中随机数次啊用如下格式
```txt
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nonce token
```
需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。
```java
@Autowired
UserDetailsService userDetailsService;
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}
DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
return http.build();
}
```
## 密码存储方式
### 内存中存储密码
Spring Security中InMemoryUserDetailsManager实现了UserDetailsService用于向基于存储在内存中的密码认证提供支持。
InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。
InMemoryUserDetailsManager可以通过如下方式进行配置
```java
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
```
#### 内存中存储密码时使用defaultPasswordEncoder
***通过defaultPasswordEncoder来指定密码编码器时无法防止通过反编译字节码来获取密码的攻击。***
```java
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
```
### JDBC Authentication
Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。
Spring Security为基于jdbc的认证提供了默认的查询语句。
#### User Schema
JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下
```sql
# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
# 用户表需要提供usernamepassword、用户状态
# 权限表需要提供用户名和权限名称
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
```
#### Group Schema
如果你的程序中使用了Group那么还额外需要一张group的表默认如下
```sql
# 如果要为group配置权限,需要引入三张表,group表,权限表和group_member表
create table groups (
id bigint auto_increment primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint auto_increment primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
```
#### 配置Datasource
```java
// 生产环境时,应该通过对外部数据库的连接来建立数据源
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build();
}
```
#### 创建JdbcUserDetailsManager Bean对象
```java
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}
```