SpringSecurity 是一个强大的、高度可定制的身份验证和访问控制框架,它是确保基于 Spring 的应用程序安全的事实标准。
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

笔记来源于 https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle

目录

第一部分 基本介绍

  1. 前言
    1. 介绍什么是 SpringSecurity
    2. Spring Security 的几种认证方式
    3. Spring Security 的包目录结构
  2. Spring Security 配置
    1. 开启 HttpSecurity 配置
    2. 处理登录请求
    3. 授权请求,验证 url
    4. 处理登出请求
    5. JDBC、Memory 认证
    6. 多重 HttpSecurity 配置
  3. 方法级别的安全配置
    1. 开启方法安全配置
    2. 两种方法级别的注释
  4. 示例

第二部分 体系结构和实现

  1. SpringSecurity 的核心组件
    1. SecurityContextHolder - Security 上下文
    2. SecurityContext - 从上下文中获取当前用户主体
    3. Authentication - 当前用户主体信息
    4. GrantedAuthority - 用户权限
    5. UserDetails - 用户主体的具体实现
    6. UserDetailsService - 加载当前用户主体的实现
  2. SpringSecurity 身份认证流程
    1. 模拟一个标准的身份认证环境
    2. web 程序中的身份认证流程
    3. 认证流程中的核心对象
    4. 安全拦截器与安全对象模型
    5. Security 与 AOP 的 around 通知
  3. 第七章 Spring Security 中的核心服务
    1. UsernamePasswordAuthenticationFilter - 用户名密码登录过滤器(责任链模式)
    2. ProviderManager - 认证管理类(迭代器模式)
    3. DaoAuthenticationProvider - 数据认证
    4. UserDetailsService - 获取具体用户实体
    5. PasswordEncoder - 用户密码加密
    6. UserDetails - 用户对象

第三部分 测试支持

  1. Spring Security 的单元测试支持
    1. @WithMockUser - 使用 Mock 的对象
    2. @WithAnonymousUser - 使用匿名 Mock 对象
    3. @WithUserDetails - 使用 DB 中的对象,需要数据库连接
  2. 自定义测试注解
    1. 模仿@WithMockUser - 使用自定义的对象
    2. 重写 WithSecurityContextFactory - 生成自定义的对象,并注入 SecurityContextHolder
  3. Spring Security 对 WebFlux 测试的支持
    1. TODO:没用过 WebFlux,等我学到了再回来补充,flag 先立起来

第四部分 Web 应用安全原理

  1. 安全过滤器链
    1. Filter Ordering - 过滤链顺序如下
      1. ChannelProcessingFilter - 确保 web 请求会被 channel 通过
      2. SecurityContextPersistenceFilter - 加载 SecurityContext
      3. ConcurrentSessionFilter - 监听 session 是否改变
      4. AbstractPreAuthenticationProcessingFilter - 抽象身份验证处理过滤器
        1. UsernamePasswordAuthenticationFilter - 用户名密码验证
        2. CasAuthenticationFilter - Cas 验证
        3. BasicAuthenticationFilter - Basic 验证,请求头认证的一种
        4. ……
      5. SecurityContextHolderAwareRequestFilter - ServletAPI 与 Security 之间的包装类
      6. JaasApiIntegrationFilter
      7. RememberMeAuthenticationFilter - 以 cookie 方式进行身份验证
      8. AnonymousAuthenticationFilter - 匿名身份验证
      9. ExceptionTranslationFilter - 异常处理
      10. FilterSecurityInterceptor - HTTP 资源的安全处理
  2. 核心过滤器
    1. FilterSecurityInterceptor
    2. ExceptionTransactionFilter
    3. SecurityContextPersistenceFilter
    4. UsernamePasswordAuthenticationFilter
  3. Spring Security 与 Servlet API 的集成
    1. getRemoteUser() - 登录用户名
    2. getUserPrincipal() - 登录用户
    3. isUserInRole(String) - 是否存在权限(去掉 ROLE 前缀)
    4. authenticate(HttpServletRequest,HttpServletResponse) - 是否通过了验证
    5. login(String,String) - 登录
    6. logout() - 登出
    7. changeSessionId() - 改变 session id
  4. Basic Authentication
  5. Remember-Me Authentication
    1. Simple Hash-Based Token Approach - 简单 hash 令牌,不建议使用
    2. Persistent Token Approach - 持久令牌,使用 datasource 存储
  6. CSRF
  7. CORS
  8. HTTP Response Headers
  9. Session Management
    1. SessionManagementFilter - session 与 security-context 的管理
    2. SessionAuthenticationStrategy - session 相关的认证策略
    3. 固化攻击、会话超时、会话数量限制
  10. Anonymous Authentication
  11. WebSocket Security

第五部分 授权体系原理

  1. Authorization Architecture
  2. Secure Object Implementations
    1. 使用 AOP 进行安全对象的保护
  3. Expression-Based Access Control
    1. PreAuthority(“hasRole(‘USER’)”)
    2. PreAuthority(“hasPermission(#contact, ‘ADMIN’)”)

第六部分 单点登录 CAS 与 OAuth2

  1. LDAP
  2. OAuth2.0
  3. JSP
  4. JAAS
  5. CAS

第七部分 Spring Data 集成

  1. Dependencies

第一章 SpringSecurity 介绍

应用程序的安全性主要体现在两个方面:身份验证(authentication)授权(authorization)(或叫做 访问控制(access-control)),同样,这也是 SpringSecurity 的两大目标。

一、身份验证 Authentication

Spring Security 支持以下的技术集成,这里只写几种常见的并简单介绍:

  1. HTTP BASIC authentication headers
    1. 请求头身份验证
    2. 需要在请求头中添加 “Authorization: Basic 用户和密码的base64加密字符串”
    3. 或者在url中添加用户名和密码:“http://username:password@api.luokaiii.cn/login”
  2. LDAP
    1. 一种非常常见的跨平台身份验证,常见于大型服务
    2. 通过WSS3.0 和轻量级目录协议LDAP一起搭建的认证方式
    3. 做法是:将用户数据放在LDAP服务器上,通过LDAP服务器上的数据对用户进行认证处理。
    4. 即登录时将用户名密码,发送给 LDAP服务器进行匹配,判断是否通过认证
  3. Form-based authentication
    1. 表单身份验证
  4. OpenID authentication
    1. 一种去中心化的网上身份认证系统
    2. 使用方法:在一个支持OpenID身份提供者的网站上进行注册,然后使用该网站提供的url来进行认证
  5. Jasig Central Authentication Service(CAS)
    1. CAS 集中式认证服务封路系统,也是一个流行的开源单点登录系统
    2. CAS 系统一般分为 CAS Server(负责对用户进行认证) 和 CAS Client(处理用户名、密码等凭证)
    3. 流程为:用户访问Client服务 》 Client重定向至SSO服务器 》 身份认证 》 返回一个 Service Ticket 》 SSO服务器验证Ticket 》 允许访问
  6. Automatic ‘remember-me’ authentication
    1. 记住我
  7. Anonymous authentication
    1. 你们身份验证
  8. Java Open Source Single Sing-On
    1. Java 开源的单点登录 JOSSO

二、Spring Security 依赖管理

获取 Spring Security 的最小依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
</dependencies>

Core - spring-security-core.jar

包含核心身份验证和访问控制类、接口、远程处理支持和基本的API。是 Spring Security 应用程序的基础包。

  • org.springframework.security.core
    • 包含Context上下文、UserDetails、UserDetailsService、jdbc验证、memory验证、token、Authority、Principal等支持
  • org.springframework.security.access
    • 访问控制类和接口
    • 包含Method拦截器、AccessDeniedException、认证失败事件、无授权事件、日志监听、访问拦截器等
  • org.springframework.security.authentication
    • 包含数据库验证、RememberMe、Test、Token、Exception等支持
  • org.springframework.security.provisioning
    • 包含创建、更新、删除、修改密码等操作UserDetails的接口,支持memory、JDBC两种类型

Web - spring-security-web.jar

包含过滤器和相关的网络安全基础设施代码。

是 web 身份验证服务和基于 url 访问控制的基础。

Config - spring-security-config.jar

包含 安全名称空间解析代码(或者Java配置代码),即xml配置和java配置。

— 以下包都是可选的 —-

CAS - spring-security-cas.jar

Spring Security 的 CAS 客户端集成,对 CAS 单点登录服务器使用 。SpringSecurityWeb 身份验证的基础。

Test - spring-security-test.jar

支持使用 Spring Security 进行测试。后面会详细说明 Spring Security 是如何进行测试的。

LDAP - spring-security-ldap.jar

LDAP 身份验证和配置代码,如果需要使用 LDAP 身份验证或管理 LDAP 用户条目,则必须使用此依赖

OAuth 2.0 Core - spring-security-oauth2-core.jar

OAuth 2.0 授权框架和 OpenID Connect Core 1.0 的核心类和接口。

客户机、资源服务器、授权服务器都需要此依赖。

OAuth 2.0 Client - spring-security-oauth2-client.jar

OAuth 2.0 客户端,当需要使用 OAuth2.0 登录或者 OAuth客户端支持时使用。

OAuth 2.0 JOSE - spring-security-oauth2-jose.jar

包含 Spring Security 对 JOSE(JavaScritp 对象签名和加密)框架的支持,包含如下规范:

  • JSON Web Token - JWT
  • JSON Web Signature - JWS
  • JSON Web Encryption - JWE
  • JSON Web Key - JWK

OpenID - spring-security-openid.jar

支持 OpenID web 身份验证,用于根据外部 OpenID服务器对用户进行身份验证。

ACL - spring-security-acl.jar

专用域对象 ACL 的实现。


第二章 Java配置SpringSecurity

从 Spring 3.1 以后,Spring框架支持了对 Java 配置的支持,所以这里就不再细述 XML 的配置方式了。感兴趣的可以直接 查看 SpringSecurity 文档,里面详细描述了 Java Configuration 对应的XML Configuration。

Spring Security 提供了许多 Java Configuration 的 Samples 示例,详见 Samples

一、创建 Spring Security Java 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
);
return manager;
}
}

