前言:这是本人在学习 Spring Security技术栈开发企业级认证与授权 第四章 使用SpringSecurity开发基于表单的登陆 课程时做的笔记,供复习之用,不会很全.
目录
第一章 项目的创建
代码结构:
imooc-security没有固定的代码,只是为了让下面的四个子模块执行统一的命令.比如打包与测试.
imooc-security-core是我们的核心业务逻辑模块,要实现的
imooc-security-browser浏览器安全特定的代码
imooc-security-app app安全特定的代码
browser与app两个模块是我们提供出去的供其它项目使用的模块
imooc-security-demo演示其它项目如何使用我们提供的安全框架(browser与app)
1.1 imooc-security
ArtifactId写的是项目名,GroupId写的是包名.
package写成pom,因为本身并不包含Java的代码,只是用来打包的.剩下的四个pacakge都写成
1.2 imooc-security-core
1.3 imooc-security-browser
1.4 browser与app
第二章 SpringSecurity基本原理
2.1 基本使用
我们的目的是写出可重用的安全模块
在包下新建类BrowserSecurityConfig,并进行配置
formLogin表示用表单登陆进行身份认证,authorizeRequests表示下面的都是授权的配置,anyRequest表示任何请求,authenticated都需要身份认证.
总的来说,表示所有的请求都需要用表单进行身份认证.
默认的用户名是user,密码会在后台打出来.
如果不想用表单可以换成basic访问.
2.2 基本原理
SpringSecurity本质上是一个过滤器链,Springboot在启动时将filter都配置进去.
UsernamePasswordAuthenticationFilter进行表单登陆
BasicAuthenticationFilter处理httpbasic登陆
最核心的是绿色的过滤器,检查请求里有没有需要的信息,比如是不是一个登陆请求,在判断有没有带用户名和密码,如果没带就会放过去,如果带了会尝试登陆,如果还有其它的认证方式,会按照这个原理一直往下走,任何一个过滤器认证成功后会做一个标记,表示认证已经成功了,最后会到FilterSecurityIntercepter进行最后的判断,如果判断需要某个过滤器,那么就会检查对应的标记,判断的结果是过和不过,如果过就请求restful服务,如果不过就将异常抛出,ExceptionTranslationFilter会对异常做一些处理,如果是因为未登陆不能访问就问引导用户去登陆.实际运行的时候可能会有十几个过滤器,目前知道这三种就可以了.
绿色的过滤器能通过配置,另其生不生效,其它的过滤器不能进行控制,一定会在过滤器链,并在指定的位置.
2.3 自定义用户认证逻辑
2.3.1 处理用户信息获取逻辑
即将用户信息从数据库中读取出来,用户信息获取逻辑是封装在UserDetailsService中的,其中只有一个方法.
这个方法是根据输入的用户名到存储(数据库)中去读取一个用户信息,这个用户信息会被封装到UserDetails的实现类里,之后SpringSecurity会拿着用户信息去做一些相应的处理与校验,如果处理和校验都通过了,会将用户放在session中,表示通过了,如果用户名找不到用户,会抛出用户名不存在的异常,SpringSecurity捕获到异常后会显示出对应的信息.
username与权限一般来说是从数据库查找的,这里我们就直接写死了.
先用密码123登陆返回密码错误,再用123456登陆,登陆成功.
2.3.2 处理用户校验逻辑
第一方面是密码是否匹配,第二部分是其它的一些校验(用户冻结,密码过期)
详细看一下userDetails的接口
isAccountNonExpired(账户是否过期 false:已过期)
isAccountNonLocked 用户冻结(可恢复)
isCredentialsNonExpired(密码是否过期(安全度高的网站要求一段时间换一次密码))
isEnabled 用户是否被删除了(不能恢复)
我们可在上面通过建立实现UserDetails的user来描述查找到的用户,它有三个参数,现在我们新建有七个参数的User,多出来的四个参数代表上面的四个权限.
2.3.3 处理密码的加密解密
处理加密解密的是passwordencoder接口,其里面有两个方法
encode方法是对密码进行加密,mathes是判断加密后的密码和没加密的是否匹配.
具体配置方法:在BrowserSecurityConfig中添加PasswordEncoderBean.它的作用是将用户输入的密码与从数据库中读取的加密后的密码进行匹配.
2.4 总结
第三章 个性化用户认证流程
3.1 自定义登陆页面
加入loginPage,指定登陆的url,但是我们所有的请求都要身份认证,所以我们需要放过这个页面,当访问这样的一个url的时,允许访问,当访问剩下的其它的url时,必须要经过认证. 我们刚才讲到我们是用UsernamePasswordAuthenticationFilter来进行过滤的,但是它的路径只是/login,我们需要进行配置登陆的路径
loginPage指的是登陆的页面
表单登陆本来是由UsernamePasswordAuthenticationFilter这样的过滤器来处理的,在过滤器中处理的是/login,post类型的请求,所以我们要用loginProcessingUrl来指定处理请求的路径.
登陆发现不能登陆,为了让它通过,我们暂且把跨站请求伪造的功能(CSRF)disable掉
现在有两个问题
1.我们返回的是html,这是不和常理的,因为我们是restful服务,所以我们应该是返回状态码什么的.
2.我们这个项目最主要是能提供一个可重用的模块,所以我们应允许用户自定义模块.
3.1.1 处理不同的请求类型(解决第一个问题)
即如果是页面请求就返回登录页,如果是信息请求就告诉对方不能请求对应的信息(返回401状态码和错误信息)
1.进行配置
拿请求要从请求的缓存中来拿,是否需要身份认证是SpringSecurity做的,如果需要它会根据配置的跳转路径进行跳转,但是在跳转之前,它会用RequestCache把请求封装.
如果返回状态码的话restful是以json进行交互的,所以我们也应返回json,我们新建SimpleResponse来封装返回的信息.具体的页面跳转要交给前端.
3.1.2 用户自定义登陆页面(解决第二个问题)
我们在imooc-security-demo中会引用我们的安全模块(imooc-security-browser),我们想实现的效果是在demo项目中如果有配置,就用demo项目中的,如果没有,就用默认的(刚才写的imooc-signIn.html)
demo项目中的配置:
我们最终完成的安全模块会有20多个配置项,为了管理这些配置模块,我们需要有一个封装,把他们封装到类里面去.
我们把所有可配置的属性都放在SecurityProperties类中,根据配置种类的不同,这个SecurityProperties类中又会有许多小的配置类.如BrowserProperties中封装的是浏览器相关的项,validate中封装的是验证码相关的项.
最终我们将把这个配置写到我们的core模块中,因为这个配置无论是app模块还是browser模块都会用.
下面在core中进行配置
这段代码的意思是,这个类会读取配置文件中所有以imooc.security开头的配置项,其中browser的这些配置都会读取到browser中去,
为了让整个的配置类生效,我们还需要加一个类让我们的配置生效
在进行了上述配置后,我们再修改我们上面书写的在browser项目中的requireAuthentication类与BrowserSecurityConfig类.
requireAuthentication类:
给后面加上登陆html的路径
BrowserSecurityConfig类:
3.2 自定义成功处理
登陆可能不是有表单来进行登陆的,可能是ajax异步请求的.
我们只要实现AuthenticationSuccessHandler接口即可,这个接口只有一个方法.登陆成功会被调用.
Authentication封装我们的认证信息(认证请求的ip,认证请求的session以及认证请求通过后我们自己写的UserDetailService返回的Details即用户信息)
我们打印一下autentication都存在什么
我们还要改一下安全配置,让系统知道我们要用自己的写的SuccessHandler
首先将我们自己的写的SuccessHandler配置成Bean
将其注入到我们的配置中
最后我们将successHandler加到我们的配置中
打印的结果,principal就是我们自己写的details的一些信息.后面我们讲微信登陆,第三方登陆时principal中会包含一些别的信息.
3.3 自定义失败处理
和自定义成功处理相似,实现一个失败的接口,同成功的相似,接口中也是只有一个方法.没有antentication取而代之的是AuthenticationException对象,查看authentication的实现继承关系,可以看到有它有一大堆实现类,包括我们在上面提高的UsernameNotFoundException.每个错误都代表一种情况.
输出的结果:
3.4 进行重构,能让用户自己配置
现在回到我们一直强调的问题上来,我们一直想要实现的是一个通用的安全框架,但是我们这里把登陆和失败都写成了固定的,有些应用的前端就是jsp和一些其它的模板语言,登陆就是表单提交那种同步的方式,默认的登陆后跳转页面的方式可能更适合这种登陆方式.
1.
2.在BrowserProperties中增加一个loginType
3. 在ImoocAuthenticationSuccessHandler中讲properties注入,并继承spring默认的SuccessHandler类
4. 同样的逻辑我们来处理我们的失败处理器
父类的处理方法其实就是spring默认的方法,跳转到一个失败页上去.
第四章 认证流程源码级详解
4.1 认证处理流程
点击登陆后跳转到UsernamePasswordAuthenticationFilter,在这个类中会获取用户名和密码,然后用用户名和密码构建了UsernamePasswordAuthenticationToken这样一个对象,
这个对象从继承树上网上找,发现它是Authentication接口的实现,这个对象中讲用户名和密码保存在了本地变量,在这里有一个拿null调用super,我们看一下它父类的构造函数,父类的构造函数需要传一组权限过来,所以这里是空,setAuthenticated代表我当前的信息是否经过了身份认证,当然是false
在setDetails中我们会把一些请求信息封装到UsernamePasswordAuthenticationToken中,包括当前发请求的ip,session等等,
最后调用了AuthenticationManager,把我们带着用户和密码的token传进去
我们现在到了manager的内部,它是providerManager实现了AuthenticationManager的接口.在它里面有一个for循环,拿到AuthenticationProvider接口,真正的校验是写在AuthenticationProvider里面的,为什么这里是个集合要for循环?因为不同的登陆方式校验方式是不一样的,如果是表单登陆,那么要去验密码,如果是第三方登陆,这时候是不需要验密码的,AuthenticationManager负责把所有的AuthenticationProvider收集起来,循环起来,挨个去问,当前的provider支不支持我传进来的Authentication这个类型,根据provider类型会挑出一个provider来进行处理
如果支持,就回去执行我们的校验逻辑,我们最终挑选出的是daoAuthenticationProvider,它继承了AbstractUserDetailsAuthenticationProvider
我们找到AbstractUserDetailsAuthenticationProvider中的authenticate方法,首先它在这里调用方法获得了UserDetails对象user
retrieveUser这个方法是一个抽象方法,刚才我们找出的daoAuthenticationProvider实现了这个方法,并在实现类中通过我们提供UserDeailsService的实现获取了Userdetails,如果没拿到,则会抛出一些异常,
如果拿到了,会对user做一些预检查,并做了一些附加的检查,附加的检查在抽象类里就是一个方法,在daoAuthenticationProvider中是具体的实现
如果上面都通过了,在后面还有一个厚检查,就是检查第四个布尔的属性,如果都通过了就说明我们的认证是成功的
如果都通过了,说明我们的用户信息是成功的以及authentication传进来的认证请求的信息来创建一个successAuthentication
在这里面重新new了一个AuthenticationToken
最终返回了一个经过认证的authentication,再往下会调用successfulAuthentication,这个会调用我们自己写的successfulHandler,
在这个过程中的任何一步出现了异常,都会调用unsuccessfulAuthentication
4.2 认证结果如何在多个请求之间共享
既然要结果共享,那么一定是放在session里了,什么时候把什么东西放在session里,又是什么时候把东西读出来了.
在上面的successfuAuthenticationfilter中有一个
它把我们的authentication先放在context中,再放在SecurityContextHolder中,SecurityContextHolder实际上是ThreadLocal的一个封装,ThreadLocal是跟线程绑定的一个map,在同一个线程里,你在某个方法里向ThreadLocal中放的东西,在另外一个方法里是可以读出来的,可以理解为一个线程级的变量,一般来说,我们一个请求到响应的方法是在一个线程里来完成的,也就是说,我们在这里把authentication放在线程里去了,在整个的方法中,我们都能用authentication方法把它读取回来,谁来用它呢?是SeurityContextPersistenceFilter,在所有过滤器的最前面,作用有两个,第一个,请求进来先进它,请求进来的时候检查请求中是否有SecurityContext,如果有就把SecurityContext拿出来放到线程里,出去的时候最后一个过它,检查线程中有没有SecurityContext,有的话把请求放到session中.
4.3 获取认证用户信息
方法一:
返回结果:
方法二:
springMVC会自动到SpirngContext中去找,来进行封装
方法三:
如果感觉返回的信息有点多,按如下配置就只有principal对象了.