Spring Security是一款基于Spring的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架Apache Shiro相比,它拥有更为强大的功能。Spring Security也可以轻松的自定义扩展以满足各种需求,并且对常见的Web安全攻击提供了防护支持。如果你的Web框架选择的是Spring,那么在安全方面Spring Security会是一个不错的选择。
一、开启Spring Security
1、导入依赖
创建一个Spring Boot项目springboot-springsecurity,然后引入spring-boot-starter-security:
<!-- Spring Security的maven依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、创建Controller
新建包com.goldwind.controller,接下来我们创建一个TestController,对外提供一个/hello服务:
package com.goldwind.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: zy
* @Description: 测试
* @Date: 2020-2-9
*/
@RestController
public class TestController {
@GetMapping("hello")
public String hello() {
return "hello spring security";
}
}
3、新建App入口
新建入口程序App.java:
package com.goldwind;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author: zy
* @Description: 启动程序
* @Date: 2020-2-9
*/
@SpringBootApplication
public class App {
public static void main(String[] args){
SpringApplication.run(App.class,args);
}
}
这时候我们直接启动项目,访问http://localhost:8080/hello,可看到页面弹出了个formLogin认证框:
这个配置开启了一个form Login类型的认证,所有服务的访问都必须先过这个认证,默认的用户名为user,密码由Sping Security自动生成,回到IDE的控制台,可以找到密码信息:
Using generated security password: a77c9456-901e-4848-a221-3822347e52ea
输入用户名user,密码a77c9456-901e-4848-a221-3822347e52ea后,我们便可以成功访问/hello
接口。
二、基于HTTP basic类型的认证
我们可以通过一些配置将表单认证修改为基于HTTP Basic的认证方式。
1、配置Spring Security
创建包com.goldwind.config,创建一个配置类BrowserSecurityConfig继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter这个抽象类并重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web应用安全配置的适配器:
package com.goldwind.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @Author: zy
* @Description: spring security配置类
* @Date: 2020-2-9
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置拦截请求资源
* @param http:HTTP请求安全处理
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic() //HTTP Basic认证方式
.and()
.authorizeRequests() // 授权配置
.anyRequest() // 所有请求
.authenticated(); // 都需要认证
}
}
Spring Security提供了这种链式的方法调用。上面配置指定了认证方式为HTTP Basic登录,并且所有请求都需要进行认证。
这里有一点需要注意,我没并没有在Spring Security配置类上使用@EnableWebSecurity注解。这是因为在非Spring Boot的Spring Web MVC应用中,注解@EnableWebSecurity需要开发人员自己引入以启用Web安全。而在基于Spring Boot的Spring Web MVC应用中,开发人员没有必要再次引用该注解,Spring Boot的自动配置机制WebSecurityEnablerConfiguration已经引入了该注解,如下所示:
package org.springframework.boot.autoconfigure.security.servlet;
// 省略 imports 行
@Configuration
// 仅在存在 WebSecurityConfigurerAdapter bean 时该注解才有可能生效
// (最终生效与否要结合其他条件综合考虑)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
// 仅在不存在 springSecurityFilterChain 时该注解才有可能生效
// (最终生效与否要结合其他条件综合考虑)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
// 仅在 Servlet 环境下该注解才有可能生效
// (最终生效与否要结合其他条件综合考虑)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity // <====== 这里启用了 Web 安全
public class WebSecurityEnablerConfiguration {
}
实际上,一个Spring Web应用中,WebSecurityConfigurerAdapter可能有多个 , @EnableWebSecurity可以不用在任何一个WebSecurityConfigurerAdapter上,可以用在每个WebSecurityConfigurerAdapter上,也可以只用在某一个WebSecurityConfigurerAdapter上。多处使用@EnableWebSecurity注解并不会导致问题,其最终运行时效果跟使用@EnableWebSecurity一次效果是一样的。
这时候我们重启项目,再次访问http://localhost:8080/hello,可以看到认证方式已经是HTTP Basic的方式了:
用户名依旧是user,密码由Spring Security自动生成,如果需要换回表单的认证方式,我们只需要简单修改configure方法中的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.formLogin() // 表单方式
http.httpBasic() // HTTP Basic方式
.and()
.authorizeRequests() // 授权配置
.anyRequest() // 所有请求
.authenticated(); // 都需要认证
}
三、Spring Security基本原理
1、基本原理
上面我们开启了一个最简单的Spring Security安全配置,下面我们来了解下Spring Security的基本原理。通过上面的的配置,代码的执行过程可以简化为下图表示:
2、URI与Filter的匹配
下面介绍几个重要的过滤器:
- UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录验证,该过滤器默认只有当请求方法为post、请求页面为/login时过滤器才生效,如果想修改默认拦截url,只需在刚才介绍的Spring Security配置类WebSecurityConfig中配置该过滤器的拦截url:.loginProcessingUrl("url")即可;
- BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,实际上该过滤器是在UsernamePasswordAuthenticationFilter过滤器之前执行,当通过HTTP Basic方式登录时,默认会发送post请求/login,并且在请求头携带Authorization:Basic dXNlcjoxOWEyYWIzOC1kMjBiLTQ0MTQtOTNlOC03OThkNjc2ZTZlZDM=信息,该信息是登录用户名、密码加密后的信息,然后由BasicAuthenticationFilter过滤器解析后,传递给UsernamePasswordAuthenticationFilter过滤器进行认证;如果请求头没有Authorization信息,BasicAuthenticationFilter过滤器则直接放行;
- FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常;
- ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息;
想了解更多过滤器的信息可以参考这篇博客:springSecurity深度解析第二版,这篇博客绘制了一个更详细的Spring Security认证和权限校验的流程图:
四、Spring Security filter的构造和初始化
我们已经知道Spring Security通过构造层层filter来实现登录跳转、权限验证,角色管理等功能。这里我们将通过剖析Spring Security的核心源码来说明Spring Security的filter是如何开始构造的。我们可以下载Spring Security源码;
1、@EnableWebSecurity
我们知道要想启动Spring Security,必须配置注解@EnableWebSecurity,我们就从该注解说起:
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
2、WebSecurityConfiguration类
我们可以看到该注解导入了WebSecurityConfiguration类,进入该类查看:
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;
private Boolean debugEnabled;
private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
private ClassLoader beanClassLoader;
@Autowired(required = false)
private ObjectPostProcessor<Object> objectObjectPostProcessor;
@Bean
public static DelegatingApplicationListener delegatingApplicationListener() {
return new DelegatingApplicationListener();
}
@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
return webSecurity.getExpressionHandler();
}
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
/**
* Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP
* tag support.
* @return the {@link WebInvocationPrivilegeEvaluator}
*/
@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public WebInvocationPrivilegeEvaluator privilegeEvaluator() {
return webSecurity.getPrivilegeEvaluator();
}
/**
* Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>}
* instances used to create the web configuration.
*
* @param objectPostProcessor the {@link ObjectPostProcessor} used to create a
* {@link WebSecurity} instance
* @param webSecurityConfigurers the
* {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to
* create the web configuration
* @throws Exception
*/
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
...
}
@Bean
public static BeanFactoryPostProcessor conversionServicePostProcessor() {
return new RsaKeyConversionServicePostProcessor();
}
@Bean
public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
ConfigurableListableBeanFactory beanFactory) {
return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
}
/**
* A custom verision of the Spring provided AnnotationAwareOrderComparator that uses
* {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class
* instances for the {@link Order} annotation.
*
* @author Rob Winch
* @since 3.2
*/
private static class AnnotationAwareOrderComparator extends OrderComparator {
private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();
@Override
protected int getOrder(Object obj) {
return lookupOrder(obj);
}
private static int lookupOrder(Object obj) {
if (obj instanceof Ordered) {
return ((Ordered) obj).getOrder();
}
if (obj != null) {
Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass());
Order order = AnnotationUtils.findAnnotation(clazz, Order.class);
if (order != null) {
return order.value();
}
}
return Ordered.LOWEST_PRECEDENCE;
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.context.annotation.ImportAware#setImportMetadata(org.
* springframework.core.type.AnnotationMetadata)
*/
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableWebSecurityAttrMap = importMetadata
.getAnnotationAttributes(EnableWebSecurity.class.getName());
AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes
.fromMap(enableWebSecurityAttrMap);
debugEnabled = enableWebSecurityAttrs.getBoolean("debug");
if (webSecurity != null) {
webSecurity.debug(debugEnabled);
}
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.
* lang.ClassLoader)
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
如果我们忽略掉细节,只看最重要的步骤,该类主要实现了如下功能:
WebSecurityConfiguration类是作为一个Spring配置源,同时定义了许多bean,这里重点看setFilterChainProxySecurityConfigurer这个方法:
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}:
this.webSecurityConfigurers = webSecurityConfigurers;
}
下面我们对每一个步骤来做相应的源代码解释,首先我们来看一下方法得第二个参数:
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}"
可以看一下autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的源代码:
public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>();
Map<String, WebSecurityConfigurer> beansOfType = beanFactory
.getBeansOfType(WebSecurityConfigurer.class);
for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
webSecurityConfigurers.add(entry.getValue());
}
return webSecurityConfigurers;
}
我们如果调试代码,可以发现beanFactory.getBeansOfType用于获取类型为WebSecurityConfigurer或其子类的bean,在这里也就是获取到我们编写的WebSecurityConfig配置类:
我们可以看WebSecurityConfig类的类图:
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
当有多个配置项时进行排序,进行order重复验证:
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
遍历webSecurityConfigurers集合,调用webSecurity的apply方法,此时也会将我们自定义的WebSecurityConfig应用到 webSecurity.apply方法上:
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
最后,初始化webSecurityConfigurers属性:
this.webSecurityConfigurers = webSecurityConfigurers;
3、WebSecurity类
到这里我们知道了WebSecurityConfiguration类调用上述方法将我们配置的WebSecurityConfig类用WebSecurity类的apply方法关联起来,那么我们详细看看WebSecurity类的apply方法:
public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
add(configurer);
return configurer;
}
private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
Assert.notNull(configurer, "configurer cannot be null");
Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
.getClass();
synchronized (configurers) {
if (buildState.isConfigured()) {
throw new IllegalStateException("Cannot apply " + configurer
+ " to already built object");
}
List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
.get(clazz) : null;
if (configs == null) {
configs = new ArrayList<>(1);
}
configs.add(configurer);
this.configurers.put(clazz, configs);
if (buildState.isInitializing()) {
this.configurersAddedInInitializing.add(configurer);
}
}
}
从上述代码可知,实际上就是将我们定义的WebSecurityConfig配置类放入了WebSecurity类的一个LinkedHashMap中:
该LinkedHashMap在WebSecurity中属性名为configurers:
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
可以看到键就是我们定义的配置类的Clazz类型,而值是List<SecurityConfigurer<Filter, WebSecurity>>类型,是一个list集合,其中只有一个元素,就是我们编写的WebSecurityConfig配置类;
我们继续回到WebSecurityConfiguration类,查看它的另外一个重要的方法:
build的方法来自于WebSecurity的父类AbstractSecurityBuilder,该方法即为Spring Security构建Filter的核心方法,通过webSecurity的build方法构建了Spring Security的Filter:
实际上调用了AbstractConfiguredSecurityBuilder的doBuild:
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
init();
buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}
这里主要看WebSecurity的init方法和performBuild方法,首先看init方法,init方法将会遍历WebSecurity的LinkHashMap configurers中每个元素configurer,执行以下步骤:
调用configurer的init方法(这里的第一个元素就是我们编写的WebSecurityConfig配置类,这里的this指定就是WebSecurity):
在configurer中:init方法中又会调用getHttp方法:
在configurer中:getHttp方法首先创建一个HttpSecurity对象,用来初始化configurer的http成员:
private HttpSecurity http;
又调用 configure 方法,最终将会执行我们在WebSecurityConfig写的configure方法:
configurer中的init方法执行完之后,WebSecurity调用addSecurityFilterChainBuilder方法将configurer创建的HttpSecurity放入了WebSecurity的一个list集合里,该list集合属性名为securityFilterChainBuilders:
public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
this.securityFilterChainBuilders.add(securityFilterChainBuilder);
return this;
}
到目前为止,我们终于知道我们编写的WebSecurityConfig类的configure方法是如何被调用的了,但是仍有许多疑问没解开,比如HttpSecurity类的作用,Spring Security是如何通过HttpSecurity类构建一条拦截器链等。
这里我们先不分析HttpSecurity类的具体实现,再来看看WebSecurity的init方法执行后所执行的performBuild方法,该方法源码如下:
@Override
protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("
"
+ "********************************************************************
"
+ "********** Security debugging is enabled. *************
"
+ "********** This may include sensitive information. *************
"
+ "********** Do not use in a production system! *************
"
+ "********************************************************************
");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;
}
该方法执行的操作主要如下:
(1)、遍历WebSecurity的securityFilterChainBuilders列表,第一个元素也就是我们的WebSecurityConfig配置类创建的HttpSecurity对象,并执行该对象的build方法,构建SecurityFilterChain类;
然后将每个HttpSecurity对象构建的SecurityFilterChain对象添加到securityFilterChains列表中:
(2)、将List<SecurityFilterChain>集合构建成一个FilterChainProxy代理类,返回这个FilterChainProxy代理类;
到这里总的过程就非常明了,WebSecurityConfiguration的springSecurityFilterChain方法最终返回了一个FilterChainProxy代理类,作为Spring Security的顶层filter,而HttpSecurity主要用于注册和实例化各种filter,HttpSecurity类有个属性名为filters的List列表专门用于保存过滤器的。
到这里就分成了两路:
- 一路是HttpSecurity的build方法构建SecurityFilterChain类的原理;
- 一路是FilterChainProxy类的作用;
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
我们先从FilterChainProxy类开始。
4、FilterChainProxy类
当请求到达的时候,FilterChainProxy会调用dofilter方法,会遍历所有的SecurityFilterChain,对匹配到的url,则调用SecurityFilterChain中的每一个filter做认证授权。FilterChainProxy的dofilter()中调用了doFilterInternal方法,如下:
private List<SecurityFilterChain> filterChains;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
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);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
/ 通过遍历filterChains,调用SecurityFilterChain的matches方法,判断当前的请求对应哪些filter,返回匹配的filter列表
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
我们理清了FilterChainProxy类的作用,那么这些SecurityFilterChain是从哪来的呢?从上节可知SecurityFilterChain是由HttpSecurity的build方法生成的,下面我们分析下HttpSecurity类。
5、HttpSecurity类
HttpSecurity与WebSecurity一样,都继承了AbstractConfiguredSecurityBuilder类,而WebSecurity的build和doBuild方法和LinkedHashMap属性,均来自AbstractConfiguredSecurityBuilder,故HttpSecurity的build方法代码与WebSecurity的相同,区别在于LinkedHashMap存储的东西不同:
(1)、WebSecurityConfig类的configure方法
在之前我们已经介绍了WebSecurity的doBuild是如何调用我们自己写的配置类WebSecurityConfig的configure方法;在该方法中http所调用的方法,最终的结果就是产生我们先前提到的url-filter的关系映射:
protected void configure(HttpSecurity http) throws Exception { http.formLogin() //HTTP Basic认证方式 .and() .authorizeRequests() // 授权配置 .anyRequest() // 所有请求 .authenticated(); // 都需要认证 }
比如authorizeRequests(),formLogin()方法分别返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,他们都是SecurityConfigurer接口的实现类,分别代表的是不同类型的安全配置器。而这些安全配置器分别对应一个或多个filter:
- formLogin对应UsernamePasswordAuthenticationFilter,此外我们还可以给安全过滤器FormLoginConfigurer指定其它参数,比如.login("/login"):自定义登录请求页面;.loginProcessingUrl("/login")指定UsernamePasswordAuthenticationFilter拦截的Form action;
.and() .formLogin() // 或者httpBasic() .loginPage("/login") // 指定登录页的路径 //我们的form表单action是将请求提交到/login/mobile页面,而在Spring Security中配置的 .loginProcessingUrl("/login") 值为/login,这两者为什么不一样呢?这样做的目的是通过指定Spring Security中的UsernamePasswordAuthenticationFilter的拦截目标为post请求/login,从而使得该过滤器不会拦截/login/mobile请求;那么针对/login/mobile请求我们会仿照UsernamePasswordAuthenticationFilter定义自己的过滤器,然后对其进行认证; .loginProcessingUrl("/login") // 指定自定义form表单提交请求的路径
- authorizeRequests对应FilterSecurityInterceptor;
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
throws Exception {
ApplicationContext context = getContext();
return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context))
.getRegistry();
}
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
都调用了getOrApply方法,再来看getOrApply方法,又调用了其中的apply方法:
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
C configurer) throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}
apply方法又调用了add方法,这里的add方法最终还是将该configurer加入了LinkedHashMap中:
private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
Assert.notNull(configurer, "configurer cannot be null");
Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
.getClass();
synchronized (configurers) {
if (buildState.isConfigured()) {
throw new IllegalStateException("Cannot apply " + configurer
+ " to already built object");
}
List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
.get(clazz) : null;
if (configs == null) {
configs = new ArrayList<>(1);
}
configs.add(configurer);
this.configurers.put(clazz, configs);
if (buildState.isInitializing()) {
this.configurersAddedInInitializing.add(configurer);
}
}
}
故HttpSecurity在构建filter的过程中,本质还是将形如ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer等类加入了它的LinkedHashMap中,该list集合属性名为configurers :
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
这里有一点需要注意,在HttpSecurity的configurers列表中存放的元素都是继承自SecurityConfigurerAdapter类,而在WebSecurity的configurers列表中存放的元素都是继承自WebSecurityConfigurerAdapter类:
啥时候我们会使用到SecurityConfigurerAdapter类,在后面我们介绍手机验证码登录的时候会有一个案例:
如果想使得这个配置生效,我们只需要在WebSecurityConfig配置类的configure方法添加如下代码:
.apply(smsAuthenticationConfig); // 将短信验证码认证配置加到 Spring Security 中 添加一个安全配置其到http的configurers集合
调用http的apply方法,最终将该smsAuthenticationConfig加入了HttpSecurity的configurers列表中。
(2)、HttpSecurity的build方法构建SecurityFilterChain类的原理
那么将这些Configurer类存入LinkedHashMap的作用又是什么?
在前面我们已经说到通过调用HttpSecurity的build方法构建SecurityFilterChain类,而build方法封装了doBuild方法;
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); }
在我们回忆上面WebSecurity类的doBuild方法,我们知道HttpSecurity类调用的doBuild方法与WebSecurity类一样,而通过观察WebSecurity类doBuild方法里init;configure;这些语句的具体实现,实际就是遍历LinkedHashMap中的元素:
并调用其init方法和configure方法:
@Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result; } }
我们现在来查看其中一个ExpressionUrlAuthorizationConfigurer类的configure方法的详细代码:
@Override
public void configure(H http) throws Exception {
FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
if (metadataSource == null) {
return;
}
FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
http, metadataSource, http.getSharedObject(AuthenticationManager.class));
if (filterSecurityInterceptorOncePerRequest != null) {
securityInterceptor
.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
}
securityInterceptor = postProcess(securityInterceptor);
http.addFilter(securityInterceptor);
http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
}
最后来看看HttpSecruity的performBuild()方法:
@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
实际上就是通过HttpSecurity的filters集合构建了SecurityFilterChain。
从上面代码可总结出,HttpSecurity内部维护一个filter列表,而HttpSecurity调用形如authorizeRequests(),formLogin()等方法实际上就是将各种filter添加入它的列表当中,最后通过performBuild()方法构建出SecurityFilterChain,至此HttpSecurity构建filter的总过程就完成了。最后放一张概括图:
6、核心接口SecurityBuilder 与 SecurityConfigurer
上面我们提到的WebSecurity,HttpSecurity都是具体的类。现在,我们从更高的层面来说,从两个核心接口以及其实现类的类图来理解下。
public interface SecurityBuilder<O> {
/**
* Builds the object and returns it or null.
*
* @return the Object to be built or null if the implementation allows it.
* @throws Exception if an error occurred when building the Object
*/
O build() throws Exception;
}
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
/**
* Initialize the {@link SecurityBuilder}. Here only shared state should be created
* and modified, but not properties on the {@link SecurityBuilder} used for building
* the object. This ensures that the {@link #configure(SecurityBuilder)} method uses
* the correct shared objects when building. Configurers should be applied here.
*
* @param builder
* @throws Exception
*/
void init(B builder) throws Exception;
/**
* Configure the {@link SecurityBuilder} by setting the necessary properties on the
* {@link SecurityBuilder}.
*
* @param builder
* @throws Exception
*/
void configure(B builder) throws Exception;
}
- HttpSecurity是接口SecurityBuilder的实现类,HttpSecuirty内部维护了一个Filter的List集合,我们添加的各种安全配置器对应的Filter最终都会被加入到这个List集合中;
- WebSecurity也是接口securityBuilder的实现类,内部维护着SecurityBuilder的列表,存储SecurityBuilder,这里主要是存储HttpSecurity;
很多官方类是XXXConfigurer,这些都是SecurityConfigurer。这些SecurityConfigurer的configure()方法,都会把对应filter添加到HttpSecurity。
五、登录、验证流程分析
我们已经明白了Spring Security的filter的构造。下面我们来介绍一下filter的执行顺序(登录方式改回表单的方式)。
当我们启动项目访问http://localhost:8080/hello时:
(1)、由于BasicAuthenticationFilter、UsernamePasswordAuthenticationFilter默认只拦截/login post请求,因此过滤器会直接放行,代码直接跳转到FilterSecurityInteceptor上进行权限校验;
(2)、此时会调用父类的beforeInvocation进行权限校验:
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + getSecureObjectClass()); } Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (rejectPublicInvocations) { throw new IllegalArgumentException( "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. " + "This indicates a configuration error because the " + "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
this.obtainSecurityMetadataSource()会调用默认的权限资源管理器,由于我们配置了任何请求都需要经过授权:
.anyRequest() // 所有请求 .authenticated(); // 都需要认证
因此/hello需要的权限为authenticated,此处我们可以通过实现FilterInvocationSecurityMetadataSource接口配置自己权限资源管理器,通过查询数据库来实现权限的动态配置,感兴趣的可以阅读:SpringSecurity动态配置权限。
由于用户没有登录,经过AnonymousAuthenticationFilter匿名过滤器处理之后,我们从上下文中可以获取到用户的主体信息为:
此时匿名用户具有的权限为ROLE_ANONYMOUS;然后经过访问决策管理器判断用户有无权限:
this.accessDecisionManager.decide(authenticated, object, attributes);
Spring提供了3个决策管理器:
- AffirmativeBased 一票通过,只要有一个投票器通过就允许访问;
- ConsensusBased 有一半以上投票器通过才允许访问资源;
- UnanimousBased 所有投票器都通过才允许访问;
这里使用默认的决策管理进行判断有无权限,可以看到决策管理中在引入了投票器(AccessDecisionVoter)的概念,有无权限访问的最终觉得权是由投票器来决定的,这里权限无法通过:
会抛出权限拒绝的异常,该异常会被ExceptionTranslateFilter捕获;
(3)、由于用户未登录直接访问/hello,所以抛出用户未认证的异常,所以接下来跳转到Spring Security提供的默认登录页 GET:http://localhost:8080/login,最终请求被DefaultLoginPageGeneratingFilter拦截进行处理,返回默认的登录页面:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//登录失败
boolean loginError = isErrorPage(request);
//登出请求
boolean logoutSuccess = isLogoutSuccess(request);
//是否是登录请求
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.length());
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
isLoginUrlRequest(request)方法中,如果判断是/login请求则接下来生成默认的登录页面返回:
private boolean isLoginUrlRequest(HttpServletRequest request) {
return matches(request, loginPageUrl);
}
private boolean matches(HttpServletRequest request, String url) {
//首先判断是不是GET请求
if (!"GET".equals(request.getMethod()) || url == null) {
return false;
}
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
if (request.getQueryString() != null) {
uri += "?" + request.getQueryString();
}
if ("".equals(request.getContextPath())) {
return uri.equals(url);
}
return uri.equals(request.getContextPath() + url);
}
generateLoginPageHtml方法中生成默认登录页面:
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
boolean logoutSuccess) {
String errorMsg = "none";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
errorMsg = ex != null ? ex.getMessage() : "none";
}
}
StringBuilder sb = new StringBuilder();
sb.append("<html><head><title>Login Page</title></head>");
if (formLoginEnabled) {
sb.append("<body οnlοad='document.f.").append(usernameParameter)
.append(".focus();'>
");
}
if (loginError) {
sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
sb.append(errorMsg);
sb.append("</font></p>");
}
if (logoutSuccess) {
sb.append("<p><font color='green'>You have been logged out</font></p>");
}
if (formLoginEnabled) {
sb.append("<h3>Login with Username and Password</h3>");
sb.append("<form name='f' action='").append(request.getContextPath())
.append(authenticationUrl).append("' method='POST'>
");
sb.append("<table>
");
sb.append(" <tr><td>User:</td><td><input type='text' name='");
sb.append(usernameParameter).append("' value='").append("'></td></tr>
");
sb.append(" <tr><td>Password:</td><td><input type='password' name='")
.append(passwordParameter).append("'/></td></tr>
");
if (rememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(rememberMeParameter)
.append("'/></td><td>Remember me on this computer.</td></tr>
");
}
sb.append(" <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
");
renderHiddenInputs(sb, request);
sb.append("</table>
");
sb.append("</form>");
}
if (openIdEnabled) {
sb.append("<h3>Login with OpenID Identity</h3>");
sb.append("<form name='oidf' action='").append(request.getContextPath())
.append(openIDauthenticationUrl).append("' method='POST'>
");
sb.append("<table>
");
sb.append(" <tr><td>Identity:</td><td><input type='text' size='30' name='");
sb.append(openIDusernameParameter).append("'/></td></tr>
");
if (openIDrememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(openIDrememberMeParameter)
.append("'></td><td>Remember me on this computer.</td></tr>
");
}
sb.append(" <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
");
sb.append("</table>
");
renderHiddenInputs(sb, request);
sb.append("</form>");
}
sb.append("</body></html>");
return sb.toString();
}
(4)、当我们输入完用户名、密码点击登录时,将会发送post请求:http://localhost:8080/login,该请求页面是在form标签action中指定的,POST请求会提交用户名和密码登录信息,此时由UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter处理,简单来说就是从请求request中获取用户名和密码进行认证操作(这里有一点需要注意,默认情况下UsernamePasswordAuthenticationFilter该过滤器默认只有当请求方法为post、请求页面为/login时过滤器才生效,如果想修改其默认拦截页面,需要在BrowserSecurityConfig中配置该过滤器的拦截url:.loginProcessingUrl("url"),也就是说只有当form标签action指定的url和.loginProcessingUrl配置的相同时,UsernamePasswordAuthenticationFilter过滤器才生效):
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//当不是登录请求 POST:/login时则直接跳过
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
//如果是登录请求 POST:/login则进行验证处理
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//从请求中获取用户名密码进行校验
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
//校验失败则跳转到登录页面
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
//校验失败则跳转到登录页面
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//校验成功则跳转到成功之后页面
successfulAuthentication(request, response, chain, authResult);
}
在doFilter方法中,首先会调用requiresAuthentication方法判断是不是登录请求,如果不是则直接跳过这个Filter,如果是则进行身份验证:
如果请求是登录操作,则接下来进行身份验证相关的操作,
UsernamePasswordAuthenticationFilter处理表单方式的用户认证。在UsernamePasswordAuthenticationFilter的attemptAuthentication方法上打个断点: 调用authResult = attemptAuthentication(request, response)方法进行验证,在attemptAuthentication中会从请求中获取用户名和密码构建UsernamePasswordAuthenticationToken :
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取用户名密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//调用AuthenticationManager的实现类进行校验
return this.getAuthenticationManager().authenticate(authRequest);
}
(5)、在接口AuthenticationManager的实现类ProviderManager调用authenticate方法进行校验操作。在authenticate方法中提供了一个List<AuthenticationProvider>,开发者可以提供不同的校验方式(用户名密码、手机号密码、邮箱密码等)只要其中有一个AutenticationProvider调用authenticate方法校验通过即可,当校验不通过时会抛出AuthenticationException ,当所有的AuthenticationProvider校验不通过时,直接抛出异常由ExceptionTranslationFilter捕捉处理,跳转到登录页面。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//可以提供多个验证器,只要其中有一个校验通过接口
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//进行校验,校验不通过则直接抛出AuthenticationException
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
//最终校验不通过则抛出异常,由ExceptionTranslationFilter捕捉处理
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
在接口AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider中调用authenticate进行用户名密码等的校验
- 首先从缓存userCahce中获取,如果获取不到,则从子类DaoAuthenticationProvider的retirveUser方法中获取,该方法利用loadUserByUsername函数根据用户名从数据库获得用户详情,如果获取不到则直接抛出异常;
- 将获取的user和authentication(从浏览器输入的用户和密码,用户名和密码会传递到Spring Security内部,最终封装成UsernamePasswordAuthenticationToken对象authentication)进行匹配,匹配成功调用createSuccessAuthentication方法创建Authentication返回。;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
//判断用户名和密码等信息是否完全匹配
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
在子类DaoAuthenticationProvider中调用接口的UserDetailsService的loadUserByUsername方法根据用户名来查找用户信息(在正式项目中实现此接口来完成从数据库等中查找),如果查找不到还是直接抛出异常,由上层去处理:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
//调用默认实现InMemoryUserDetailsManager查找用户名密码
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
当找到用户信息后,还需要根据用户信息和password字段进行匹配,在additionalAuthenticationChecks中完成完整的用户名和密码认证:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
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.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
(6)、当认证通过时,进行权限校验,执行FilterSecurityInterceptor代码的doFilter方法:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
在invoke方法中会调用父类AbstractSecurityInterceptor的beforeInvocation对Authentication对象进行权限校验:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//调用父类的beforeInvocation进行权限校验
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
在beforeInvocation中调用接口AccessDecisionManager的实现类,校验角色:
protected InterceptorStatusToken beforeInvocation(Object object) {
//省略部分代码
//获取当前线程的Authentication对象
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//权限角色校验
this.accessDecisionManager.decide(authenticated, object, attributes);
}
//校验失败抛出异常
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
//省略部分代码
}
最终调用子类AffirmativeBased的decide方法,在decide方法中会获取AccessDecisionVoter对权限进行投票处理,获取投票结果,当投票结果是1时则表示有权限,否则等于-1表示没有权限,拒绝:
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
//投票获取结果
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
//当有权限时直接返回
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
//否则拒绝
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
//
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
在实现类WebExpressionVoter会根据authentication结果进行校验判断,根据Authentication对象创建接口SecurityExpressionOperations的实现类SecurityExpressionRoot:
- 当请求url被配置为permitAll,则直接返回true,校验通过;
- 其他需要登录请求url会调用isAuthenticated方法,最终调用AuthenticationTrustResolver的AuthenticationTrustResolverImpl的方法进行判断;
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
protected final Authentication authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
/** Allows "permitAll" expression */
public final boolean permitAll = true;
/** Allows "denyAll" expression */
public final boolean denyAll = false;
private PermissionEvaluator permissionEvaluator;
public final String read = "read";
public final String write = "write";
public final String create = "create";
public final String delete = "delete";
public final String admin = "administration";
/**
* Creates a new instance
* @param authentication the {@link Authentication} to use. Cannot be null.
*/
public SecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
}
this.authentication = authentication;
}
public final boolean hasAuthority(String authority) {
return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
public final boolean hasRole(String role) {
return hasAnyRole(role);
}
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
public final Authentication getAuthentication() {
return authentication;
}
public final boolean permitAll() {
return true;
}
public final boolean denyAll() {
return false;
}
public final boolean isAnonymous() {
return trustResolver.isAnonymous(authentication);
}
public final boolean isAuthenticated() {
return !isAnonymous();
}
public final boolean isRememberMe() {
return trustResolver.isRememberMe(authentication);
}
public final boolean isFullyAuthenticated() {
return !trustResolver.isAnonymous(authentication)
&& !trustResolver.isRememberMe(authentication);
}
/**
* Convenience method to access {@link Authentication#getPrincipal()} from
* {@link #getAuthentication()}
* @return
*/
public Object getPrincipal() {
return authentication.getPrincipal();
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
/**
* <p>
* Sets the default prefix to be added to {@link #hasAnyRole(String...)} or
* {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN")
* is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is
* "ROLE_" (default).
* </p>
*
* <p>
* If null or empty, then no default role prefix is used.
* </p>
*
* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
*/
public void setDefaultRolePrefix(String defaultRolePrefix) {
this.defaultRolePrefix = defaultRolePrefix;
}
private Set<String> getAuthoritySet() {
if (roles == null) {
roles = new HashSet<String>();
Collection<? extends GrantedAuthority> userAuthorities = authentication
.getAuthorities();
if (roleHierarchy != null) {
userAuthorities = roleHierarchy
.getReachableGrantedAuthorities(userAuthorities);
}
roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return roles;
}
public boolean hasPermission(Object target, Object permission) {
return permissionEvaluator.hasPermission(authentication, target, permission);
}
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return permissionEvaluator.hasPermission(authentication, (Serializable) targetId,
targetType, permission);
}
public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
/**
* Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role
* does not already start with defaultRolePrefix.
*
* @param defaultRolePrefix
* @param role
* @return
*/
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;
}
}
在AuthenticationTrustResolverImpl方法isAnonymous中就是判断传递过来的Authentication对象是不是AnonymousAuthenticationToken,如果是AnonymousAuthenticationToken则表示没有登录,因为登录之后生成的对象是UsernamePasswordAuthenticationToken或其他Authentication对象,这也是Spring Security设计的最精华也是最难以理解也是最简单的方式。
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
public boolean isAnonymous(Authentication authentication) {
if ((anonymousClass == null) || (authentication == null)) {
return false;
}
return anonymousClass.isAssignableFrom(authentication.getClass());
}
总结:Spring Security对url的权限判断有两种方式,一种是请求是permitAll的则直接返回校验通过,另外一个是判断Authentication是不是AnonymousAuthenticationToken,因为正常登录等产生的不是这个对象,如果不是这个类型的对象则表示登录成功了。
(7)、如果权限通过,代码最终跳转到/hello上:
浏览器页面将显示hello spring security信息;
参考文章:
[1] Spring Boot中开启Spring Security(转载)
[2] Spring Security原理学习--用户名和密码认证(三)
[3] Spring Security原理学习--权限校验(四)
[4] Spring Security Config : 注解 EnableWebSecurity 启用Web安全
[5] Spring Security实现原理剖析(一):filter的构造和初始化
[6] Spring Security 实现原理的理解记录(推荐)