1. 引导
跨站脚本攻击(XSS)
XSS攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
如何防护?
现在主流的浏览器内置了防范XSS的措施,对于开发者来说,有如下方式来防止XSS攻击。
HTTPOnly防止截取Cookie
作为一个标准,浏览器将禁止页面的JavaScript访问带有HTTPOnly属性的Cookie,严格来说,HttpOnly并非阻止XSS攻击,而是能阻止XSS攻击后的Cookie劫持攻击。
输入检查
对于用户的任何输入要进行检查、过滤和转义,建立可信任的字符和HTML标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。
输出检查
一般来说,除富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。例如利用sanitize-html对输出内容进行有规则的过滤之后再输出到页面中。
跨站请求伪造(CSRF)
什么是跨站请求伪造?
简单来说,就是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站,并运行一些操作(如发邮件,发消息,转账等),由于浏览器曾经认证过该网站,所以被访问的网站会认为是真正的用户操作而去运行,这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求来自某个用户的浏览器,而不能保证请求本身是用户资源发出的。
产生的原因:
CSRF攻击的可能原因是受害者网站的HTTP请求与攻击者网站的请求完全相同。这意味着没有办法拒绝来自邪恶网站的请求并允许来自银行网站的请求。
如何防御?
为了防御CSRF攻击,我们需要确保恶意站点无法提供请求中的某些内容,因此我们可以区分这两个请求。
Spring提供了两种机制来防御CSRF攻击:
- 同步器令牌模式
- 在会话Cookie上指定SameSite属性
同步器令牌模式
该解决方案是为了确保我们每个Http请求除了我们的会话cookie外,还必须在HTTP请求中包含一个安全的,随机生成的值,称为CSRF令牌。
提交HTTP请求时,服务器必须查找预期的CSRF令牌,并将其与HTTP请求中的实际CSRF令牌进行比较。如果值不匹配,则应拒绝HTTP请求。
此外,实际的CSRF令牌应该位于浏览器不会自动包含的HTTP请求的一部分中。例如,在HTTP参数或HTTP header中要求实际的CSRF令牌能防止CSRF攻击。在cookie中要求实际CSRF令牌不起作用,因为浏览器会自动将cookie包含在HTTP请求中。
同时也不能在HTTP GET中包含随机令牌,因为这有可能导致令牌泄漏。
SameSite属性
防止CSRF攻击的一种新兴方法是在cookie上指定SameSite属性。
Spring Security不直接控制session和cookie的创建,因此不提供对SameSite属性的支持。
这里也不做过多介绍,感兴趣的可以点击这里进行了解。
什么情况下使用CSRF保护
官方建议是对普通用户可能由浏览器处理的任何请求使用CSRF保护(即使你的应用程序是无状态的也可能会受到CSRF攻击)。如果仅创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。
2. Servlet安全性(全局角度)
2.1 过滤器链(Filters)
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。
下面通过单个HTTP请求的处理程序的典型分层图来说明过滤器链的使用。
客户向应用程序发送一个请求,容器创建一个FilterChain,其中包含过滤器和Servlet,根据请求URI的路径处理HttpServletRequest。在Spring MVC应用程序中,Servlet是DispatcherServlet的一个实例。一个Servlet最多只能处理一个HttpServletRequest和HttpServletResponse,但是,可以使用多个过滤器来进行如下操作:
- 阻止下游过滤器或Servlet被调用。在这个情况下,过滤器通常会编写HttpServletResponse
- 修改下游过滤器和Servlet使用的HttpServletRequest或HttpServletResponse
FilterChain的用法例子:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
由此可以看出,过滤器仅仅会对下游的过滤器和servlet带来影响,因此在实际代码编写中,尤其要考虑调用的先后顺序。
2.2 DelegatingFiterProxy(重要)
Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext之间架桥。Servlet容器允许使用自己的标准注册过滤器,但它不知道Spring定义的bean。可以通过标准的Servlet容器机制注册DelegatingFilterProxy,但将所有工作委托给实现Filter的Spring Bean。
DelegatingFilterProxy 执行的伪代码如下:
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);
}
首先思考一个问题,在SpringBoot中是如何注册 DelegatingFilterProxy 呢?
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
代码中DelegatingFilterProxyRegistrationBean 的作用便是在 SpringBoot 环境下通过 TomcatStarter 等内嵌容器启动类来注册一个 DelegatingFilterProxy。
DelegatingFilterProxy 是 SpringSecurity 的“门面”,而它本身是 Spring Web 包中的类,并不是 SpringSecurity 中的类。这是因为 Spring 考虑到了多种使用场景,自然希望将侵入性降到最低,所以使用了这个委托代理类来代理真正的 SpringSecurityFilterChain。DelegatingFilterProxy 实现了 javax.servlet.Filter 接口,使得它可以作为一个 java web 的标准过滤器,其职责也很简单,只负责调用真正的 SpringSecurityFilterChain。
下面来看看它的源码(有删减):
org.springframework.web.filter.DelegatingFilterProxy
public class DelegatingFilterProxy extends GenericFilterBean {
private WebApplicationContext webApplicationContext;
// springSecurityFilterChain
private String targetBeanName;
// <1> 关键点
private volatile Filter delegate;
private final Object delegateMonitor = new Object();
public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// 获取Spring根应用程序上下文并尽早初始化委托,如果可能的话。如果根应用程序上下文将在此之后启动过滤器代理,我们将不得不求助于延迟初始化。
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 过滤器代理支持懒加载
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// 让代理过滤器执行实际的过滤行为
invokeDelegate(delegateToUse, request, response, filterChain);
}
// 初始化过滤器代理
// <2>
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
// 调用代理过滤器
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
}
通过阅读源码可以发现,整个DelegatingFilterProxy 类都是围绕delegate来处理,在初始化过滤器代理的时候,DelegatingFilterProxy 尝试去容器中获取名为 targetBeanName 的类,而 targetBeanName 的默认值便是 Filter 的名称,也就是 springSecurityFilterChain!说白了,DelegatingFilterProxy 只是名称和 targetBeanName 叫 springSecurityFilterChain,真正容器中的 Bean(name=”springSecurityFilterChain”) 其实并不是它,而是我们接下来将讲到的FilterChainProxy。
2.3 FilterChainProxy(重要)
Spring Security的Servlet支持包含在FilterChainProxy中。FilterChainProxy是Spring Security提供的一个特殊的过滤器,它允许通过SecurityFilterChain委托给多个过滤器实例。因为FilterChainProxy是一个Bean,它通常被包装在一个DelegatingFilterProxy中。
org.springframework.security.web.FilterChainProxy
public class FilterChainProxy extends GenericFilterBean {
// <1> 包含了多个 SecurityFilterChain
private List<SecurityFilterChain> filterChains;
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}
@Override
public void afterPropertiesSet() {
filterChainValidator.validate(this);
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
doFilterInternal(request, response, chain);
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
// <1>
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
/**
* <1> 可能会有多个过滤器链,返回第一个和请求 URL 匹配的过滤器链
*/
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
}
看 FilterChainProxy 的名字就可以发现,它依旧不是真正实施过滤的类,它内部维护了一个 SecurityFilterChain,这个过滤器链才是请求真正对应的过滤器链,并且同一个 Spring 环境下,可能同时存在多个安全过滤器链,如 private List filterChains 所示,需要经过 chain.matches(request) 判断到底哪个过滤器链匹配成功,每个 request 最多只会经过一个 SecurityFilterChain。为何要这么设计?因为 Web 环境下可能有多种安全保护策略,每种策略都需要有自己的一条链路,所以实际每次请求,最多只有一个安全过滤器链被返回!
所以说,SecurityFilterChain 才是真正意义上的 SpringSecurityFilterChain:
org.springframework.security.web.DefaultSecurityFilterChain
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
//这里的List filters就包含了 UsernamePasswordAuthenticationFilter,SecurityContextPersistenceFilter,FilterSecurityInterceptor 等常用的 Filter。
public List<Filter> getFilters() {
return filters;
}
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
}
2.4 SecurityFiterChain(重要)
SecurityFilterChain由FilterChainProxy使用,以确定应为此请求调用哪个Spring安全过滤器。
SecurityFilterChain中的安全过滤器通常是bean,但是它们是在FilterChainProxy中注册的,而不是委托给FilterProxy。
那么SecurityFiterChain是如何注册的?
在我们写SecurityConfig配置类的时候,一般都会使用@EnableWebSecurity
注解和继承WebSecurityConfigurerAdapter
来进行安全配置,来到WebSecurity类中:
org.springframework.security.config.annotation.web.builders.WebSecurity
public final class WebSecurity extends
AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements
SecurityBuilder<Filter>, ApplicationContextAware {
@Override
protected Filter performBuild() throws Exception {
int chainSize = ignoredRequests.size()+ securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
// <1> FilterChainProxy 由 WebSecurity 构建
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
postBuildAction.run();
return result;
}
}
总结起来就是:一个名称 SpringSecurityFilterChain,借助于 Spring 的 IOC 容器,完成了 DelegatingFilterProxy 到 FilterChainProxy 的连接,并借助于 FilterChainProxy 内部维护的 List 中的某一个 SecurityFilterChain 来完成最终的过滤。
使用FilterChainProxy的好处
FilterChainProxy为直接向Servlet容器或 DelegatingFilterProxy注册提供了许多好处。
首先,它为Spring Security的所有Servlet支持提供了一个起点。因此,如果您正尝试对Spring Security的Servlet支持进行故障排除,那么在FilterChainProxy中添加一个调试点是一个很好的起点。
第二,由于FilterChainProxy是Spring安全使用的核心,它可以执行非可选的任务。例如,它清除SecurityContext以避免内存泄漏。它还可以使用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击。
此外,它在确定何时应该调用SecurityFilterChain方面提供了更大的灵活性。在Servlet容器中,仅根据URL调用过滤器。但是,FilterChainProxy可以通过利用RequestMatcher接口根据HttpServletRequest中的任何内容确定调用。
事实上,FilterChainProxy可以用来决定应该使用哪个SecurityFilterChain。这允许在应用程序中为不同的片提供完全独立的配置。
在上图中,FilterChainProxy决定应该使用哪个SecurityFilterChain。只有第一个匹配的SecurityFilterChain才会被调用。如果一个URL的/api/messages/被请求,它将首先匹配SecurityFilterChain0的模式/api/,所以只有SecurityFilterChain0将被调用,即使它也匹配SecurityFilterChainn。如果一个/messages/的URL被请求,它将不匹配SecurityFilterChain0的/api/模式,所以FilterChainProxy将继续尝试每个SecurityFilterChain。
注意SecurityFilterChain0只配置了三个安全过滤器实例。但是,SecurityFilterChainn配置了四个安全过滤器。需要注意的是,每个SecurityFilterChain都可以是唯一的,并在隔离状态下进行配置。事实上,如果应用程序希望Spring security忽略某些请求,SecurityFilterChain可能没有安全过滤器。
2.5 SecurityFilter
【建议参考这篇博客】
所有的过滤器汇总:
ChannelProcessingFilter
ConcurrentSessionFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter:两个主要职责:请求来临时创建 SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder。
HeaderWriterFilter
CorsFilter
CsrfFilter:默认开启的一个过滤器,用于防止 csrf 攻击
LogoutFilter:处理注销的过滤器
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter:表单提交了 username 和 password,被封装成 token 进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。
ConcurrentSessionFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter:匿名身份过滤器
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter: 和 session 相关的过滤器,内部维护了一个 SessionAuthenticationStrategy,两者组合使用,常用来防止 session-fixation protection attack,以及限制同一用户开启多个会话的数量
ExceptionTranslationFilter:这个过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理
FilterSecurityInterceptor:决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。
SwitchUserFilter
2.6 处理安全异常
ExceptionTranslationFilter允许将AccessDeniedException和AuthenticationException转换为HTTP响应。
ExceptionTranslationFilter作为安全过滤器之一插入到FilterChainProxy中。
- 首先,ExceptionTranslationFilter调用FilterChain.doFilter(request,response)将调用应用程序的其余部分。
- 如果用户未通过身份验证或为AuthenticationException,则开始身份验证。
- 1 该SecurityContextHolder中被清除出
- 2 将HttpServletRequest保存在中RequestCache。用户成功通过身份验证后,将RequestCache用于重播原始请求。
- 3 AuthenticationEntryPoint用于从客户机请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate头。
- 否则,如果是AccessDeniedException,则拒绝访问。调用AccessDeniedHandler处理拒绝的访问。
ExceptionTranslationFilter伪代码
try {
filterChain.doFilter(request, response); //1
} catch (AccessDeniedException | AuthenticationException e) {
if (!authenticated || e instanceof AuthenticationException) {
startAuthentication(); //2
} else {
accessDenied(); //3
}
}
3.认证
3.1 认证组件
SecurityContextHolder - 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限… 这些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
SecurityContext -从securitycontextHolder中获取,并包含当前已验证用户的身份验证。
Authentication -直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。可以见得,Authentication 在 spring security 中是最高级别的身份 / 认证的抽象。
GrantedAuthority-在Authentication(例如角色,范围等)上授予委托人的权限
AuthenticationManager-定义Spring Security的Filters如何执行身份验证的API 。
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,甚至,可能允许用户使用指纹登录,所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List
ProviderManager-AuthenticationManager的常用实现类。
ProviderManager 中的 List,会依照次序去认证,认证成功则立即返回,若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
AuthenticationProvider-用于ProviderManager执行特定类型的身份验证,最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。
用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在 Spring Security 中。提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法便是 retrieveUser,虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作用,返回一个 UserDetails。还需要完成 UsernamePasswordAuthenticationToken 和 UserDetails 密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的,如果这个 void 方法没有抛异常,则认为比对成功。
Request Credentials with AuthenticationEntryPoint -用于从客户端请求凭证(即,重定向到登录页面,发送WWW-Authenticate响应等)
AbstractAuthenticationProcessingFilter-用于认证的基础。这也为高级身份验证流程以及各个部分如何协同工作提供了一个好主意。
3.2认证机制
Username and Password - 使用用户名和密码来进行身份验证
OAuth 2.0 Login - 使用OpenID Connect和非标准OAuth 2.0登录(即GitHub)登录的OAuth 2.0
SAML 2.0 Login - SAML 2.0 Log In
Central Authentication Server (CAS) - 中央身份验证服务器(CAS)支持
Remember Me - 将用户信息保存在cookie中,在浏览器关闭后重新打开,用户再去访问hello接口,此时会携带cookie中的remember-me到服务端,服务端拿到值以后,可以方便的计算出用户名和过期时间,再根据用户名查询到用户密码,然后通过MD5散列函数计算出散列值,再将计算出的散列值和浏览器传递来的散列值进行对比,就能确认这个令牌是否有效。
JAAS Authentication - 使用JAAS进行认证
OpenID - OpenID身份验证(请勿与OpenID Connect混淆)
Pre-Authentication Scenarios - 使用诸如SiteMinder或Java EE安全性之类的外部机制进行身份验证,但仍使用Spring Security进行授权和防范常见漏洞。
X509 Authentication -X509验证
3.3 SecurityContextHolder
如何设置SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext(); //1
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");//2
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);//3
<1>:我们首先创建一个空的SecurityContext,重要的是创建一个新SecurityContext实例,而不是使用它SecurityContextHolder.getContext().setAuthentication(authentication)来避免跨多个线程的竞争条件。
<2>:接下来,我们创建一个新Authentication对象。Spring Security并不关心Authentication在上设置了哪种类型的实现SecurityContext。我们在这里使用TestingAuthenticationToken它是因为它非常简单。更常见的生产方案是UsernamePasswordAuthenticationToken(userDetails, password, authorities)。
<3>:最后,我们可以在SecurityContext上设置SecurityContext。
如果想访问当前认证的用户,可以参考如下代码:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
缺省情况下,会SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着SecurityContext即使SecurityContext未将显式地传递给这些方法的参数,该方法也始终可用于同一执行线程中的方法。ThreadLocal如果在处理当前委托人的请求之后要清除线程,则以这种方式使用是非常安全的。Spring Security的FilterChainProxy确保SecurityContext is always cleared.
3.4 SecurityContext
从SecurityContextHolder中所得。该SecurityContext包含认证对象的所有信息。
3.5 Authentication
Authentication在Spring Security中有两个主要目的:
- AuthenticationManager的输入,提供用户提供的用于身份验证的凭据。在此场景中使用时,isAuthenticated()返回false。
- 表示当前已验证的用户。当前的身份验证可以从SecurityContext中获得。
接口源码如下:
package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); //1
Object getCredentials();// 2
Object getDetails();// 3
Object getPrincipal();// 4
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
(1):权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。
(2):密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(3):细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。
(4):敲黑板!!!最重要的身份信息,大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。
3.6 GrantedAuthority
GrantedAuthoritys是授予用户的高级权限。主要是角色或范围。
可以从authentication.getauthoritys()方法获得GrantedAuthoritys。此方法提供授予的权限对象的集合。这些权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。
3.7 AuthenticationManager
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,甚至,可能允许用户使用指纹登录,所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的实现类去完成认证工作。
3.8 ProviderManager
ProviderManager是AuthenticationManager最常用的实现。ProviderManager委托给AuthenticationProvider列表。每个AuthenticationProvider都有机会指示身份验证应该成功、失败,或者指示它不能做出决定并允许下游的AuthenticationProvider作出决定。在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。如果所有配置的authenticationprovider都不能进行身份验证,那么身份验证将失败,出现ProviderNotFoundException,这是一个特殊的AuthenticationException,表明ProviderManager不支持Aut类型。
事实上,多个ProviderManager实例可能共享同一个父AuthenticationManager。在有多个SecurityFilterChain实例的场景中经常见到,这些实例有一些共同的身份验证(共享的父AuthenticationManager),但也有不同的身份验证机制(不同的ProviderManager实例)。
默认情况下,ProviderManager将尝试从成功的身份验证请求返回的Authentication对象中清除任何敏感的凭据信息。这可以防止密码等信息在HttpSession中保留的时间超过必要的时间。
例如,当您使用用户对象的缓存来提高无状态应用程序的性能时,这可能会导致问题。如果身份验证包含对缓存中的对象的引用(例如UserDetails实例),并且该对象的凭据已被删除,则不再能够根据缓存的值进行身份验证。如果使用缓存,则需要考虑这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中复制对象。
3.9 AuthenticationProvider
可以将多个AuthenticationProvider注入到ProviderManager中。每个AuthenticationProvider执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider支持对JWT令牌进行身份验证。
AuthenticationProvider 最最最常用的一个实现便是 DaoAuthenticationProvider。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。
接下来分析DaoAuthenticationProvider是如何认证用户的?
org.springframework.security.authentication.dao.DaoAuthenticationProvider
//1.根据用户名加载用户
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
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) {
//各种异常处理
}
//...省略剩下的catch块
}
//2.完成密码的比对工作
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
在 Spring Security 中。提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法便是 retrieveUser,虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作用,返回一个 UserDetails。还需要完成 UsernamePasswordAuthenticationToken 和 UserDetails 密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的,如果这个 void 方法没有抛异常,则认为比对成功。
3.10 AuthenticationEntryPoint
AuthenticationEntryPoint 是认证的入口点,用于发送HTTP响应,以从客户端请求凭据。
有时,客户端会主动包含凭据(例如用户名/密码)以请求资源。在这些情况下,Spring Security无需提供HTTP响应即可从客户端请求凭证,因为它们已包含在内。
如果ExceptionTranslationFilter检测到 AuthenticationException(认证异常),则将会交给内部的 AuthenticationEntryPoint 去处理,如果检测到 AccessDeniedException(访问异常),需要先判断当前用户是不是匿名用户,如果是匿名访问,则和前面一样运行 AuthenticationEntryPoint,否则会委托给 AccessDeniedHandler 去处理,而 AccessDeniedHandler 的默认实现,是 AccessDeniedHandlerImpl。所以 ExceptionTranslationFilter 内部的 AuthenticationEntryPoint 是至关重要的。
比如说,客户端将向未经授权访问的资源发出未经身份验证的请求。在这种情况下,AuthenticationEntryPoint的实现用于从客户机请求凭据。AuthenticationEntryPoint实现可能会重定向到登录页面,响应一个WWW-Authenticate头。
3.11 AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter用作对用户凭证进行身份验证的基础过滤器。在认证凭证之前,Spring Security通常使用AuthenticationEntryPoint请求凭证。
接下来,AbstractAuthenticationProcessingFilter可以验证提交给它的任何身份验证请求。
(1):当用户提交其凭证时,AbstractAuthenticationProcessingFilter从HttpServletRequest创建一个要进行身份验证的Authentication。创建的Authentication类型取决于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter根据在HttpServletRequest中提交的用户名和密码创建UsernamePasswordAuthenticationToken。
(2):接下来,将Authentication传递到AuthenticationManager中进行身份验证。
(3):如果身份验证失败,则将该Authentication从SecurityContextHolder中被清除出去,RememberMeServices.loginFail被调用(如果开启了记住我功能),AuthenticationFailureHandler 被调用。
(4):如果身份验证成功,则
- SessionAuthenticationStrategy 收到新登录通知;
- 该Authentication被设置在SecurityContextHolder中。稍后将SecurityContextPersistenceFilter保存SecurityContext到HttpSession;
- RememberMeServices.loginSuccess被调用(如果开启了记住我功能);
- ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent。
3.12 Username/Password Authentication
验证用户身份的最常见方法之一是验证用户名和密码。这样,Spring Security为使用用户名和密码进行身份验证提供了全面的支持。
读取用户名和密码
Spring Security提供了以下内置机制,用于从中读取用户名和密码HttpServletRequest:
- 表单登录
- 基本认证
- 摘要式身份验证
储存机制
- 用于读取用户名和密码的每种受支持的机制都可以利用任何受支持的存储机制:
- 带有内存身份验证的简单存储
- 具有JDBC身份验证的关系数据库
- 使用UserDetailsService的自定义数据存储
- 具有LDAP认证的 LDAP存储
表单登录
- 首先,用户向未经授权的资源发出未经身份验证的请求/private。
- Spring Security 通过抛出AccessDeniedException来表示FilterSecurityInterceptor拒绝未认证的请求。
- 由于用户未进行身份验证,ExceptionTranslationFilter启动启动身份验证,并使用配置的AuthenticationEntryPoint向登录页面发送重定向。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的实例。
- 然后,浏览器将请求将其重定向到的登录页面。
- 应用程序中的某些内容必须呈现登录页面。
提交用户名和密码等信息后,将对用户名和密码进行UsernamePasswordAuthenticationFilter身份验证。该UsernamePasswordAuthenticationFilter扩展AbstractAuthenticationProcessingFilter,所以这张图看起来应该非常相似。
当用户提交其用户名和密码时,UsernamePasswordAuthenticationFilter会通过UsernamePasswordAuthenticationToken从中Authentication提取用户名和密码来创建,这是一种HttpServletRequest。
接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager外观的细节取决于用户信息的存储方式。
如果身份验证失败,则将该Authentication从SecurityContextHolder中被清除出去,RememberMeServices.loginFail被调用(如果开启了记住我功能),AuthenticationFailureHandler 被调用。
如果身份验证成功,则
- SessionAuthenticationStrategy 收到新登录通知;
- 该Authentication被设置在SecurityContextHolder中。稍后将SecurityContextPersistenceFilter保存SecurityContext到HttpSession;
- RememberMeServices.loginSuccess被调用(如果开启了记住我功能);
- ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent。
基本认证
- 首先,用户向未经授权的资源发出未经身份验证的请求。
- Spring Security 通过抛出来FilterSecurityInterceptor表示未认证的请求被拒绝AccessDeniedException。
- 由于用户没有经过身份验证,ExceptionTranslationFilter启动Authentication进行身份验证。配置的AuthenticationEntryPoint是BasicAuthenticationEntryPoint的一个实例,它发送一个WWW-Authenticate头。RequestCache通常是一个NullRequestCache,它不保存请求。
当客户端收到WWW-Authenticate标头时,它知道应该使用用户名和密码重试。以下是正在处理的用户名和密码的流程。
基本流程同表单登录内存中身份验证
Spring Security的InMemoryUserDetailsManager实现UserDetailsService为在内存中检索的基于用户名/密码的身份验证提供支持。 通过实现接口InMemoryUserDetailsManager提供管理。
@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验证
Spring Security的JdbcDaoImpl实现了UserDetailsService,为使用JDBC检索的基于用户名/密码的身份验证提供支持。JdbcUserDetailsManager扩展了JdbcDaoImpl,通过UserDetailsManager接口提供对用户详细信息的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails的身份验证。
准备数据库用于存放用户信息,权限信息,以及用户权限关联表,Spring Security为基于JDBC的身份验证提供默认查询以及与默认查询一起使用的相应默认架构(路径:org/springframework/security/core/userdetails/jdbc/users.ddl)
默认的用户模式:create table users( username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(50) 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);
设置数据源
配置JdbcUserDetailsManager让我们通过JDBC的方式将数据库和Spring Security连接起来。
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //userService是UserDetailsService的实现类 auth.userDetailsService(userService); }
UserDetails
public interface UserDetails extends Serializable {
//获取用户权限集
Collection<? extends GrantedAuthority> getAuthorities();
//获得用户名
String getUsername();
//账户是否过期
boolean isAccountNonExpired();
//账户是否锁定
boolean isAccountNonLocked();
//凭证是否过期
boolean isCredentialsNonExpired();
//账户是否可用
boolean isEnabled();
}
一般我们都是实现该接口,另外加上自己想要的信息,比如手机号,地址等等。
- UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
里面就一个方法,根据用户名返回用户信息,这里可以根据不同的验证方式来定制我们自己所需的UserDetailsService实现类。UserDetailsService由DaoAuthenticationProvider用于检索用户名、密码和其他属性,用于使用用户名和密码进行身份验证。Spring Security提供了基于内存和JDBC的UserDetailsService的实现类。
@Service
public class UserService implements UserDetailsService {
@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findUserByUsername(username);
if(user==null){
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
- 密码编码器
可以根据需要选择PasswordEncoder接口的实现类。
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- DaoAuthenticationProvider
DaoAuthenticationProvider是AuthenticationProvider的一种实现类,利用UserDetailsService和PasswordEncoder验证用户名和密码。
1、从读取用户名和密码的Authentication Filter将一个UsernamePasswordAuthenticationToken传递给由ProviderManager实现的AuthenticationManager。
2、ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider;
3、 DaoAuthenticationProvider从UserDetailsService中查找用户详细信息。
4、 然后,DaoAuthenticationProvider使用PasswordEncoder验证上一步返回的用户详细信息上的密码;
5、当身份验证成功时,返回的身份验证类型为UsernamePasswordAuthenticationToken,其主体是配置的UserDetailsService返回的用户详细信息。最终,返回的UsernamePasswordAuthenticationToken将由Authentication Filter在securitycontextHolder中设置
3.13 Session Management(会话管理)
HTTP会话相关的功能由SessionManagementFilter和SessionAuthenticationStrategy接口的组合来处理,该接口是过滤器委托给的。典型的用法包括会话固定保护攻击预防、会话超时检测和对已验证用户可以同时打开多少会话的限制。
超时检测
配置Spring Security来检测提交的无效会话ID,并将用户重定向到适当的URL。这是通过以下session-management元素实现的:
<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>
请注意,如果使用此机制来检测会话超时,则在用户注销然后重新登录而不关闭浏览器的情况下,它可能会错误地报告错误。这是因为在使会话无效时不会清除会话cookie,即使用户已注销,会话cookie也会重新提交。您可以通过在注销时显式删除JSESSIONID cookie,例如通过在注销处理程序中使用以下语法:
<http>
<logout delete-cookies="JSESSIONID" />
</http>
不幸的是,不能保证它可以与每个servlet容器一起使用,因此您需要在您的环境中对其进行测试,这里不展开讨论。
并发会话控制
如果您希望限制单个用户登录应用程序的能力,Spring Security会通过以下简单的补充来支持此功能。首先,您需要向文件中添加以下侦听器,web.xml以使Spring Security保持有关会话生命周期事件的最新信息:
@Bean
HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}
然后将以下行添加到您的应用程序上下文:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.sessionManagement()
.maximumSessions(1)
这将防止用户多次登录,第二次登录将使第一次登录无效。如果您希望避免再次登录,在这种情况下,您可以使用
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
然后,第二次登录将被拒绝。“拒绝”是指如果用户authentication-failure-url正在使用基于表单的登录名,则该用户将被发送到该页面。如果第二次身份验证是通过另一个非交互机制(例如“ remember-me”)进行的,则“未授权”(401)错误将发送给客户端。相反,如果要使用错误页面,则可以将属性添加session-authentication-error-url到session-management元素。
3.14 Remember-Me Authentication
“记住我”或“持久登录”身份验证是指网站能够记住会话之间的主体身份。通常,这是通过向浏览器发送一个cookie来实现的,该cookie在以后的会话中被检测到并引起自动登录。Spring Security提供了进行这些操作所需的钩子,并具有两个具体的“记住我”实现。一种使用散列来保留基于cookie的令牌的安全性,另一种使用数据库或其他持久性存储机制来存储生成的令牌。
请注意,两个实现都需要一个UserDetailsService。如果您正在使用不使用的身份验证提供程序UserDetailsService(例如LDAP提供程序),那么除非您UserDetailsService在应用程序上下文中也有bean,否则它将无法工作。
简单的基于哈希的令牌方法
这种方法使用哈希来实现有用的“记住我”策略。本质上,在成功进行交互式身份验证后,会将cookie发送到浏览器,该cookie的组成如下:
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
因此,“记住我”令牌仅在指定的期限内有效,并且前提是用户名,密码和密钥不变。值得注意的是,这存在潜在的安全问题,因为捕获的“记住我”令牌将可从任何用户代理使用,直到令牌到期为止。这与摘要身份验证相同。如果委托人知道已捕获令牌,则他们可以轻松更改密码并立即使所有出现问题的“记住我”令牌失效。如果需要更重要的安全性,则应使用下一节所述的方法。另外,根本不应该使用“记住我”服务。
开启“记住我”功能:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.rememberMe()
.key("yyj")
持久令牌方法
持久化令牌就是在基本的自动登录功能实现的基础上,又增加了新的校验参数,来提高系统的安全性。
在持久化令牌中,新增了两个经过MD5散列函数计算的校验参数,一个是series,另一个是token,其中series只有当用户在使用用户名/密码登录的时候才会生成或者更新,而token只要有新的会话就会重新生成,这就避免了一个用户同时在多端登录。
持久化令牌的具体处理类在PersistentTokenBasedRememberMeServices中。
首先我们需要一张表来记录令牌信息,这张表我们可以完全自定义,也可以使用系统默认提供的JDBC来操作,如果使用默认的JDBC,即JdbcTokenRepositoryImpl。
这里提供了一个JdbcTokenRepositoryImpl实例,并为其配置DataSource数据源,最后通过tokenRepository将JdbcTokenRepositoryImpl实例纳入配置中。
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.rememberMe()
.key("yyj")
.tokenRepository(jdbcTokenRepository())
3.15 Handling Logouts
注销Java配置
使用时WebSecurityConfigurerAdapter,将自动应用注销功能。默认设置是访问URL /logout,将通过以下方式注销用户:
- 使HTTP会话无效
- 清理配置的所有RememberMe身份验证
- 清除 SecurityContextHolder
- 重定向到 /login?logout
但是,与配置登录功能相似,您还可以使用各种选项来进一步自定义注销要求:
protected void configure(HttpSecurity http) throws Exception {
http
.logout(logout -> logout //1
.logoutUrl("/my/logout") //2
.logoutSuccessUrl("/my/index") //3
.logoutSuccessHandler(logoutSuccessHandler) //4
.invalidateHttpSession(true) //5
.addLogoutHandler(logoutHandler) //6
.deleteCookies(cookieNamesToClear) //7
)
...
}
- 提供注销支持。使用时会自动应用WebSecurityConfigurerAdapter。
- 触发注销的URL(默认为/logout)。如果启用了CSRF保护(默认),那么请求也必须是POST。
- 注销发生后重定向到的URL。默认值为/login?logout。
- 让我们指定一个定制的 LogoutSuccessHandler。如果指定,将logoutSuccessUrl()被忽略。
- 指定HttpSession在注销时是否使无效。默认情况下是这样。SecurityContextLogoutHandler在幕后进行配置。
- 添加一个LogoutHandler。默认情况下SecurityContextLogoutHandler被添加为最后一个LogoutHandler。
- 允许指定成功注销后将删除的cookie名称。这是CookieClearingLogoutHandler显式添加快捷方式。
通常,为了自定义注销功能,可以添加 LogoutHandler 和/或 LogoutSuccessHandler 实现。
注销成功处理程序
LogoutSuccessHandler在成功注销后,将调用LogoutFilter,以处理例如重定向或转发到适当的目的地,不过可能会引起异常。
有如下两种实现可供选择:
- SimpleUrlLogoutSuccessHandler
- HttpStatusReturningLogoutSuccessHandler
正如上面提到的,您不需要直接指定SimpleUrlLogoutSuccessHandler。相反,fluent API通过设置logoutSuccessUrl()提供了一个快捷方式。这将在幕后设置SimpleUrlLogoutSuccessHandler。在注销之后,提供的URL将被重定向到。默认是/login?logout
在REST API类型的场景中,HttpStatusReturningLogoutSuccessHandler非常有趣。LogoutSuccessHandler允许您提供要返回的纯HTTP状态代码,而不是在成功注销后重定向到URL。如果没有配置状态码200将默认返回。
3.16Authentication Events
对于成功或失败的每个身份验证,分别触发AuthenticationSuccessEvent或AuthenticationFailureEvent。
要收听这些事件,您必须首先发布AuthenticationEventPublisher。Spring Security DefaultAuthenticationEventPublisher可能会做得很好:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
然后,您可以使用Spring的@EventListener支持:
@Component
public class AuthenticationEvents {
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) {
// ...
}
@EventListener
public void onFailure(AuthenticationFailureEvent failures) {
// ...
}
}
添加异常映射
默认情况下,DefaultAuthenticationEventPublisher将发布以下事件的AuthenticationFailureEvent:
发布者进行精确Exception匹配,这意味着这些异常的子类也不会产生事件。
为此,您可能希望通过以下setAdditionalExceptionMappings方法向发布者提供其他映射:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
Map<Class<? extends AuthenticationException>,
Class<? extends AuthenticationFailureEvent>> mapping =
Collections.singletonMap(FooException.class, FooEvent.class);
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
return authenticationEventPublisher;
}
4.授权
pring Security中的高级授权功能代表了其受欢迎程度的最令人信服的原因之一。无论选择哪种身份验证方式(使用Spring Security提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成),您都会发现可以在应用程序中以一致且简单的方式使用授权服务。
4.1 授权架构
Authorities
之前讲Authentication时就提到了getAuthorities()方法用来返回GrantedAuthority集合。而这些GrantedAuthority集合就代表了已授予委托人的权限。授予权限对象由AuthenticationManager插入到身份验证对象中,然后在进行授权决策时由AccessDecisionManager 读取。
GrantedAuthority 是一个只有一个方法的接口:
String getAuthority();
此方法允许AccessDecisionManager获取授予权限的精确字符串表示形式。通过返回一个表示为字符串的形式,大多数AccessDecisionManager都可以轻松地“读取”授予的权限。如果授予的权限不能精确地表示为字符串,则认为授予的权限是“复杂的”,而getAuthority()必须返回null。
“复杂”授予权限的一个示例是存储应用于不同客户帐号的操作和权限阈值列表的实现。将这个复杂的授予权限表示为字符串非常困难,因此getAuthority()方法应该返回null。这将向任何AccessDecisionManager表明,它将需要专门支持GrantedAuthority实现,以便理解其内容。
Spring安全性包括一个具体的授予权限实现SimpleGrantedAuthority。这允许将任何用户指定的字符串转换为授予的权限。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority填充Authentication对象。
调用前处理
Spring Security提供了拦截器,用于控制对安全对象的访问,例如方法调用或Web请求。由做出关于是否允许进行调用的预调用决定AccessDecisionManager。
AccessDecisionManager
由AccessDecisionManager调用,AbstractSecurityInterceptor并负责做出最终的访问控制决策。该AccessDecisionManager接口包含三种方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
AccessDecisionManager的decide()方法被传递所有它需要的相关信息,以便进行授权决策。特别是,传递安全对象允许检查实际安全对象调用中包含的那些参数。例如,让我们假设安全对象是一个MethodInvocation。可以很容易地查询任何客户参数的MethodInvocation,然后在AccessDecisionManager中实现某种安全逻辑,以确保允许主体对该客户进行操作。如果请求被拒绝,则抛出AccessDeedException异常。
在启动时,supports(ConfigAttribute)由方法调用此方法AbstractSecurityInterceptor,以确定是否AccessDecisionManager可以处理传递的ConfigAttribute。supports(Class)安全拦截器实现调用该方法,以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全对象的类型,一般都默认返回true。
调用处理后
尽管在继续进行安全对象调用之前AccessDecisionManager由AbstractSecurityInterceptor调用了,但是某些应用程序需要一种修改安全对象调用实际返回的对象的方法。尽管您可以轻松实现自己的AOP问题来实现这一目标,但Spring Security提供了一个方便的挂钩,该挂钩具有几种与其ACL功能集成的具体实现。
与Spring Security的许多其他部分一样,AfterInvocationManager具有一个具体的实现AfterInvocationProviderManager,它轮询AfterInvocationProviders 的列表。每个AfterInvocationProvider都允许修改返回对象或抛出一个AccessDeniedException。实际上,由于前一个提供程序的结果将传递到列表中的下一个,因此多个提供程序可以修改对象。
请注意,如果你使用AfterInvocationManager,你仍然需要配置属性,让MethodSecurityInterceptor的AccessDecisionManager允许的操作。如果您使用的是典型的Spring Security包含的AccessDecisionManager实现,则没有为特定的安全方法调用定义配置属性,则将导致每个人AccessDecisionVoter都放弃投票。反之,如果AccessDecisionManager属性“ allowIfAllAbstainDecisions”为false,AccessDeniedException则会抛出一个。您可以通过(i)将“ allowIfAllAbstainDecisions”设置为true(尽管通常不建议这样做)或(ii)只需确保至少有一个配置属性AccessDecisionVoter将被投票授予访问权限来避免此潜在问题。后一种(推荐)方法通常是通过ROLE_USER或ROLE_AUTHENTICATED配置属性。
层次角色
通常要求应用程序中的特定角色应自动“包括”其他角色。例如,在具有“管理员”和“用户”角色概念的应用程序中,您可能希望管理员能够执行普通用户可以执行的所有操作。为此,您可以确保还为所有管理员用户分配了“用户”角色。或者,您可以修改每个需要“用户”角色也要包括“管理员”角色的访问约束。
使用角色层次结构可以配置哪些角色(或权限)应该包括其他角色。Spring Security的RoleVoter的扩展版本RoleHierarchyVoter配置了一个RoleHierarchy,从这个RoleHierarchy中可以获得分配给用户的所有“可到达的权限”。一个典型的配置可能是这样的:
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
<constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<property name="hierarchy">
<value>
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
</value>
</property>
</bean>
在这里,我们在层次结构中具有四个角色ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST。ROLE_ADMIN当AccessDecisionManager使用上述配置评估安全性约束时,通过身份验证的用户的行为就好像他们具有所有四个角色一样RoleHierarchyVoter。该>符号可以被认为是“包含”的意思。
角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。对于更复杂的要求,您可能希望在应用程序需要的特定访问权限与分配给用户的角色之间定义逻辑映射,并在加载用户信息时在两者之间进行转换。
4.2 使用FilterSecurityInterceptor授权HttpServletRequest
1、首先,FilterSecurityInterceptor从SecurityContextHolder中获得认证。
2、其次,FilterSecurityInterceptor创建一个FilterInvocation从HttpServletRequest,HttpServletResponse和FilterChain被传入FilterSecurityInterceptor。
3、其次,它通过FilterInvocation以SecurityMetadataSource获得ConfigAttributes。
4、最后,它将身份验证、FilterInvocation和ConfigAttributes传递给AccessDecisionManager。
5、如果授权被拒绝,AccessDeniedException则抛出。在这种情况下,ExceptionTranslationFilter将处理AccessDeniedException。
6、如果授予访问权限,则FilterSecurityInterceptor继续执行FilterChain,该链接可允许应用程序正常处理。
默认情况下,Spring Security的授权将要求对所有请求进行身份验证。显式配置如下所示:
每个请求都必须被认证:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
授权请求:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize //1
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() //2
.mvcMatchers("/admin/**").hasRole("ADMIN") //3
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") //4
.anyRequest().denyAll() //5
);
}
(1)指定了多个授权规则。每个规则均按其声明顺序进行考虑。
(2)我们指定了任何用户都可以访问的多个URL模式。具体来说,如果URL以“ / resources /”开头,等于“ / signup”或等于“ / about”,则任何用户都可以访问请求。
(3)任何以“/admin/”开头的URL都将被限制为具有“ROLE_ADMIN”角色的用户。您将注意到,由于我们调用hasRole方法,所以不需要指定“ROLE_”前缀。
(4)任何以“ / db /”开头的URL都要求用户同时具有“ ROLE_ADMIN”和“ ROLE_DBA”。您会注意到,由于我们使用的是hasRole表达式,因此无需指定“ ROLE_”前缀。
(5)任何尚未匹配的URL都会被拒绝访问。如果您不想意外忘记更新授权规则,这是一个很好的策略。
4.3 基于表达式的访问控制
常见的内置表达式:
注:以上文档主要来自Spring Security的官方文档,自己通过翻译软件翻译了一部分用于学习,其中也参考了一些大佬的博客,在这里推荐一位博主【https://www.cnkirito.moe/】,看了他的Spring Security系列文章,受益颇多,在此表示感谢。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!