查看 @EnableWebSecurity 的代码,可以发现其引入了 WebSecurityConfiguration.class 的配置类。

该配置创建了一个 SpringSecurityFilterChain 的过滤器,负责应用程序内部的所有安全(如保护应用的URL、验证提交的用户名密码、重定向至登录表单等等)。

其具有的主要功能如下:

  1. 默认对应用程序中的每个 URL 进行身份验证
  2. 生成一个登录表单
  3. 允许用户使用表单的username、password进行身份认证
  4. 允许用户使用 logout 注销
  5. 预防 CSRF 攻击
  6. 维持会话固定(Session Fixation)
  7. 集成安全报头(Security Header)
    1. X-Content-Type-Options
    2. Cache Control - 缓存控制
    3. X-XSS-Protection integration - 保护X-XSS一体化
    4. X-Frame-Options integration to help prevent - 防止点击劫持
  8. 与 Servlet API 集成
    1. HttpServletRequset#getRemoteUser()
    2. HttpServletRequest#getUserPrincipal()
    3. HttpServletRequest#isUserInRole(String)
    4. HttpServletRequest#login(String,String)
    5. HttpServletRequest#logout()

二、HttpSecurity 配置

在开启 Java 配置之后,会默认开启所有路径的验证、表单登录、退出支持等等操作,但是我们并没有提供配置,这是因为 WebSecurityConfigurerAdapter 提供了一个默认的 configure(HttpSecurity http) 配置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their
* configuration. The default configuration is:
*
* <pre>
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
* </pre>
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception if an error occurs
*/
// @formatter:off
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

http
.authorizeRequests()
.anyRequest().authenticated() // 配置对应用程序的所有请求都需要身份验证
.and()
.formLogin().and() // 开启表单登录
.httpBasic(); // 允许使用 Http Basic 方式登录
}

三、Java 配置和表单登录

一般情况下,我们会使用自己定义的登录页面,而不是SpringSecurity默认提供的登录页面。因此,我们可以修改 HttpSecurity 的配置:

1
2
3
4
5
6
7
8
9
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // 指定登录页位置
.permitAll(); // 允许所有用户访问基于表单登录关联的所有url权限
}

四、授权请求

默认情况下, HttpSecurity选择拦截所有的请求,并且只要有身份认证即可访问。我们可以通过 http.authorizeRequest() 方法添加多个子类来指定 url 的自定义需求。

1
2
3
4
5
6
7
8
9
10
11
12
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 按照子类的顺序进行匹配
.antMatcher("/resources/**","signup","/about").permitAll() // 允许指定路径无需认证
.antMatcher("/admin/**").hasRole("ADMIN") // 指定路径需要 ADMIN 权限
.antMatcher("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 使用 hasRole 表达式,这样不需要 ROLE 前缀
.anyRequest().authenticated() // 其他所有请求都需要身份验证
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}

五、登出处理

在使用 WebSecurityConfigurerAdapter 时,将会自动开启注销功能,访问 /logout 后会执行如下操作使用户注销:

  1. 使用 Http Session 无效
  2. 清理任何与 RememberMe 相关的身份验证
  3. 清理 SecurityContextHolder
  4. 跳转到 “/login?logout”

或者,也可以自定义注销需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatcher("/resources/**","signup","/about").permitAll()
.antMatcher("/admin/**").hasRole("ADMIN")
.antMatcher("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout() // 提供注销支持
.logoutUrl("/my/logout") // 触发注销操作的 url,如果开启了 CSRF保护,则此请求需要是POST
.logoutSuccessUrl("/my/index") // 注销后要重定向的URL,默认为 /login?logout
.logoutSuccessHandler(logoutSuccessHandler) // 自定义登出成功处理器
.invalidateHttpSession(true) // 在注销时是否需要使 HttpSession 无效
.addLogoutHandler(logoutHandler) // 添加登出处理器
.deleteCookies(cookieNamesToClear); // 注销成功时删除指定的 cookie,这是一种显式添加 CookieClearingLogoutHandler 的方式
}

5.1 LogoutHandler 登出处理器

LogoutHandler 实现能够参与注销处理的类,通过调用它们来执行必要的清理,且不会引发异常。各种实现如下:

  1. PersistentTokenBasedRememberMeServices
  2. TokenBasedRememberMeServices
  3. CookieClearingLogoutHandler
  4. CsrfLogoutHandler
  5. SecurityContextLogoutHandler

以上处理器实现,大致能做到见名识义。

5.2 LogoutSuccessHandler 登出成功处理器

与 LogoutHandler 类似,但是可能会引发异常。在 LogoutFilter 成功注销之后,LogoutSuccessHandler 会处理例如重定向或转发到适当的目标,具体实现如下:

  1. SimpleUrlLogoutSuccessHandler
    1. 注销成功之后返回到指定 url,默认为 /login?logout
  2. HttpStatusReturningLogoutSuccessHandler
    1. 允许在注销成功之后返回一个普通的 HTTP 状态码,而不是重定向至 URL

5.3 其他的登出考虑

  1. 测试登录
  2. csrf注意事项
  3. 单点登出

六、WebFlux Security

关于 Spring-WebFlux 具体是什么,可以详见 《Spring-WebFlux》

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
@Bean
public SecurityWebFilterChain spirngSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.formLogin();
}
}

七、OAuth 2.0

TODO:@see

八、Authentication 认证

8.1 内存认证(In-Memory Authentication)

直接在内存中配置多个用户,示例如下:

1
2
3
4
5
6
7
8
@Bean
public UserDetailsService userDetailsService() throws Exception {
UserBuilder user = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("user").password("password").roles("USER").build());
manager.createUser(users.username("admin").password("password").roles("ADMIN").build());
return manager;
}

8.2 JDBC认证

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
UserBuilder user = User.withDefaultPasswordEncoder();
auth
.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema()
.withUser(users.username("user").password("password").roles("USER"))
.withUser(users.username("admin").password("password").roles("ADMIN"));
}

其他如:LDAP认证、AuthenticationProvider、UseDetailsService不再赘述。

九、多重 HttpSecurity 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@EnableWebSecurity
public class MultiHttpSecurityConfig {
@Configuration
@Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatch("/api/**")
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
}

@Configuration
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
}

如果请求的url以 /api/ 开头,将使用 ApiWebSecurityConfigurationAdapter 配置。其他请求使用 FormLoginWebSecurityConfigurerAdapter 配置。


第三章 基于方法级别的安全配置

Spring Security 提供了两种方法级别的安全配置,一个是框架的原始注释 @secure ,另一个新的基于表达式的注释。

可以针对单个 Bean 进行保护,也可以使用 AspectJ 风格的切入点跨服务层保护多个 Bean。

一、EnableGlobalMethodSecurity 注解

在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 注释,启用基于注释的安全性。示例如下:

