这段时间了解了一下spring security以及oauth 2.0的相关内容,特在此总结,以供之后查阅。spring oauth 2.0会另开一篇文章。
首先声明,这段时间看了很多资料,但是最后只是在宏观上大致的把握了一下方向,对于绝大部分的技术细节都没有去了解,毕竟时间有限。所以本文也就是能说明白spring security的简单工作原理,大概也就这个程度了。废话到此为止。
本文参考的文章很多,这里只列出比较有关键影响的。
spring security reference 5.0.6.RELEASE
徐靖峰的spring security系列
spring security——Spring Security Architecture
以及一篇可以用来提升英语阅读能力的文章 ;-)
我们首先看spring security,让我们从一篇官方文档开始看起,这篇文档给了我们一个非常简单的spring security的例子。
这篇文章中和安全有关的只有这一处的配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//inMemoryAuthentication 从内存中获取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("jason").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
}
}
在configure(HttpSecurity http)方法中我们进行了路径匹配等需要进行认证的资源等,还指定了对应的login页面什么的,可以说,这个方法的配置是使用spring security的最核心部分。
下面的configure(AuthenticationManagerBuilder auth)方法主要和认证管理相关,这里是对用户进行身份认证的地方,不过例子中我们仅仅在内存中设置了一个用户,所以只有这一个用户可以通过认证。
至此,一个简单的spring security其实已经完成了,很不可思议对吧,只做了这么点配置。
不过接下来,我们才应该真正看看spring security是如何工作的。
首先,spring security是由一系列过滤器链组成的,我想这个可能大家都从各方面文档了解到了。我们可以通过启动spring security的项目来查看相应的拦截器链都有什么:
这一行显示了加载的默认spring security拦截器链,其中较重要的有SecurityContextPersisitenceFilter
、LogoutFilter
、UsernamePasswordAuthenticationFilter
、AnonymousAuthenticationFilter
、FilterSecurityInterceptor
等。其他的相比之下不是那么重要。
关于这部分过滤器的解读可以参阅这篇文章。
仅仅知道这个过滤器链还是不够,我们需要在深入一点,最起码看看这一切到底是如何实现的。
首先我们需要了解一下SecurityContextHolder
,这个用于存储安全上下文的类,最常用的方式就是:
Authentication authentication = (Authentication)SecurityContextHolder.getContext().getAuthentication();
和安全有关的所有信息都由它保存,我们可以在需要的时候从这里取出相应的内容。注意,从SecurityContextHolder
中取出的也可能是匿名认证类AnonymousAuthenticationToken
;所以这里的处理需要注意,上面的写法就会出现类型转换异常。
接着我们可以通过Authentication
的getPrincipal()
方法得到一个UserDetails
,很多人可能对于Principal
一直感觉有点难以理解,我就是这样,不过我们可以看一些它的源码注释:
它表示的是一种主体的抽象概念,可以用来表示物体,个体,法人/公司,及登陆id。
用法类似下面:
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails details = (UserDetails) authentication.getPrincipal();
String username = details.getUsername();
......
}
我们可以看到,使用getPrincipal()
方法取出了一个UserDetails
;而UserDetails
是spring对身份信息的一个封装。
我们再看之前涉及的另一个接口,也是贯穿始终的Authentication
。
它继承了Principal
,是另一种抽象,代表身份认证的抽象。从这个接口我们可以获得用户的权限信息列表、密码、用户细节信息、用户身份信息、是否认证等。其中最常用的方法为上面介绍的getPrincipal()
。
我们尝试简单的模拟一下登陆的过程。
首先,我们在网站上输入用户名和密码,然后发送请求,用户名和密码被UsernamePasswordAuthenticationFilter
截获,而这个类继承了一个抽象类AbstractAuthenticationProcessingFilter
,这个抽象类才有过滤器的方法doFilter()
,我们可以看一下它的实现:
这两个划线部分是重点,第一部分是由UsernamePasswordAuthenticationFilter
实现的,我们看下它的实现:
我们看到这个方法返回了一个Authentication
,具体的过程是封装一个UsernamePasswordAuthenticationToken
,然后使用setDetails()
方法将具体信息都塞到这个Authentication
中(这部分可能有点绕,因为UsernamePasswordAuthenticationToken
继承AbstractAuthenticationToken
,而它是Authentication
的一个抽象实现类),接着就是用AuthenticationManager
通过authenticate()
方法认证这个UsernamePasswordAuthenticationToken
,并返回一个Authentication
;
接着我们看第一张图的第二个划线部分,这里的successfulAuthentication()
方法。
可以看到,认证成功后,我们就将得到的Authentication
通过SecurityContextHolder.getContext().setAuthentication()
方法设置到SecurityContextHolder
中,这也是为什么我们在前面可以通过SecurityContextHolder.getContext().getAuthentication()
得到Authentication
。
这样,认证就结束了,不过这里我们再看一下认证过程中比较关键的方法authenticate()
。
首先我们知道这个方法是AuthenticationManager
接口的方法,而实现这个接口的常用类为ProviderManager
,接着我们看一下它的authenticate()
方法:
我们看一下上面三个划线的位置,第一处是AuthenticationProvider.authenticate()
方法,我们看一下,找到它的具体实现类AbstractUserDetailsAuthenticationProvider
:
这些check()
方法是UserDetailsChecker
接口的
可以看到没有返回值,随便找一个实现类,可以看到它的原理就是不抛异常即成功。
而provider的authenticate()
方法绕来绕去,最后又返回了UsernamePasswordAuthenticationToken
(它是Authentication
的一个实现类),然后擦除凭证并发布认证成功事件。之后就是后面的放到SecurityContextHolder
中了。
到这里还没有结束,我们刚才并没有提到认证中的信息比对,直接说认证成功了,那么信息比对是发生在哪里的呢?
我们刚才提到过AbstractUserDetailsAuthenticationProvider
,我们回过头看它的authenticate()
方法,它是从缓存中取出UserDetails
的,那如果缓存中没有呢?
它将执行下面的retrieveUser()
方法,具体实现是由DaoAuthenticationProvider
来完成的:
到这里,我们就快要接触到核心了,我们看到了UserDetailsService接口,它的实现类分别为:
好,我们就到此为止吧,不再深入了,基本在这里已经可以看出端倪了,它负责加载用户信息,可以从内存、缓存或数据库中。
在最后,我们还要强调一点概念,Authentication
中的信息是认证信息,它的getCredentials()
方法获取的是之前提交的密码,而UserDetails
是真正的用户信息,它的getPassword()
方法才是用户正确的密码;之后的Authentication
中的getAuthorities()
是通过UserDetails
的getAuthorities()
传递而来的。
那么我们现在完整的捋一遍整个认证过程:
用户提交帐密,被封装为UsernamePasswordAuthenticationToken
,然后我们对它进行认证,比对相应的UserDetails
中的相关信息,认证成功后,我们将填充了信息的Authentication
(其实这里就是UsernamePasswordAuthenticationToken
,当然如果认证失败,这里的Token就会变成AnonymousAuthenticationToken
)放到了SecurityContextHolder
中,以供我们以后使用。
这里我就偷两张UML类图,可以更清晰的看到整个过程:
这两张图都引自徐靖峰的文章。
相应的权限关系都是在我们配置的configure(HttpSecurity http)方法中完成的,到时只需验证有无权限即可,这里权限认证没有找到比较好的资料,自己也实在是看不太懂,所以关于spring security的内容就写到这了。
下一篇,我们来看看spring security oauth 2.0.