SpringSecurity是一个权限验证的安装框架,有身份验证(用户名密码)和用户授权(权限)等功能.
快速上手
SpringSecurity在整合springboot的时候非常简单,只在maven文件中引入jar即可
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在日志中有密码,在访问restfull接口的时候,需要输入用户名和密码,
密码是在日志中提示的,用户名是user
Using generated security password: afdda172-b982-40aa-9552-18560c8e8ecc
进阶
快速上手是springsecurity默认的,如何定制化呢
需要实现WebSecurityConfigurerAdapter,首先实现类要用@Configuration
和@EnableWebSecurity标注
重写两个方法configure(HttpSecurity http)配置规则和configure(AuthenticationManagerBuilder auth)身份认证,从内存或者数据库中获取用户信息,configure(WebSecurity web)可以规定忽略哪些路径
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/hello").permitAll() // 对于hello路径放行
.anyRequest().authenticated()
.and()
.formLogin().and()
; //浏览器以form表单形式
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 用户信息存储在内存中
auth.inMemoryAuthentication().withUser("user")
.password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception{
//忽略/world路径
web.ignoring().antMatchers("/world");
}
@Bean
public PasswordEncoder passwordEncoder(){
// 官网建议的加密方式,相同的密码,每次加密都不一样,安全性更好一点
return new BCryptPasswordEncoder();
}
上面的示例,在内存中生成了用户,使用form表单的方式,放行了 hello和world路径的访问
filter
那springsecurity是如何工作的,就是依托于servlet的filter调用链,加上springsecurity的filter,这个filter中又维护了一个filter链,springsecurity终止自己的调用链后,继续执行servlet的filter
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter看这个方法就能理解security的filter的大致工作原理,currentPosition为security的filter自己调用链的数量,如果都执行完,就继续执行servlet的filter
springsecurit主要的filter为
--UsernamePasswordAuthenticationFilter 校验用户名密码,主要的作用为验证用户名密码,如果正确则退出springsecurity自己的过滤器链.
--ExceptionTranslationFilter 如果FilterSecurityInterceptor权限不通过,重定向到登入页
--FilterSecurityInterceptor 校验权限等
当然不止这三个,所有filter之间的order为100,目的是为了可以将自定义的filter放到容器中并指定顺序,
一般都是放到UsernamePasswordAuthenticationFilter 前后,加入自己的filter之后,访问都会执行整个调用链,也就都会访问自己添加的filter,所以需要考虑加判断,不满足条件直接跳过,执行下一个filter,获取或者终止调用链,直接跳过当前filter就是执行chain.doFilter(request, response);终止就是不执行
chain.doFilter(request, response);那调用链就结束了.如果要深入了解可以关注
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter
这里分析UsernamePasswordAuthenticationFilter ,因为我们自定义的filter都是遵循
UsernamePasswordAuthenticationFilter 的模式,比如想做一个手机验证.
UsernamePasswordAuthenticationFilter 是如何工作的,这里就不详细分析每一个细节了,大概分析思路,就可以扩展了.
UsernamePasswordAuthenticationFilter 是AbstractAuthenticationProcessingFilter的子类,在构造方法中初始化了AntPathRequestMatcher
image.png
意思是我只对login路径,方法为post进行拦截,自定义自己的filter的时候,就可以模仿这个构造器
doFilter方法在父类AbstractAuthenticationProcessingFilter中,可以看到调用了
attemptAuthentication方法,
authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter 的attemptAuthentication方法的返回值
return this.getAuthenticationManager().authenticate(authRequest);
this.getAuthenticationManager()返回的是ProviderManager,那看一下authenticate
方法中的for循环
for (AuthenticationProvider provider : getProviders())
for循环里关注两个代码,
if (!provider.supports(toTest))和result = provider.authenticate(authentication);
toTest是放进来的Token,这里指的是UsernamePasswordAuthenticationToken,
如果想要自定义,比如手机验证使用自定义mobileAuthenticationToken,
还需要提供自己的provider,因为getProviders()返回的是provider集合,那如何知道使用哪个provider,就是通过if (!provider.supports(toTest))
举例手机验证就能清晰一点
MobileAuthenticationToken 直接复制UsernamePasswordAuthenticationToken代码,改一下类名就可以,
MobileAuthenticationProvider继承AuthenticationProvider,实现两个方法
authenticate(Authentication authentication)和supports(Class<?> authentication)
这两个方法就对应上面说的for循环中的两行代码
authenticate(Authentication authentication)方法中,验证手机验证码是否正确,返回MobileAuthenticationToken 中this.authenticated要复制为true,证明已经验证成功了,这个非常重要,
创建MobileAuthenticationFilter,复制UsernamePasswordAuthenticationFilter就好,简单改一下就可以,构造方法中创建AntPathRequestMatcher,拦截路径/mobile/form,方法POST,那如何将自定义的filter放到springsecurity的调用链呢,最开始说的WebSecurityConfigurerAdapter子类重写的configure(HttpSecurity http)方法中
另一个要考虑的问题就是,登入之后其他路径是怎么被放行的呢,先讲解通过session的方式
image.png
这就是登入成功之后,可以被访问的原因,这是其中一个filter,SecurityContextPersistenceFilter,
通过sessionid从内存中获取MobileAuthenticationToken ,并放到SecurityContextHolder中,是ThreadLocal类型的变量,该线程就可以使用了,会在springsecurity的最后一个filter(FilterSecurityInterceptor),从SecurityContextHolder获取MobileAuthenticationToken,并判断
token中的authenticated是否为true,如果是则证明已经认证过,当然还有判断是否有访问该接口的权限,就不展开了.
说回UsernamePasswordAuthenticationToken,
流程就是先调用父类的AbstractAuthenticationProcessingFilter的doFilter
-->authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter#attemptAuthentication
-->this.getAuthenticationManager().authenticate(authRequest);
在for循环中调用
result = provider.authenticate(authentication);
UsernamePasswordAuthenticationFilter在调用attemptAuthentication方法的时候,使用UsernamePasswordAuthenticationToken,在for循环里调用provider的support方法,判断应该使用DaoAuthenticationProvider,provider.authenticate(authentication)方法其实是父类的authenticate方法,authenticate作用是通过调用DaoAuthenticationProvider#retrieveUser方法,获取用户信息,并将用户密码和前端输入的密码比较,如果成功之后,要返回UsernamePasswordAuthenticationToken,并将authenticated要为true,证明已经验证过了,这个在自定义filter的时候非常重要.
filter中还有认证成功处理器和认证失败处理器
image.png
如果是前后端分离的,就response写入信息就好了.
刚提到的DaoAuthenticationProvider#retrieveUser方法,获取用户信息,这也是一个扩展点,这种源码用到了成员变量,基本就是可以扩展的地方,流程就是默认给一个值,也可以程序员手动的赋值.
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
这里说的成员变量,就是 this.getUserDetailsService()的userDetailsService
创建一个类实现UserDetailsService,重写loadUserByUsername方法,该方法从数据库里获取用户信息,并封装为UserDetails返回即可.
流程就是这样,下篇文章实战,对上面说的能有个更好的理解.
网友评论