1
2
3
4
5
@EnableGlobalMethodSecurity(securedEnabled = true)
@Configuration
public class MethodSecurityConfig {
// ...
}

然后向方法添加注释,将相应地限制对该方法的访问。这些信息将会被传递给 AccessDecisionManager:

1
2
3
4
5
6
7
8
9
10
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

或者使用 基于表达式的语法:

1
2
3
4
5
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class MethodSecurityConfig {
// ...
}
1
2
3
4
5
6
7
8
9
10
public interface BankService {
@PreAuthorize("isAnnoymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnnoymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

二、自定义方法安全配置

如果需要执行比 @EnableGlobalMethodSecurity 注释提供的更复杂的操作,可以扩展 GlobalMethodSecurityConfiguration。

1
2
3
4
5
6
7
8
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class methodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler(){
// create and return custom handler
return expressionHandler;
}
}

第四章 示例

示例请参考 官方网站:http://spring.io/spring-security/,或者 Github


第五章 SpringSecurity 中的核心组件

通过前四章简单介绍了下 SpringSecurity 的命名空间配置和Java 配置,已经可以简单一个基于 Spring Security 的应用程序。

下面,将会研究其中一些在整个框架中使用的中心接口、类和抽象概念,了解它们如何协同工作,以支持 Spring Security 中的身份验证和访问控制。

一、SecurityContextHolder

Spring-Security-Core 中对 SecurityContextHolder 的介绍如下:

1
2
Associates a given {@link SecurityContext} with the current execution thread.
将给定的{@link SecurityContext}与当前执行线程关联。

SecurityContextHolder 用于存储应用程序当前Security 上下文的地方,其中包含当前使用应用程序的主体的细节。

默认情况下,SecurityContextHolder 使用ThreadLocal 来存储这些细节,如果在处理完当前主体的请求之后清除线程,那么这种方式就是非常安全的。

一般没有必要修改SecurityContextHolder 的默认值,但是 SpringSecurity 提供了修改的途径。

二、SecurityContext

1
Interface defining the minimum security information associated with the current thread of execution.

与安全信息相关的抽象接口,用来获取或设置 Authentication

三、Authentication

实现了 Principal 与序列化的接口,包含了当前请求的主体信息。通常是保存在 SecurityContext 中。

获取方法如下:

1
2
3
4
5
6
7
8
9
10
11
public String getUsernameBySecurityContext(){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username;
if(prinicpal instanceof UserDetails){
username = ((UserDetails)principal).getUsername();

}else {
username = principal.toString();
}
return username;
}

四、GrantedAuthority

除了用户主体之外,身份验证还提供了一额外的方法 getAuthorities(),该方法返回一个 GrantedAuthority 对象数组。

GrantedAuthority 通常为“角色”,用于配置 web授权、方法授权、域对象授权等。该属性通常由 UserDetailsService 加载给 UserDetails。

如果一个用户有几千个这种权限,内存的消耗将会是非常巨大的。

五、UserDetails

从上面代码可以看出 Authentication 在大部分情况下是可以直接转换为 UserDetails 对象的。他表示一个主体,可以看做是用户数据库与 SecurityContextHolder 之间的适配器。

UserDetails 是 SpringSecurity 的核心接口,可以通过继承 UserDetails 来实现自定义的方法与属性。

扩展UserDetails

通过自定义 UserDetails 的实现,我们可以将其作为数据库实体与Authentication 的适配者。

数据库实体 User:

1
2
3
4
5
6
7
8
9
@Data
@Document(collection = "db_user")
class User{
@Id
private String id;
private String username;
private String password;
private String[] roles;
}

适配器实体 CustomUserDetails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@EqualsAndHashCode(callSuper = true)
@Data
public class CustomUserDetails extends User implements UserDetails {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.createAuthorityList(super.getRoles());
}

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

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

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

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

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

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

这样,我们就可以通过 CustomUserDetails 在 User 与 Authentication 之间相互转换,实现数据库认证 SpringSecurity。

那么 UserDetails 对象是从哪里创建的呢?就是下面的 UserDetailsService 类。

六、UserDetailsService

UserDetailsService 只有一个特殊方法,接收一个 string 的用户名参数,并返回一个 UserDetails。

1
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

通过自定义实现 UserDetailsService,返回自己的 CustomUserDetails 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private UserService userService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if(user == null)
throw new UsernameNotFoundException("user not found");

final CustomUserDetails userDetails = new CustomUserDetails();
BeanUtils.copyProperties(user,userDetails);
// todo 一些其他附加属性等
return userDetails;
}
}

第六章 SpringSecurity 身份认证流程

一、标准的身份认证

一个标准的身份验证流程:

  1. 用户提供用户名与密码
  2. 系统验证用户名密码是否正确
  3. 获取该用户的上下文信息(如角色列表、权限等)
  4. 为用户建立安全上下文
  5. 访问受保护资源时,通过上下文信息验证权限

以下示例来自于 Spring Security Reference,演示了一个简单的迷你认证环境。

模拟身份认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

while(true) {
// 1. 接收用户名与密码
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
// 2. 生成 token
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// 3. 验证 token 是否正确
Authentication result = am.authenticate(request);
// 4. 将认证主题注入上下文
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}

通常情况下,这个流程是由 Spring Security 内部执行的,但是这大致显示了 Spring Security 通过 username、password 构建一个 SecurityContext 的过程。

直接设置 SecurityContextHolder

如果你需要在一个已经拥有身份认证的系统(如自定义了过滤器或MVC控制器、拥有自己的认证系统等)中,接入 Spring Security 环境。

只需要在原有系统中读取第三方用户信息,构建一个 Spring Security 特定的 Authentication 对象,并将其放入 SecurityContextHolder 中即可。

二、Web应用中的身份验证

在一个Web 应用程序中使用 Spring Security 访问受保护的资源,流程如下:

访问资源的验证及认证流程

Spring Security 中提供了不同的类负责上图中的不同流程,主要参与者(按照使用顺序)是 ExceptionTranslationFilter、AuthenticationEntryPoint和 authentication mechanism。负责调用的是 AuthenticationManager。

安全拦截器与安全对象模型

ExceptionTranslationFilter

ExceptionTranslationFilter 是一个 Spring Security 过滤器,负责检测所有引发的 Spring Security 异常。通常情况下,异常都是由 AbstractSecurityInterceptor 引发的,它是授权服务的主要提供者。

ExceptionTranslationFilter 在验证主题时(如图中的 ③⑦),负责返回错误代码403(即认证成功,但是缺少权限的情况),或者启动 AuthenticationEntryPoint(尚未登录)。

AuthenticationEntryPoint

负责为 web 应用程序提供一个默认的身份认证策略(③ )。

Authentication Mechanism 认证机制

浏览器提供了身份验证凭据之后,服务器就需要收集这些身份信息,并进入“身份验证机制”。将用户验证的凭据生成一个“request”对象,然后交给 AuthenticationManager。

如果 AuthenticationManager 验证接收回完全填充的 Authentication 对象后,它将认为请求有效,并将 Authentication 放入 SecurityContextHolder 中,并进行重试请求。

如果 AuthenticationManager 拒绝请求,身份验证机制将要求用户重新进行认证(跳转至url或者返回http状态码)。

Store SecurityContext

在 Spring Security 中,由 SecurityContextPersistenceFilter 来承担在请求之间存储 SecurityContext,它默认将 SecurityContext 作为HttpSession 的属性存储。

每个请求都会将 SecurityContext 恢复到 SecurityContextHolder 汇总,并且在请求完成时清除 SecurityContextHolder。这样做是十分安全的,且您不应该直接与 HttpSession交互,而应当总是与 SecurityContextHolder 交互。

如果在不使用 HttpSession 的应用程序(如 无状态的 Restful web 服务)中,对每个请求进行身份验证。SecurityContextPersistenceFilter 依然十分重要,因为它会确保 SecurityContextHolder 在每个请求之后被清除。

您可以通过自定义 SecurityContextPersistenceFilter 的行为,为每个请求创建一个新的 SecurityContext。以防止一个线程中的更改影响另一个线程,或者在临时更改上下文的地方创建一个新实例。

Access-Control 访问控制

在 Spring Security 中负责访问控制决策的接口是 AccessDecisionManager,它具有一个 decide 方法,该方法使用 Authentication 对象代表请求访问的主体、安全对象、和一个基于该对象的安全元数据属性列表(如角色列表属性等)。

