Spring Security部分详解
原文:https://www.2cto.com/kf/201808/765042.html
简介:
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
SpringSecurity核心功能:
认证(你是谁) 授权(你能干什么) 攻击防护(防止伪造身份)
1 核心组件
1.1 SecurityContextHolder
SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。
SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder的策略则需要被替换,鉴于我的初衷是基于web来介绍Spring Security,所以这里以及后续,非web的相关的内容都一笔带过。
获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
1 2 3 4 5 6 |
|
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。Authentication和UserDetails的介绍在下面的小节具体讲解,本节重要的内容是介绍SecurityContextHolder这个容器。
1.2 Authentication
先看看这个接口的源码长什么样:
1 2 3 4 5 6 7 8 9 |
|
<1> Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security包中的。可以见得,Authentication在spring security中是*别的身份/认证的抽象。
<2> 由这个*接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
还记得1.1节中,authentication.getPrincipal()返回了一个Object,我们将Principal强转成了Spring Security中最常用的UserDetails,这在Spring Security中非常常见,接口返回Object,使用instanceof判断类型,强转成对应的具体实现类。接口详细解读如下:
getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。 getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。 getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。 getPrincipal(),敲黑板!!!最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。UserDetails接口将会在下面的小节重点介绍。
Spring Security是如何完成身份认证的?
1 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2 AuthenticationManager 身份管理器负责验证这个Authentication
3 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
4 SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,其实只剩下一个AuthenticationManager 是我们没有接触过的了,这个身份管理器我们在后面的小节介绍。将上述的流程转换成代码,便是如下的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
注意:上述这段代码只是为了让大家了解Spring Security的工作流程而写的,不是什么源码。在实际使用中,整个流程会变得更加的复杂,但是基本思想,和上述代码如出一辙。
1.3 AuthenticationManager
初次接触Spring Security的朋友相信会被AuthenticationManager,ProviderManager ,AuthenticationProvider …这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。这样一来四不四就好理解多了?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。
只保留了关键认证部分的ProviderManager源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。
到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
1.4 DaoAuthenticationProvider
AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。由于本文是一个Overview,姑且只给出其UML类图:
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
1.5 UserDetails与UserDetailsService
上面不断提到了UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
1 2 3 4 5 6 7 8 9 |
|
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
1 2 3 |
|
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。
1.6 架构概览图
为了更加形象的理解上述我介绍的这些核心类,附上一张按照我的理解,所画出Spring Security的一张非典型的UML图
2 Spring Security Guides
2.1 引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
由于我们集成了springboot,所以不需要显示的引入Spring Security文档中描述core,config依赖,只需要引入spring-boot-starter-security即可。
2.2 创建一个不受安全限制的web应用
这是一个首页,不受安全限制
src/main/resources/templates/home.html
1 |
|
Welcome!
Click here to see a greeting.
这个简单的页面上包含了一个链接,跳转到”/hello”。对应如下的页面
src/main/resources/templates/hello.html
1 |
|
Hello world!
接下来配置Spring MVC,使得我们能够访问到页面。
首先在application.yml配置文件中配置thymeleaf模板的配置信息
1 2 3 4 5 6 7 8 |
|
配置WebSecurityConfig 继承WebSecurityConfigurerAdapter,配置自定义的登录页面及成功返回页面等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
控制层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
注意:使用@Controller,而不能使用@RestController。因为@RestController会导致返回的结果为Json串信息,而不是与前端模板封装的.HTML文件。
2.3 配置Spring Security
一个典型的安全配置如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
<1> @EnableWebSecurity注解使得SpringMVC集成了Spring Security的web安全支持。另外,WebSecurityConfig配置类同时集成了WebSecurityConfigurerAdapter,重写了其中的特定方法,用于自定义Spring Security配置。整个Spring Security的工作量,其实都是集中在该配置类,不仅仅是这个guides,实际项目中也是如此。
<2> configure(HttpSecurity)定义了哪些URL路径应该被拦截,如字面意思所描述:”/“, “/home”允许所有人访问,”/login”作为登录入口,也被允许访问,而剩下的”/hello”则需要登陆后才可以访问。
<3> configureGlobal(AuthenticationManagerBuilder)在内存中配置一个用户,admin/admin分别是用户名和密码,这个用户拥有USER角色。
我们目前还没有登录页面,下面创建登录页面:
1 |
|
Invalid username and password.
You have been logged out.
User Name :
Password:
这个Thymeleaf模板提供了一个用于提交用户名和密码的表单,其中name=”username”,name=”password”是默认的表单值,并发送到“/ login”。 在默认配置中,Spring Security提供了一个拦截该请求并验证用户的过滤器。 如果验证失败,该页面将重定向到“/ loginerror”,并显示相应的错误消息。 当用户选择注销,请求会被发送到“/ loginlogout”。
最后,我们为hello.html添加一些内容,用于展示用户信息。
1 |
|
Hello [[${#httpServletRequest.remoteUser}]]!
我们使用Spring Security之后,HttpServletRequest#getRemoteUser()可以用来获取用户名。 登出请求将被发送到“/ logout”。 成功注销后,会将用户重定向到“/ loginlogout”。
2.4 添加启动类
1 2 3 4 5 6 |
|
2.5 测试
访问首页https://localhost:8080/:
点击here,尝试访问受限的页面:/hello,由于未登录,结果被强制跳转到登录也/login:
输入正确的用户名和密码之后,跳转到之前想要访问的/hello:
点击Sign out退出按钮,访问:/logout,回到登录页面:
3 核心配置解读
3.1 功能介绍
这是Spring Security入门指南中的配置项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
当配置了上述的javaconfig之后,我们的应用便具备了如下的功能:
除了“/”,”/home”(首页),”/login”(登录),”/logout”(注销),之外,其他路径都需要认证。 指定“/login”该路径为登录页面,当未认证的用户尝试访问任何受保护的资源时,都会跳转到“/login”。 默认指定“/logout”为注销页面 配置一个内存中的用户认证器,使用admin/admin作为用户名和密码,具有USER角色 防止CSRF攻击 Session Fixation protection(可以参考我之前讲解Spring Session的文章,防止别人篡改sessionId) Security Header(添加一系列和Header相关的控制) HTTP Strict Transport Security for secure requests 集成X-Content-Type-Options 缓存控制 集成X-XSS-Protection.aspx) X-Frame-Options integration to help prevent Clickjacking(iframe被默认禁止使用) 为Servlet API集成了如下的几个方法 HttpServletRequest#getRemoteUser()) HttpServletRequest.html#getUserPrincipal()) HttpServletRequest.html#isUserInRole(java.lang.String)) HttpServletRequest.html#login(java.lang.String, java.lang.String)) HttpServletRequest.html#logout())
3.2 解读@EnableWebSecurity
我们自己定义的配置类WebSecurityConfig加上了@EnableWebSecurity注解,同时继承了WebSecurityConfigurerAdapter。你可能会在想谁的作用大一点,先给出结论:毫无疑问@EnableWebSecurity起到决定性的配置作用,他其实是个组合注解,背后SpringBoot做了非常多的配置。
---------------------------------------------------------------继续研究----------------------------------------------------------------------------------------------
配置用户认证逻辑,因为我们是有自己自定义的一套用户体系的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这里我们没有进行过多的校验,用户名可以随意的填写,但是密码必须得是“123456”,这样才能登录成功。
同时可以看到,这里User对象的第三个参数,它表示的是当前用户的权限,我们将它设置为”admin”。
UserDetails
刚刚我们在写MyUserDetailsService的时候,里面实现了一个方法,并返回了一个UserDetails。这个UserDetails 就是封装了用户信息的对象,里面包含了七个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
我们在返回UserDetails的实现类User的时候,可以通过User的构造方法,设置对应的参数。
运行一下程序进行测试,会发现登录界面有所改变
这是因为我们在配置文件中配置了http.formLogin()
我们这里随便填写一个User,然后Password写填写一个错误的(非123456)的。这时会提示校验错误:
同时在控制台,也会打印出刚才登录时填写的user
现在我们再来使用正确的密码进行登录试试,可以发现就会通过校验,跳转到正确的接口调用页面了。
密码加密解密
SpringSecurity中有一个PasswordEncoder接口
1 2 3 4 5 6 |
|
我们只需要自己实现这个接口,并在配置文件中配置一下就可以了。
这里我暂时以默认提供的一个实现类进行测试
1 2 3 4 5 |
|
加密使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
这里简单的对123456进行了加密的处理。我们可以进行测试,发现每次打印出来的password都是不一样的,这就是配置的BCryptPasswordEncoder所起到的作用。
自定义登录页面
完成了登录页面之后,就需要将它配置进行SpringSecurity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
处理不同类型的请求
因为现在一般都前后端分离了,后端提供接口供前端调用,返回JSON格式的数据给前端。刚才那样,调用了被保护的接口,直接进行了页面的跳转,在web端还可以接受,但是在App端就不行了, 所以我们还需要做进一步的处理。
这里做一下简单的思路整理
首先来写自定义的Controller,当需要身份认证的时候就跳转过来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
当然还需要将配置文件进行相应的修改, 这里我就不贴代码了。 就是将该接口开放出来 。
扩展:
这里我们是写死了如果是从网页访问的接口,那么就跳转到”/login.html”页面,其实我们可以扩展一下,将该跳转地址配置到配置文件中,这样会更方便的。
自定义处理登录成功/失败
在之前的测试中,登录成功了都是进行了页面的跳转。
在前后端分离的情况下,我们登录成功了可能需要向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。
这里涉及到了Spring Security中的两个接口AuthenticationSuccessHandler和AuthenticationFailureHandler。我们可以实现这个接口,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
这里我们通过response返回一个JSON字符串回去。
这个方法中的第三个参数Authentication,它里面包含了登录后的用户信息(UserDetails),Session的信息,登录信息等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
这个方法中的第三个参数AuthenticationException,包括了登录失败的信息。
同样的,还是需要在配置文件中进行配置,这里就不贴出全部的代码了,只贴出相应的语句
1 2 3 |
|
记住我功能的基本原理
“记住我”功能SpringSecurity源码分析
根据最开始的原理分析,我们可以知道一般是在认证成功之后,才会去做记住我的操作,所以我们可以看successfulAuthentication方法中的相关代码
1 2 3 4 5 6 7 8 9 10 11 |
|
这里在认证成功之后,调用了RememberMeServices的loginSuccess方法,该方法会进一步调用onLoginSuccess:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这样相关的数据库操作和Token操作就完成了。
接下来看看下次登录的时候,是怎么进行记住我认证的。这里我们就直接看RememberMeAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
------------------------------------------------------------再次继续研究--------------------------------------------------------------------------
1完整POM文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
|
2完整logback.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
|
3,完整application.yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|