第九章:保护Web应用
9.1Spring Security简介
Spring Security是一种基于SpringAOP和Servlet规范中的Filter实现的安全框架。
Spring Security是基于Spring的应用程序提供声明式安全保护的安全性框架。能够在Web请求级别和方法调用级别处理身份认证和授权。Spring Security使用Filter保护Web请求并限制URL级别的访问,还能够使用Spring AOP保护方法调用。
Spring Security包含:
ACL(access control list):支持通过访问控制列表,为域对象提供安全性
切面(Aspects):允许使用基于AspectJ的切面而不是使用标准的SpringAOP
CAS(CAS Client):提供单点登录进行集成的功能
配置(Configuration):包含通过XML和Java配置Spring Security的功能支持
核心(Core):提供Spring Security基本库
加密(Cryptography):提供加密和密码编码功能
LDAP:支持基于LDAP进行认证
OpenID:支持使用OpenID进行集中式认证
Remoting:提供对Spring Remoting的支持
标签库(Tag Library):Spring Security的JSP标签库
Web:提供了Spring Security基于Filter的Web安全性支持
第一次尝试,导入最基本的Core、Configuration 使用Web和JSP标签库辅助。
DelegatingFilterProxy
首先要在Web.xml配置一个DelegatingFilterProxy过滤器,由于Filter是Java中的规范,本来是需要每一个都在web.xml中进行配置。该过滤器的目的是通过只配置DelegatingFilterProxy这一个,来避免其他Filter手动在web.xml进行配置。DelegatingFilterProxy是一个特殊的Servlet Filter,将工作委托给一个Filter实现类,这个实现类作为一个bean注册在Spring应用的上下文中。该类通过代理设计模式将Filter和Security随后配置的类联系起来。
在Web.xml下配置DelegatingFilterProxy如下:(必须在SpringMVC之前配置该过滤器)
<!-- Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置springMVC需要加载的配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<!-- 匹配所有请求,此处也可以配置成 *.do 形式 -->
<url-pattern>/*</url-pattern>
</servlet-mapping>
在Java类中配置DelegatingFilterProxy只需要扩展一个类:
AbstractSecurityWebApplicationInitializer中继承了WebApplicationInitializer(Spring启动时注册并调用onStartup)并在onStartup()方法中注册了DelegatingFilterProxy。AbstractSecurityWebApplicationInitializer还有insertFilters()和appendFilters()用来注册自己的Filter。
DelegatingFilterProxy会将所配置的URL拦截委托给ID为springSecurityFilterChain这个bean。SpringSecurity依赖一系列ServletFilter来提供不同的安全特性,这些在启动的时候会自动创建。
编写安全性配置
Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中。在Spring应用上下文中, 任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security
通过java配置,只需要在配置类上@EnableWebSecurity,使用SpringMVC时使用@EnableWebMvcSecurity。
WebSecurityConfigurerAdapter已经实现了WebSecurityConfigurer的方法,继承他并覆写我们需要自定义的方法即可。
@EnableWebMvcSecurity注解还配置了一个Spring MVC解析器(argument resolver) , 这样的话处理器方法就能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。它同时还配置了一个bean, 在使用Spring表单绑定标签库来定义表单时, 这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forgery, CSRF) token输入域。
要通过重载WebSecurityConfigurerAdapter中的一个或多个方法来实现自定义拦截功能。重载三个configure()方法来配置Web安全性。
configure(WebSecurity) 配置Spring Security的Filter链
configure(HttpSecurity)配置如何通过拦截器保护请求
configure(AuthenticationManagerBuilder)配置user-detail服务
现在没有重载任意一个方法,即相当于全部锁定,默认的configure(HttpSecurity)相当于如下配置:
http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
任意请求都被认证,支持基于表单登录,支持Basic方式的认证。
没有重载configure(AuthenticationManagerBuilder)方法, 所以没有用户存储支撑认证过程。
9.2选择查询用户详细信息的服务
需要用户存储,存储用户名、密码以及其他信息存储的地方,在进行认证决策的时候对其进行检索。Spring Security非常灵活,能够基于各种数据存储来认证用户。它内置了多种常见的用户存储场景,如内存、关系型数据库以及LDAP。但我们也可以编写并插入自定义的用户存储实现。
在内存中维护用户存储:
重载configure()方法, 并以AuthenticationManagerBuilder作为传入参数。通过inMemoryAuthentication()方法, 我们可以启用、配置并任意填充基于内存的用户存储。
如上,通过调用inMemoryAuthentication()就能启用内存用户存储。在内存中维护两个用户user和admin,设置其密码和权限。withUser()返回的是AuthenticationManagerBuilder,
AuthenticationManagerBuilder使用构造者风格的接口来构建认证配置。(简单来说就是每次调用方法都返回其自身以便重复调用)。roles()方法是authorities()方法的简写形式功能相同。roles()方法所给定的值都会添加一个“ROLE_”前缀, 并将其作为权限授予给用户。
.role(“USER”) 相当于. authorities(“ROLE_USER”)
配置用户方法还有如下其他方法:
accountExpired(boolean) 定义账号是否已经过期
accountLocked(boolean) 定义账号是否已经锁定
and() 用来连接配置
authorities(GrantedAuthority…) 授予某个用户一项或多项权限
authorities(List<? extends GrantedAuthority>) 授予某个用户一项或多项权限
authorities(String…) 授予某个用户一项或多项权限
credentialsExpired(boolean) 定义凭证是否已经过期
disabled(boolean) 定义账号是否已被禁用
password(String) 定义用户的密码
roles(String…) 授予某个用户一项或多项角色
基于数据库表进行认证
使用jdbcAuthentication()并注入Datasource。在下面这个文件夹中有默认的SQL语句。
即用户表是users、authorities、group_name、groups、group_authorities、group_members等这几个表,如果要修改,则必须重新编写自己的SQL。
其中比较重要的是:
//查询用户表中是否存在并启用,进行用户认证
Select username, password, enabled from users where username = ?
//查询用户权限
Select username, authority from authorities where username = ?
//查询用户在群组内的权限
Select g.id, g.group_name, ga.authority from group g, group_members gm, group_authorities ga where username = gm.username and g.id = ga.group_id and g.id = gm.group_id
如下,我把users和authorities合成一张表,此时手动编写SQL语句。
方法是一一对应的,还使用groupAuthoritiesByUsername()方法,查询群组权限。都是将用户名作为唯一的参数。
此时,登录页面会去校验数据库表中的数据,判断用户是否存在以及查询用户的权限。
如果在数据库中,密码需要加密,可以借助passwordEncoder()方法指定一个密码转换器,参数可以接收Spring Security中PasswordEncoder接口的任意实现。包括BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。如下:
也可以通过实现PasswordEncoder接口自定义密码转码器,实现接口中encode()和matches(),前者为加密算法。后者判断是否密码相同。
1、BCryptPasswordEncoder采用bcrypt进行加密,Security提供了一个BCrypt类进行bcrypt加密。
2、NoOpPasswordEncoder,什么都不做,相当于不加密。
3、StandardPasswordEncoder类,是PasswordEncoder接口的(唯一)一个实现类,-采用SHA-256算法,迭代1024次,使用一个密钥(site-wide secret)以及8位随机盐对原密码进行加密。 随机盐确保相同的密码使用多次时,产生的哈希都不同;
基于LDAP进行认证
LDAP为目录服务,类似于Unix的文件结构,树形结构,可以快速的进行数据读取,但写操作慢。没有事务处理能力,可以将其内容以文本格式(LDIF)返回。跨平台的Internate协议。
使用基于LDAP的认证,使用ldapAuthentication()方法。
方法userSearchFilter()和groupSearchFilter()用来为基础LDAP查询提供过滤条件, 它们分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP层级结构的根开始。 但是我们可以通过指定查询基础来改变这个默认行为:
声明用户应该在名为people的组织单元下搜索,而组应该在名为groups的组织单元下搜索。
基于LDAP进行认证的默认策略是进行绑定操作,直接通过LDAP服务器认证用户。 另一种可选的方式是进行比对操作。 这涉及将输入的密码发送到LDAP目录上, 并要求服务器将这个密码和用户的密码进行比对。因为比对是在LDAP服务器内完成的,实际的密码能保持私密。
如果希望通过密码比对进行认证,可以通过声明passwordCompare()方法来实现:
默认情况下, 在登录表单中提供的密码将会与用户的LDAP条目中的userPassword属性进
行比对。 如果密码被保存在不同的属性中, 可以通过passwordAttribute()方法来声明密码属性的名称:
在本例中, 我们指定了要与给定密码进行比对的是“passcode”属性。 另外, 我们还可以指定密码转码器。在进行服务器端密码比对时,有一点非常好, 那就是实际的密码在服务器端是私密的。但是进行尝试的密码还是需要通过线路传输到LDAP服务器上,这可能会被黑客所拦截。为了避免这一点,我们可以通过调用passwordEncoder()方法指定加密策略。在本示例中,密码会进行MD5加密。这需要LDAP服务器上密码也使用MD5进行加密。(即此时会当前密码加密后发给LDAP进行比对,发送过程中使用MD5加密,所以实际密码在LDAP中,不得而知)
引用远程的LDAP服务器
默认情况下,Spring Security的LDAP认证假设LDAP服务器监听本机的33389端口。 如果LDAP服务器在另一台机器上,使用contextSource()方法来配置这个地址:
contextSource()方法会返回一个ContextSourceBuilder对象,提供了url()方法用来指定LDAP服务器的地址。嵌入式的LDAP服务器,通过root()方法指定嵌入式服务器的根前缀就可以了:
当LDAP服务器启动时, 它会尝试在类路径下寻找LDIF文件来加载数据。 LDIF(LDAP数据交换格式) 是以文本文件展现LDAP数据的标准方式。 每条记录可以有一行或多行, 每项包含一个名值对。 记录之间通过空行进行分割。
如果你不想让Spring从整个根路径下搜索LDIF文件的话, 那么可以通过调用ldif()方法来明确指定加载哪个LDIF文件:
如下就是一个包含用户数据LDIF文件, 我们可以使用它来加载嵌入式LDAP服务器
dn: ou = groups,dc=habuma,dc=com
objectclass:top
objectclass:organizationalUnit
ou:groups
dn:ou=people,dc=habuma,dc=com
objectclass:top
objectclass:organizationalUnit
ou:people
dn:uid=habuma,ou=people,dc=habuma,dc=com
objectclass:top
objectclass:person
objectclass:organizationalPerson
objectclass:inetOrgPerson
cn:Craig Walls
sn:Walls
uid:habuma
userPassword:password
dn:uid=jsmith,ou=people,dc=habuma,dc=com
objectclass:top
objectclass:person
objectclass:organizationalPerson
objectclass:inetOrgPerson
cn:John Smith
sn:Smith
uid:jsmith
userPassword:password
dn:cn=spittr,ou=groups,dc=habuma,dc=com
objectclass:top
objectclass:groupOfNames
cn:spittr
member:uid=habuma,ou=people,dc=habuma,dc=com
配置自定义的用户服务
如果需要认证的用户存储在非关系型数据库中,如Mongo或Neo4j,需要提供一个自定义的UserDetailsService接口实现。在接口中,实现loadUserByUsername()方法。通过userDetailsService()方法将其设置到安全配置中:
在Service中自定义查询用户的方法。
返回的User是UserDetails的具体实现。也可以修改数据库返回的Do 让其实现UserDetails。
loadUserByUsername()就能直接返回该对象了, 而不必再将它的值复制到User对象中。
9.3拦截请求
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。 如下的代码片段展现了重载的configure(HttpSecurity)方法, 它为不同的URL路径有选择地应用安全性,展示了antMatchers重载的三个方法:
configure()方法中得到的HttpSecurity对象可以在多个方面配置HTTP的安全性。 在这里调用authorizeRequests(),然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。
最后对anyRequests()的调用中, 说明其他所有的请求都是允许的,不需要认证和任何的权限。authenticated()要求在执行该请求时,必须已经登录了应用。如果用户没有认证, Spring Security的Filter将会捕获该请求, 并将用户重定向到应用的登录页面。同时permitAll()方法允许请求没有任何的安全限制。
当前配置下,在浏览器输入URL:/test可以访问,/super可以访问(因为是get),/admin/*则不可以访问,报403错误,权限未被授予。
也能对URL进行身份的验证,例如需要ADMIN才能进入,hasRole和hasAuthority的区别是前者会自动增加ROLE_前缀。
regexMatchers()可以匹配正则,antMatchers()可以匹配ant通配。
如果使用了hasRole,则无法再对当前这个路径hasIpAddress()限制IP,所以需要借助access()方法,将SpEL作为声明访问限制的一种方式。
强制通道的安全性(HTTP和HTTPS)
敏感信息要通过HTTPS来加密发送。传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道。路径的选取方案与authorizeRequests()是相同的。
对login/login.do的请求,Spring Security都视为需要安全通道并自动将请求重定向到HTTPS上。使用requiresInsecure()代替requiresSecure()方法, 声明其他URL都通过HTTP传送:
使用Https请求只需要Http的URL或者使用Http请求需要Https的URL,SpringSecurity会自动将请求重定向到另外一个通道上。
防止跨站请求伪造
跨站请求伪造(cross-site request forgery, CSRF)如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击。从Spring Security 3.2开始, 默认就会启用CSRF防护。 实际上,除非你采取行为处理CSRF防护或者将这个功能禁用, 否则的话, 在应用中提交表单时, 你可能会遇到问题。
Spring Security通过一个同步token的方式来实现CSRF防护的功能。它将会拦截状态变化的请求(例如, 非GET、 HEAD、 OPTIONS和TRACE的请求) 并检查CSRF token。如果请求中不包含CSRF token的话, 或者token不能与服务器端的token相匹配, 请求将会失败, 并抛出CsrfException异常。
这意味着在应用中,所有的表单必须在一个“_csrf”域中提交token,而且这个token必须
要与服务器端计算并存储的token一致,才能进行匹配。
例如在JSP中进行CSRF域的声明:
<input type = “hidden” name=”$(_csrf.parameterName)” value=”$(_csrf.token)” />
可以在配置中通过调用csrf().disable()禁用Spring Security的CSRF防护功能。
9.4认证用户
在configure(HttpSecurity)方法中, 调用formLogin()即可获取到自带的简单登录页面。默认的登录页代码如下:
可以配置登录成功跳转页面或者登录失败跳转页面。
启用HTTP Basic认证
HTTP Basic认证(HTTP Basic Authentication) 会直接通过HTTP请求本身, 对要访问应用程序的用户进行认证。要启用HTTP Basic认证的话,只需在configure()方法所传入的HttpSecurity对象上调用httpBasic()即可。 另外, 还可以通过调用realmName()方法指定域。
启用Remember-me功能
Spring Security使得为应用添加Remember-me功能变得非常容易。需在configure()方法所传入的HttpSecurity对象上调用rememberMe()即可。默认情况下,这个功能是通过在cookie中存储一个token完成的, 这个token最多两周内有效。存储在cookie中的token包含用户名、 密码、 过期时间和一个私钥——在写入cookie前都进行了MD5哈希。 默认情况下, 私钥的名为SpringSecured,可以自定义名称。登录请求必须包含一个名为remember-me的参数。在登录表单中, 增加一个复选框:
此时,勾选上记住我则会生成一个cookie,使得关闭浏览器不用重新登录。
退出
退出功能是通过Servlet容器中的Filter实现的(默认情况下), 这个Filter会拦截“/logout”的请求。 因此,为应用添加退出功能只需添加如下的表单即可,需要添加Token,使用post。
也可以设置自定义登出的地址等,logout成功重定向的地址,例如重定向到首页:
9.5保护视图
使用security:authentication标签获得认证对象的信息:
在JSP中引入标签库:
<%@ taglib prefix=“security” uri=“http://www.springframework.org/security/tags” %>
可以在JSP中将用户的值放到域中,供别的页面获取:(放到session下的变量)
<security:authentication property=“principal.username” var=“loginUserName” scope=“session”/>
principal是用户的基本对象信息(用户存储中返回的User对象)。
如下:能获取到对应的信息,IP,SID,权限等。
条件性的渲染内容
使用security:authorize标签根据用户被授予的权限有条件地渲染页面的部分内容。
如下,使用Spel条件必须登录才渲染下面的内容,则不登录时并不会进行渲染。
使用url属性间接来引用一个URL的安全性约束,此时即与/admin/a11的拦截规则相同(可以配置上method属性来指定get或者post),可以只在配置中配置一次,页面引用:
ifAllGranted、ifAnyGranted和ifNotGranted属性来判断权限是否满足:
必须有ADMIN权限和USER权限才渲染,会自动获取到配置中的权限列表。
使用var属性来存储鉴权结果,方便其他地方引用。
鉴权的顺序是access > url > ifGranted
accesscontrollist标签是用于鉴定ACL权限的。