Spring Security 与 AOP

AOP 中提供了如:before、after、throws、around通知。Spring Security 为方法调用和 Web 请求提供了一个大致的建议,通过 around 通知,可以决定该方法是否继续调用,是否修改响应,以及是否抛出异常。


第七章 Spring Security 中的核心服务

SpringSecurity中的核心组件与服务

一、核心接口与其实现

第五章 SpringSecurity 中的核心组件第六章 SpringSecurity 身份认证流程 中介绍了 Spring Security 体系结构及其核心类。下面更加深入的了解一些核心接口与其实现方法。

上图描述了一个请求认证,大致需要经过的过滤器、接口,及各个接口的子类实现。

其中标黄的部分是下面讲解的重点。

具体的接口与实现类的含义,可以参考官方网站中的解释并结合图中的关系进行梳理。我只会挑选一些核心的实现方法来说。

如果不想看源码,请直接看我写的注释,也能明白这些类的执行过程。

1. UsernamePasswordAuthenticationFilter

拦截并处理用户的登录请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 登录认证请求必须是POST
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

// 2. 检查请求 request 中是否包含 username、password 参数
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

// 3. 将 username、password 封装为一个 UsernamePasswordAuthenticationToken 对象
// 该对象是 Authentication 的实现类
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

setDetails(request, authRequest);

// 4. 调用 AuthenticationManager 的认证方法
return this.getAuthenticationManager().authenticate(authRequest);
}

2. ProviderManager

上面调用 authenticate(authRequset) 的 AuthenticationManager 是一个接口类,其具体实现是 ProviderManager。当然,也可以选择其他实现类或者自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* 该方法用于验证 authentication(也就是上面传入的 UsernamePasswordAuthenticationToken)
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();

// 1. 循环遍历 AuthenticationProvider 的实现类集合
// 如果有超过一个的 provider 支持,则使用第一个
// 如果一个都不支持,则返回 AuthenticationException 异常
for (AuthenticationProvider provider : getProviders()) {
// 2. 判断是否支持
if (!provider.supports(toTest)) {
continue;
}

if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}

try {
// 3. 调用 provider 的认证,下面会以 DaoAuthenticationProvider 为例进行介绍
result = provider.authenticate(authentication);

if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}

if (result == null && parent != null) {
try {
// 4. 进行父类的认证尝试
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {

}
catch (AuthenticationException e) {
lastException = e;
}
}

if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 5. 认证完成,请求结束之后,删除 SecurityContextHolder 中保存的 SecurityContext,清除相关数据
((CredentialsContainer) result).eraseCredentials();
}

// 6. 发送验证成功的事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}

// 7. 身份验证失败,抛出异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}

prepareException(lastException, authentication);

throw lastException;
}

3. DaoAuthenticationProvider

DaoAuthenticationProvider 包含两个接口属性 PasswordEncoder 与 UserDetailsService,通过 UserDetailsService 加载用户数据,并使用 PasswordEncoder 比较 UserDetails.password 与 token.password 是否相等。

以此来决定是否认证成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用 UserDetailsService 的 loadUserByUsername 方法,查询装载 UserDetails
// 在实际开发中,通常会自定义 UserDetailsService 的实现,例如将数据库查询的 User 对象与 UserDetails 对象进行一个适配
// 适配者也需要我们自己定义,如 CustomUserDetails 实现 UserDetails,并将 user.role 转换为 authority等。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

4. UserDetailsService

UserDetailsService 是整个 Dao 认证框架的核心,用来装载特殊的用户数据。

因为该类为接口类,只声明了一个 loadUserByUsername(String) 方法,所以这里我们使用自定义实现来演示在日常开发场景中的使用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 实现 UserDetailsService 接口,自定义获取用户的方法
@Service
public class DemoUserDetailsService implements UserDetailsService {

// 2. 注入数据库查询实例,这里不限数据库,只要能获取到用户即可。自己new的也行
@Autowired
private UserService userService;

@Override
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 3. 从数据库中查询到 User 对象
DBUser user = userService.findUserByUsername(username);
if(user == null){
throw new AuthenticationCredentialsNotFoundException("user not found.");
}
// 4. 将 DBUser 包装为一个 UserDetails 对象,并将 user.roles 转换为 Authority
return User.withDefaultPasswordEncoder().username(user.username).password(user.password).authorities(user.roles);
}
}

二、其他

上面几个类大致说明了,如果将数据库的用户转换为 Spring Security 支持的 Authentication 对象的。

其它如内存认证、jdbcDaoImpl 的实现不再赘述。

还有如自定义 PasswordEncoder、自定义 UserDetails 等其它实现,就不一一列举了,网上一搜一大把。


第八章 Spring Security 的测试支持

本章节只介绍 Spring Security 提供的测试支持,包含哪些关键的注解、如何设置Security测试环境、以及注解的作用。

在下一章节会介绍如何通过重写 @WithMockUser 来使用自定义的 UserDetails 对象。

官网上的是 Spring Security 与原始的 Spring Test 环境的集成,这里就不做赘述了,我们使用的是 SpringBoot Test 环境

一、设置测试环境

需要先引入 spring-security-test 依赖,这样就可以直接在 @SpringBootTest 中集成 Security 的测试环境,就是这么简单.

1
2
3
4
5
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoTest {
// .. 一些 JUnit 单元测试方法
}

二、常用注解

单元测试要尽量做到“单元”化,只测试一个具体的功能,而不用将MVC、DAO各层都测试一遍。

因此这些注解可以帮助我们建立一个模拟的测试环境,使我们的测试代码只关注业务是否正常执行,代码是否正确。而不用考虑用户是否登陆,是否需要重新授权等问题。

1. @WithMockUser

在方法上使用一个模拟的用户,而不用真的去注册并登陆。

该注解会 Mock 一个用户名为‘user’,密码为 ’password‘,角色为 ’ROLE_USER’ 的Authentication,并将其注入到 SecurityContext 中。

1
2
3
4
5
6
7
@Test
@WithMockUser
public void getMessageByUser() {
// 查询SecurityContext 上下文环境,可以取到 Mock 的模拟用户数据
String username = ((UserDetails)SecurityContextHolder.getSecurityContext().getAuthentication().getPrincipal()).getUsername();
// ......
}

思考:如果我们使用的是自定义的 CustomUserDetails 呢?比如扩展了 UserDetails 的属性,增加了 level 等级属性,那么将如何Mock并从上下文中获取呢? –这将会在下一节“自定义注解”中说明

2. @WithAnonymousUser

当需要用户登录,但是不需要用户的信息时,可以考虑以匿名用户的身份运行一些测试。这样会更加方便。

3. @WithUserDetails

使用自定义的 UserDetailsService 来创建身份验证的主题,但是需要用户存在。在正常业务中,可能还涉及到数据库查询,如果包含数据库查询,还需要与业务逻辑的数据库隔离等等问题。

自定义查找的用户名,以及自定义用来查找的 UserDetailsService。

1
@WithUserDetails(value = "customUsername", userDetailsServiceBeanName = "myUserDetailsService")

三、完整的测试案例

UserService 用户业务服务,提供一个创建用户的功能,并且在保存之前,记录当前创建人。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 用户业务服务
@Service
public class UserService {
// 创建用户,并且指定创建人与创建时间
public User createUser(User user){
// 从当前环境中取出当前主体的用户名
String username = ((UserDetails)SecurityContextHolder.getSecurityContext().getAuthentication().getPrincipal()).getUsername();
// 保存新建用户的创建者、创建时间
user.setCreateUser(username);
user.setCreateDate(new Date());
return save(user);
}
}

测试用户在创建时,是否设置了创建人(即@WithMockUser 是否把用户注入了SecurityContext中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Runwith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
UserService userService;

@Test
@WithMockUser(username = "admin", roles = {"ADMIN","USER"})
public void test(){
User user = userService.create(new User("张三"));
Assertions.assertThat(user.getCreateUser()).isEqualsTo("admin");
}
}

第九章 自定义 Security Mock 注解

第八章 Spring Security 的测试支持 可以看到,如果不使用自定义的身份验证主体,@WithMockUser 是一个很好的选择。

一、@WithMockUser 存在的问题

大部分情况下,我们使用都不是 UserDetails 对象,而是 UserDetails 的自定义实现。

且 @WithUserDetails 注解还需要查询数据库,并且要求用户存在。

二、解决方案

我们可以通过模仿 @WithMockUser 注解,创建自己的 @WithMockSutomUser注解。并通过实现 WithSecurityContextFactory 来使用自己的注解。

1. @WithMockSutomUser

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

String username() default "rob";

String name() default "Rob Winch";
}

2. WithMockCustomUserSecurityContextFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final class WithUserDetailsSecurityContextFactory
implements WithSecurityContextFactory<WithUserDetails> {

private UserDetailsService userDetailsService;

@Autowired
public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

public SecurityContext createSecurityContext(WithUserDetails withUser) {
String username = withUser.value();
Assert.hasLength(username, "value() must be non-empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}

现在,我们使用新的注解来测试类或方法,Spring Security 的 WithSecurityContextTestExecutionListener 将确保我们的 SecurityContext 得到了适当的填充。


第十章 SpringSecurity中的过滤器链

Spring Security 的 web 基础完全是基于标准的 servlet 过滤器的,它在内部不使用 servlet 或任何基于 servlet 的框架(如SpringMVC),因此它与任何特定的 web 技术都没有特别强的关联。

Spring Security 不关心请求是来自于 浏览器、web服务器、HttpInvoker 还是 ajax应用。

在 Spring Security 的 HttpSecurityBuilder 类的 addFilter(Filter filter) 上,存在这样一段注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Adds a {@link Filter} that must be an instance of or extend one of the Filters
* provided within the Security framework. The method ensures that the ordering of the
* Filters is automatically taken care of.
*
* The ordering of the Filters is:
*
* <ul>
* <li>{@link ChannelProcessingFilter}</li>
* <li>{@link ConcurrentSessionFilter}</li>
* <li>{@link SecurityContextPersistenceFilter}</li>
* <li>{@link LogoutFilter}</li>
* <li>{@link X509AuthenticationFilter}</li>
* <li>{@link AbstractPreAuthenticatedProcessingFilter}</li>
* <li><a href="{@docRoot}/org/springframework/security/cas/web/CasAuthenticationFilter.html">CasAuthenticationFilter</a></li>
* <li>{@link UsernamePasswordAuthenticationFilter}</li>
* <li>{@link ConcurrentSessionFilter}</li>
* <li>{@link OpenIDAuthenticationFilter}</li>
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}</li>
* <li>{@link ConcurrentSessionFilter}</li>
* <li>{@link DigestAuthenticationFilter}</li>
* <li>{@link BasicAuthenticationFilter}</li>
* <li>{@link RequestCacheAwareFilter}</li>
* <li>{@link SecurityContextHolderAwareRequestFilter}</li>
* <li>{@link JaasApiIntegrationFilter}</li>
* <li>{@link RememberMeAuthenticationFilter}</li>
* <li>{@link AnonymousAuthenticationFilter}</li>
* <li>{@link SessionManagementFilter}</li>
* <li>{@link ExceptionTranslationFilter}</li>
* <li>{@link FilterSecurityInterceptor}</li>
* <li>{@link SwitchUserFilter}</li>
* </ul>
*
* @param filter the {@link Filter} to add
* @return the {@link HttpSecurity} for further customizations
*/
H addFilter(Filter filter);

DelegatingFilterProxy

在 Spring Security 中,过滤器类也是在应用上下文中定义的 Bean,因此能够充分利用 Spring 的依赖注入工具和生命周期接口。

DelegatingFilterProxy 提供了 web.xml 和应用程序上下文之间的关联。

1
2
3
4
5
6
7
8
9
10
<!-- 当使用DelegatingFilterProxy时,可以在 web.xml 中看到如下内容 -->
<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Filter Ordering 过滤器的顺序

  1. ChannelProcessingFilter
    1. 可以重定向到不同的协议
  2. SecurityContextPersistenceFilter
    1. 在web请求开始及结束时,将HttpSession复制到SecurityContext
  3. ConcurrentSessionFilter
    1. 当session更新时,通过 SessionRegistry 来更新 SecurityContextHolder
    2. 并更新 Principal
  4. Authentication processing machanisms
    1. 认证处理机制
    2. UsernamePasswordAuthenticationFilter、CasAuthenticationFilter、BasicAuthenticationFilter等等
    3. 用于向 SecurityContextHolder填充Authentication令牌
  5. SecurityContextHolderAwareRequestFilter
  6. JassApiIntegrationFilter
    1. 用于处理 JassAuthenticationToken,并将其放入SecurityContextHolder
  7. RememberMeAuthenticationFilter
    1. 如果没有其他身份更新 SecurityContextHolder,会尝试请求是否有RememberMe 的cookie,并生成一个Authentication 放入SecurityContextHolder
  8. AnonymousAuthenticationFilter
    1. 如果没有其他身份更新 SecurityContextHolder,则会存储一个匿名对象
  9. ExceptionTranslationFilter
    1. 捕获任何Spring Security 异常,并返回适当的Http响应
    2. 可以由 AuthenticationEntryPoint 抛出
  10. FilterSecurityInterceptor
    1. 保护web uri,并在拒绝访问时引发异常

第十一章 Spring Security 中的几个核心过滤器

FilterSecurityInterceptor

FilterSecurityInterceptor 负责处理 HTTP 资源的安全性,提供了适用于不同 HTTP URL 请求的配置属性,引用了 AuthenticationManager 和 AccessDecisionManager。

ExceptionTranslationFilter

ExceptionTranslationFilter 位于安全过滤器堆栈中的 FilterSecurityInterceptor 之上,它本身不执行任何实际的安全性控制,但处理安全性拦截器引发的异常。

ExceptionTranslationFilter 的另一个职责是在调用 AuthenticationEntryPoint 之前保存当前请求,这允许用户在进行身份验证之后,恢复请求。如用户用表单登录之后,通过 SavedRequestAwareAuthenticationSuccessHandler 重定向至原始的 URL。

RequestCacheFilter 的任务是,当用户被重定向到原始URL时,从缓存实际还原已保存的请求。

AuthenticationEntryPoint

如果用户请求一个安全的 HTTP 资源,但是它们没有经过身份验证,则会调用 AuthenticationEntryPoint。

AuthenticationException 或者 AccessDeniedException 将由安全拦截器在调用堆栈的下方抛出,从而触发入口点上的起始方法。这将为用户提供适当的响应,以便可以开始身份验证。

如果使用的是 LoginUrlAuthenticationEntryPoint,它将会定位到登录页面。

AccessDeniedHandler

如果用户已经通过身份验证,并访问受保护的资源。如果引发了 AccessDeniedException,那么意味着用户尝试了一个他们没有权限的操作。这种情况下,AccessDeniedHandler 将被调用。

向客户端发送一个 403(Forbidden)响应。

RequestCache接口

RequestCahce 封装了存储和检索 HttpServletRequest 实例所需的功能,默认情况下使用 HttpSessionRequestCache,它将请求存储在 HttpSession 中。

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 有两个主要任务,1.负责在 HTTP 请求之间存储 SecurityContext 内容,2. 在请求完成时清除 SecurityContextHolder。

清除存储上下文的 ThreadLocal 是必不可少的,否则线程可能会被替换到 servlet 容器的线程池中,而特定用户的 SecurityContext 仍然附加在上面。这个线程如果在后面被用到,可能会使用错误的 credentials 执行操作。

SecurityContextRepository

从 Spring Security 3.0 开始,加载和存储上下文的方法都放在 SecurityContextRepository 的策略接口上:

1
2
3
4
5
6
7
8
public interface SecurityContextRepository {
// HttpRequestResponseHolder 只是传入请求和响应对象的容易,允许使用包装类进行替换
// 加载的 SecurityCOntext 将被传递给过滤器链
SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);


void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
}

默认由 HttpSessionSecurityContextRepository 来实现,它将 SecurityContext 存储在 HttpSession 中,参数名为 “SPRING_SECURITY_CONTEXT”。

UsernamePasswordAuthenticationFilter

这个过滤器是最常用的身份验证过滤器,也是最长被定制的过滤器,它包含了 form-login 元素所使用的实现。配置它需要三步:

  1. 配置 LoginUrlAuthenticationEntryPoint,使用登录页面的 URL
  2. 实现登录页面,JSP或MVC控制器
  3. 配置一个 UsernamePasswordAuthenticationFilter 在应用中
  4. 将过滤器的 Bean 添加到自定义的过滤器链代理中

Authentication Success and Failure

过滤器调用 AuthenticationManager 来处理每个身份验证请求,成功的身份验证由 AuthenticationSuccessHandler 策略接口来处理,失败的身份验证由 AuthenticationFailureHandler 策略接口来处理。

该过滤器允许自定义失败成功处理器,包含有 SimpleUrlAuthenticationSuccessHandler、SavedRequestAwareAuthenticationSuccessHandler、SimpleUrlAuthenticationFailureHandler、ExceptionMappingAuthenticationFailureHandler、DelegatingAuthenticationFailureHandler、AbstractAuthenticationProcessingFilter 等实现。或者你可以选择自定义一个自己的失败或成功处理器。


第十二章 Spring Security 与 Servlet API 集成

一、与 Servlet 2.0 的集成

1. getRemoteUser()

HttpServletRequest.getRemoteUser() 返回 SecurityContextHolder.getContext().getAuthentication().getName()

2. getUserPrincipal()

HttpServletRequest.getUserPrincipal() 返回 SecurityContextHolder.getContext().getAuthentication(),返回结果类型为 Authentication,当使用用户名、密码的方式进行的身份验证时,其为 UsernamePasswordAuthenticationToken 的一个实例。

可以通过 Authentication.getPrincipal() 方法,将其转换为 UserDetails 的实现类.

3. isUserInRole(String)

HttpServletRequest.isUserInRole(String) 返回 SecurityContextHolder.getContext().getAuthentication().getAuthorities() 是否包含传入的 String,getAuthorities() 返回一个 GrantedAuthority 集合。

用户通常不需要传入 “ROLE” 前缀,它由系统自动添加。

二、与 Servlet 3.0 的集成

4. authenticate(HttpServletRequest,HttpServletResponse)

通过 HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse) 方法可以确保用户得到了身份验证。如果它们未经过身份验证,将会使用 AuthenticationEntryPoint 请求用户进行身份验证(如重定向至登录页)。

5. login(String,String)

HttpServletRequest.login(String,String) 可用于使用当前 AuthenticationManager 对用户进行身份验证。

1
2
3
4
5
try{
httpServletRequest.login("username","password");
} catch(ServletException e) {
// fail to authenticate,失败信息
}

如果需要 Spring Security 处理失败的身份验证,则不用捕获 ServletException。

6. logout()

使用 HttpServletRequest.logout() 将当前用户注销。

通常这意味着会将 SecurityContextHolder 清除、HttpSession 失效、任何与 “RememberMe”相关的身份验证将被清除,等等。但是如果你自定义了 LogoutHandler,则将根据具体配置来,注意你仍然需要定义服务端的相应(比如重定向至登录页等)。

7. 异步请求

三、与 Servlet 3.1 的集成

8. changeSessionId()

HttpServletRequest.changeSessionId() 是 Servlet3.1 及更高版本中,防止会话固定攻击的默认方法。


第十三章 Basic Authentication

Basic 认证和 Digest 认证是 web 应用中常用的两种认证机制。

一、Basic Authentication

Basic Authentication 通常用于无状态客户机,无状态客户机在每个请求上,传递其凭据。它与基于表单的身份验证结合使用是非常常见的。

Basic Authentication 会将密码使用纯文本的形式传输,因此只能在加密的传输层(如HTTPS)上使用密码。

1. BasicAuthenticationFilter

BasicAuthenticationFilter 负责处理 HTTP 头中提供的基本身份验证凭据。基本身份验证被广泛部署在用户代理中,并且实现非常简单。认证格式类似于:https://zhangsan:123456@www.luokaiii.cn/login

2. Configuration

配置的 AuthenticationManager 处理每个身份验证请去,如果身份验证失败,将使用 AuthenticationEntryPoint 重试身份验证过程。通常需要结合使用过滤器和 BasicAuthenticationEntryPoint,返回一个带有401 Header的响应来重试 HTTP Basic 身份验证。

如果身份验证成功,则生成 Authentication 对象并放入 SecurityContextHolder 中。

二、Digest Authentication

Digest Authentication 试图解决 Basic Authentication 中存在的许多缺陷和弱点,特别是以明文形式发送凭据。

1
2
You should not use Digest in modern applications because it is not considered secure. 
翻译:你不应该在现代应用中使用 Digest,因为它是不安全的。

so,这里就不写了。详见 digest-processing-filter


第十四章 Remember-Me Authentication

Remember-Me 或者 persistent-login 验证是指网站能够在会话之间记住主体的身份。通常是向浏览器发送 cookie 来实现的,并且在后面的会话中,会去检测 cookie 并进行自动登录。

Spring Security 提供了两种方式来实现具体的 remember-me 操作,一种是使用 hash 来保护基于 cookie 令牌的安全性,另一种是使用数据库或其他持久存储机制来存储生成的令牌。

这两种方式都需要 UserDetailsService,否则身份验证程序将无法工作。

一、Simple Hash-Based Token Approach

使用散列来实现一个 remember-me 策略,在身份验证成功后,cookie会被发送到浏览器,其组成如下:

1
2
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" + password + ":" + key))

在令牌的有效期内,用户名、密码、秘钥不能更改。这种方式存在一个安全问题,因为 remember-me 令牌可以从任何用户代理使用,直到令牌过期为止。那么如果一个主体知道了令牌已经被 captured,那么可以很容易地更改密码,并使所有的 remember-me 令牌失效。

Alternatively remember-me services should simply not be used at all. 因此,hash-based remember-me 不应该被使用。

二、Persistent Token Approach

持久令牌的方法,需要为 remember-me 配置一个持久化数据源,如下:

1
2
3
<http>
<remember-me data-srouce-ref="someDataSource"/>
</http>

该数据库需要包含一个 “persistent_logins” 表,表结构如下:

1
2
3
4
5
6
create table persistent_logins(
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)

三、Remember-Me 的接口与实现

Remember-Me 通常与 UsernamePasswordAuthenticationFilter 一起使用,在通过用户名、密码方式登录时,选择是否记住用户密码。并且通过 AbstractAuthenticationProcessingFilter 父类的钩子来实现。

它也可以与 BasicAuthenticationFilter 一起使用,钩子会在适当的时候调用一个具体的 RememberMeServices 服务。

1. RememberMeServices Interface

以下是 RememberMeServices 接口的内容:

1
2
3
4
5
6
7
8
9
// 该接口为 remember-me 提供了基础的,与认证相关的事件通知
public interface RememberMeServices {

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);
}

在 AbstractAuthenticationProcessingFilter 中只调用了 loginFail 和 loginSuccess,只要 SecurityContextHolder 中不包含身份验证,那么 RememberMeAuthenticationFilter 就会调用 autoLogin() 方法。

2. TokenBasedRememberMeServices Implements

TokenBasedRememberMeService 生成一个 RememberMeAuthenticationToken,并交给 RememberMeAuthenticationProvider 进行处理。

在 TokenBasedRememberMeService 的构造中,需要两个参数,一个是 key,另一个是 UserDetailsService,用于比较用户名和密码,并生成 RememberMeAuthenticationToken 来包含正确的 GrantedAuthority。

该 Service 还实现了 LogoutFilter,因此可以使用 LogoutFilter 自动清除 cookie。

3. PersistentTokenBasedRememberMeServices

与 TokenBasedRememberMeServices 一样,但是需要一个 PersistentTokenRepository 来存储这些令牌。

PersistentTokenRepository 有两个具体实现:

  1. InMemoryTokenRepositoryImpl
  2. JdbcTokenRepositoryImpl

第十五章 CORS - HTTP请求控制

Spring 框架为 CORS 提供了一系列的支持。CORS 的处理在 Spring Security 之前,因为此时的请求不包含任何 cookie(JSESSIONID)。

处理CORS 最简单的方法是使用 CorsFilter,通过配置一个 CorsConfigurationSource,可以将 CorsFilter 与 Spring Security 集成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void config(HttpSecurity http) throws Exception {
http
// 默认情况下使用名为 corsConfigurationSource 的bean
// 这里可以省略指定 CorsConfigurationSOurce
.cors().and()
...
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("https://example.com"));
config.setAllowedMethods(Arrays.asList("GET","POST"));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

第十六章 Security HTTP Response Headers

Spring Security 允许用户注入默认的安全headers,用来保护应用程序。

一、Headers 默认配置

默认设置如下:

1
2
3
4
5
6
7
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

也可以使用 Java 来配置响应头的内容:

1
2
3
4
5
6
7
8
9
10
11
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void config(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin()
.httpStrictTransportSecurity().disable();
}
}

二、Header 的具体内容

1. Cache Control

缓存控制,意味着用户是否可以通过浏览器缓存来查看经过身份验证的页面。

默认情况下,是不使用缓存的,入股你希望缓存特定的响应,那么可以在应用程序中进行如下配置:

1
2
3
4
5
6
7
8
9
10
@EnableWeb
public class WebMvcConfiguration impelements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry
.addResourceHandler("/resources/**")
.addResourceLocations("/resources/")
.setCachePeriod(31556926);
}
}

在开发中,我们可以使用 SpringMVC 的 HttpServletResponse 来增加 Header:

1
2
3
4
5
6
7
@GetMapping("/shops")
public ModelAndView(ModelAndView mv, HttpServletResponse response) {
response.addHeader(HttpHeaders.CACHE_CONTROL, "public,max-age=5");

mv.setViewName("shop");
return mv;
}

2. Content-Type

响应内容的类型

3. Http Strict Transport Security (HSTS)

HTTP 严格传输安全,要求所有请求必须是 https 的。

4. Http Public Key Pinning (HPKP)

防止中间人攻击伪造的证书,它告诉网络客户端将一个特定的加密公钥与一个特定的网络服务器相关联。

5. X-Frame-Options

是否允许网站被添加到一个框架中。如登录到银行的用户可能会点击授予其他用户访问权限的按钮。这种攻击被称为点击劫持。

默认为 DENY。

6. X-XSS-Protection

默认开启,过滤XSS 攻击。

7. Content Security Policy

内容安全策略(CSP)


第十七章 Session Management - 会话管理

Http Session 相关的功能由 SessionManagementFilter 和 SessionAuthenticationStrategy 接口组合处理。

典型用法包括:Session 固化攻击预防、Session 超时检测、限制用户同时打开的会话数量等。

一、SessionManagementFilter

SessionManagementFilter 通过 SecurityContextRepository 来检查当前 SecurityContextHolder 中的内容,以此来确保当前用户已经通过了身份验证。

如果 repository 中包含 SecurityContext,则过滤器不做任何操作。如果没有,且 ThreadLocal 中 SecurityContext 包含一个(非匿名)身份验证对象,则该过滤器假定它们已经由之前的过滤器做了身份验证。

然后将调用已配置的 SessionAuthenticationStrategy。

如果用户没有经过身份验证,过滤器会检查是否请求了无效的 Session Id(如超市等),此时过滤器会调用 InvalidSessionStrategy。最常见的行为就是重定向至一个固定的额URL,由 SimpleRedirectInvalidSessionStrategy 实现。

二、SessionAuthenticationStrategy

Session 认证策略,既被 SessionManagementFitler 使用,也被 AbstractAuthenticationProcessingFilter 使用。因此,如果使用自定义表单登录类,则需要同时注入到这两个类中。

三、Concurrency Control 并发控制

Spring Security 能够防止主体对同一应用进行超过指定次数的并发身份验证。

如果您阻止用户从两个不同的会话登录到 web 应用程序。您可以终止他们以前的登录,也可以在他们尝试再次登录时报告错误,防止二次登录。但是,如果是第二种情况的话,如果用户没有显式的注销的话,在原 session 有效期间是无法登陆的。

四、Querying the SessionRegistry for currently authenticated users and their sessions

通过 SessionRegistry 查询当前登录的用户,以及用户的 sessions。


第十八章 Anonymous Authentication - 匿名身份认证

Spring Security 3.0后,会自动提供匿名身份验证支持,并且可以使用匿名元素进行定制。

一、Configuration

AnonymousAuthenticationToken 实现了 Authentication,并且关联了 GrantedAuthority。通过 AnonymousAuthenticationProvider,连接到 ProviderManager 中。并最终由 AnonymousAuthenticationFilter 向 SecurityContextHolder 中添加一个匿名的Authentication。

二、AuthenticationTrustResolver

AuthenticationTrustResolver 接口提供了一个 isAnonymous(Authentication) 方法,用来校验 Authentication 是否是匿名身份。其实现为 AuthenticationTrustResolverImpl。

在 ExceptionTranslationFilter 访问接口,并抛出 AccessDeniedException 异常,且当前身份验证为匿名类型,则不会抛出 403(FORBIDDEN) 响应,而是由过滤器转向 AuthenticationEntryPoint。以便主体能够重新进行身份验证。

这个区别是必须的,否则的话,用户将永远无法使用form、basic、digest等其他正常身份验证机制来登录。


第十九章 WebSocket Security

一、WebSocket Configuration

Spring Security 4.0 通过 Spring Messaging 抽象引入了对 WebSocket 的授权支持。如果需要使用 Java 配置授权,只需要扩展 AbstractSecurityWebSocketMessageBrokerConfigurer,并配置 MessageSecurityMetadataSourceRegistry。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1. 继承了 config 后,任何的入站 connect 消息都需要提供有效的 CSRF 令牌来实施同源策略
* 2. SecurityContextHolder 由入站请求中的额 simpUser 头部属性中的用户填充
*/
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
// 任何以 "/user/" 开头的入站请求都需要 ROLE_USER 权限
messages
.simpDestMatchers("/user/*").authenticated();
}
}

也可以使用 XML 的方式进行配置,如下:

1
2
3
<websocket-message-broker>
<intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>

二、 WebSocket Authentication - WebSocket 认证

在创建与重连 WebSocket 连接时,在 HTTP 请求中发现的相同的认证信息。这意味着 HttpServletRequest 上的主体将会被移交给 websocket。

三、WebSocket Authorization - WebSocket 授权

授权配置实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// 任何没有 destination 的消息都需要身份验证
.nullDestMatcher().authenticated()
// 任何人都可以订阅 /user/queue/errors
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
// 任何以 /app/ 开头的 destination 消息都需要 ROLE_USER 角色
.simpDestMatchers("/app/**").hasRole("USER")
// 任何以 /user/ 或 /topc/friends 开头的 SUBSCRIBE 订阅都需要 ROLE_USER 角色
.simpSubscribeDestMatchers("/user/**","/topic/friends/*").hasRole("USER")
// 任何其他 MESSAGE 或 SUBSCRIBE 类型的消息都将被拒绝
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
// 任何其他消息都将被拒绝(这样做,可以确保不会遗漏任何消息)
.anyMessage().denyAll();
}
}

四、WebSocket Authorization Notes

1. WebSocket Authorization on Message Types

WebSocket 授权说明,一般情况下,我们只希望客户端订阅 “/topic/system/notifications”,而不希望客户端向该 destination 发送消息。

如果我们允许客户端向 “/topic/system/notifications” 发送消息,那么客户端就可以直接向所有的订阅端点发送消息,并模拟系统。

2. WebSocket Authorization on Destinations

WebSocket 订阅的授权。以聊天应用为例,我们希望客户端监听 “/user/queue”,它将转换为 “/queue/user/message-“,但是我们不希望客户机能够监听 “/queue/*”,这样客户端就能查看每个用户的消息了。

一般情况下,应用程序通常拒绝向以代理前缀(如”/topic”或”/queue”) 开头的消息发送任何订阅。

3. Outbound Messages

Spring 包含了一个名为 “Flow of Message(消息流)” 的部分,描述了消息如何在系统中流动。

值得注意的是,Spring Security 只保护 clientInboundChannel,而不保护 clientOutboundChannel。

这是因为每一条进入的信息,通常对应着更多发出的信息。因此为了性能考虑,Spring Security 鼓励保护端点的订阅。

五、Enforcing Same Origin Policy 同源策略

假定在用户浏览器中,访问了某个网站并进行了身份验证,此时用户另打开一个标签页并访问了另一个网址,那么同源策略确保了网站二不能读写数据到网站一。

而 websocket 与 SocketJS,都绕过了同源策略,因此开发人员需要显式地保护自己的应用不受外域攻击。

1. Adding CSRF to Stomp Headers

默认情况下,Spring Security 要求在任何 connect 消息中使用 CSRF 令牌,这确保只有访问 CSRF 令牌的站点才能连接。

但是 SockJS 不允许这些,因此我们必须在 Stomp 头文件中包含 token。

2. Disable CSRF within WebSockets

如果允许其他域访问站点,可以禁用 SpringSecurity 的保护,配置如下:

1
2
3
4
5
6
7
8
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

@Override
protected boolean smaeOriginDesabled() {
return true;
}
}

第二十章 Authorization Architecture - 授权架构

一、Authorization

所有的 Authentication 都存储着一个 GrantedAuthority 对象列表,这些对象代表了 Principal 所具有的权利。

GrantedAuthority 由 AuthenticationManager 插入到 Authentication 对象中,然后由 AccessDecisionManager 在作出授权决策时读取。

GrantedAuthority 只有一个方法接口: getAuthority(),该方法允许 AccessDecisionManager 获得 GrantedAuthority,通过字符串的形式返回。

SimpleGrantedAuthority 实现了 GrantedAuthority,这将允许任何用户 将 String 转换为 GrantedAuthority,在 Spring Security 中,所有的 AuthenticationProvider 都是用 SimpleGrantedAuthority 来填充 Authentication 对象。

二、Pre-Invocation Handling - 预处理

Spring Security 提供了 拦截器,用于控制安全对象的访问,比如方法调用或 web 请求等。AccessDecisionMananger 决定了是否允许进行调用。

2.1 The AccessDecisionManager

AccessDecisionManager 由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策。接口包含三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 做出最终访问控制的决策
*/
public interface AccessDecisionManager {
// decide 方法传递它所需要的所有信息,以便做出授权决策
void decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attrs) throws AccessDeniedException;

// 该方法用来确定 AccessDecisionManager 是否可以处理传入的 ConfigAttribute
boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);
}

2.2 Voting-Based AccessDecisionManager Implementations - 基于投票的辅助决策管理器的实现

Spring Security 包含了几个基于投票的 AccessDecisionManager 实现。用户也可以实现自己的 AccessDecisionManager 来控制授权的所有方面。

Access-Decision-Voting

使用这种方法,会对一系列的 AccessDecisionVoter 进行轮询,然后 AccessDecisionManager 根据投票结果来决定是否抛出 AccessDeniedException。

AccessDecisionVoter 接口有以下三种方法:

1
2
3
4
5
6
7
8
public interface AccessDecisionVoter {
// 返回结果: ACCESS_GRANTED = 1 允许访问; ACCESS_DENIED = -1 拒绝访问; ACCESS_ABSTAIN = 0 弃权;
int vote(Authenticaiton authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);
}

Spring Security 提供了三种具体的访问决策管理器来记录选票。

  1. UnanimousBased
    1. 无论投了多少 ACCESS_GRANTED 通过票,只要有 ACCESS_DENIED 反对票,则判断为不通过
    2. 如果没有 ACCESS_DENIED 反对票,且有 ACCESS_GRANTED 通过票,则判断为通过
  2. AffirmativeBased
    1. 只要有 ACCESS_GRANTED 通过票,则判断为通过
    2. 如果没有 ACCESS_GRANTED 通过票,且 ACCESS_DENIED 反对票在两个及以上,则判断为不通过
  3. ConsensusBased
    1. 如果 ACCESS_GRANED 通过票数大于 ACCESS_DENIED 反对票数,则判断为通过
    2. 如果 ACCESS_GRANED 通过票数小于 ACCESS_DENIED 反对票数,则判断为不通过
    3. 如果 ACCESS_GRANED 通过票数等于 ACCESS_DENIED 反对票数,则根据配置的 allowIfEqualGrantedDeniedDecisions(默认为true)来进行判断
  4. RoleVoter
    1. 将配置属性当做简单的角色名称,如果用户存在该角色,则通过授权
  5. AuthenticatedVoter
    1. 用来区分匿名、完全验证和 remember-me 用户

三、After Invocation Handling - 处理后

After Invocation Handling


第二十一章 Security Object Implementations - 安全对象的实现

Spring Security 在2.0之后,通过配置的 MethodSecurityInterceptor 来确保 MethodInvocation 能够拦截到特定的 Bean 或者在多个 Bean 之间共享。拦截器使用 MethodSecurityMetadataSource 实例获取应用于特定方法调用的配置属性。

MapBasedMethodSecurityMetadataSource 用于存储由方法名称键值的配置属性,并在内部使用 标签来定义属性。

大致的意思是,配置一个 MethodSecurityInterceptor 显式的方法安全拦截器,可以使用 Spring AOP 的代理机制结合 AspectJSecurityInterceptor,来拦截指定的方法。进行方法保护。


第二十二章 Expression-Based Access Control - 基于表达式的访问控制

Spring Security 使用 SpringEL 作为表达式支持。

一、常用的内置表达式

Expression Description
hasRole([role]) 主体是有具有指定角色
hasAnyRole([role…]) 主体是有具有指定角色之一
hasAuthority([authority]) 是否具有指定权限
hasAnyAuthority([authority…]) 是否具有指定权限之一
principal 允许直接访问主体对象
authentication 允许直接从 SecurityContext 获取当前主体
permitAll 总是为true
denyAll 总是为false
isAnonymous() 是否是匿名用户
isRememberMe() 是否是 remember-me 用户
isAuthenticated() 是否不是匿名用户
isFullyAuthenticated() 是否不是匿名用户,且不是remember-me 用户
hasPermission(Object target,Object permission) 用户是否可以访问权限为permission的目标
hasPermission(Object targetId,String targetType,Object permission) 用户是否可以访问权限为permission的目标

二、方法注解表达式

这两个注解 @PreAuthorize("hasRole('USER')")@PreAuthorize("hasPermission(#contact, 'ADMIN')") 能够使用在具体的方法上,以一个方法参数作为表达式的一部分,来决定当前用户是否拥有给定的权限。


自定义安全表达式

1
2
3
4
5
6
7
/**
* Base root object for use in Spring Security expression evaluations.
*
* @author Luke Taylor
* @since 3.0
*/
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations

SecurityExpressionRoot 是所有安全表达式的基类,我们需要做的就是:

  1. 继承 SecurityExpressionRoot,自定义 MethodSecurity 表达式
  2. 继承 DefaultMethodSecurityExpressionHandler,自定义方法级别的安全校验处理器
  3. 继承 GlobalMethodSecurityConfiguration,添加自定义的表达式处理器

一、MethodSecurityConfig

开启方法级别的安全校验,注入登录用户时加载 UserDetails 的DB服务对象,通过 createExpressionHandler 注入自定义的表达式处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 配置 MethodSecurity 表达式
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

private final LoginDetailService loginDetailService;

public MethodSecurityConfig(LoginDetailService loginDetailService) {
this.loginDetailService = loginDetailService;
}

@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new ResourceMethodSecurityExpressionHandler(loginDetailService);
}
}

二、ResourceMethodSecurityExpressionHandler

接收 UserDetailsService,并创建一个处理表达式的操作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 自定义方法级别的安全校验处理器
*/
public class ResourceMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

private final LoginDetailService loginDetailService;

private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

public ResourceMethodSecurityExpressionHandler(LoginDetailService loginDetailService) {
this.loginDetailService = loginDetailService;
}

@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
final ResourceMethodSecurityExpressionRoot root = new ResourceMethodSecurityExpressionRoot(authentication, loginDetailService);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(this.trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}

三、ResourceMethodSecurityExpressionRoot

仿照 hasAuthority,编写一个自己的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 自定义 MethodSecurity 表达式
*/
@Slf4j
public class ResourceMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

private LoginDetailService loginDetailService;

private Object filterObject;
private Object returnObject;
private Object target;

public ResourceMethodSecurityExpressionRoot(Authentication authentication, LoginDetailService loginDetailService) {
super(authentication);
this.loginDetailService = loginDetailService;
}

/**
* 自定义接口,是否允许对该id的访问
*/
public boolean canReadCourse(String courseId) {
log.debug("method params courseId", courseId);
log.debug("current principal {}", getPrincipal());
return true;
}

public final boolean hasGlobalAuthority(String authority) {
return hasAnyGlobalAuthority(authority);
}

public final boolean hasAnyGlobalAuthority(String... authorities) {
return hasAnyGlobalAuthorityName(null, authorities);
}

private boolean hasAnyGlobalAuthorityName(String prefix, String... roles) {
final String username = ((UserDetails) getPrincipal()).getUsername();
final UserDetails details = loginDetailService.loadUserByUsername(username);

if (details.getAuthorities() != null) {
final Set<String> roleSet = details.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());

for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
}

return false;
}

private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
}
if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(defaultRolePrefix)) {
return role;
}
return defaultRolePrefix + role;
}

@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}

@Override
public Object getFilterObject() {
return filterObject;
}

@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}

@Override
public Object getReturnObject() {
return returnObject;
}

@Override
public Object getThis() {
return target;
}

public void setTarget(Object target) {
this.target = target;
}

@Override
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
super.setRoleHierarchy(roleHierarchy);
}
}