保护Web应用
在这一章我们将使用切面技术来探索保护应用程序的方式。不过我们不必自己开发这些切面————我们将使用Spring Security,一种基于Spring AOP和Servlet规范的Filter实现的安全框架。
Spring Security简介
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。它能够在Web请求级别和方法调用级别处理身份认证和授权。充分利用了依赖注入和面向切面技术。
理解Spring security的模块
模块 | 描述 |
---|---|
ACL | 支持通过访问控制列表(ACL)为域对象提供安全性 |
切面 | 使用基于AspectJ的切面,而不是使用标准的SpringAOP |
CAS客户端 | 提供与Jasig的中心认证服务(CAS)进行集成的功能 |
配置(Configuration) | 包含通过XML和java配置Spring Security的功能支持 |
核心(Core) | 提供Spring Security基本库 |
加密 | 提供了加密和密码编码的功能 |
LDAP | 支持基于LDAP进行认证 |
OpenID | 支持使用OpenID进行集中式认证 |
Remoting | 提供了对Spring Remoting的支持 |
标签库 | Spring Security的JSP标签库 |
Web | 提供了Spring Security基于Filter的Web安全性支持 |
应用程序的类路径下至少要含有Core和Configuration这两个模块。
过滤Web请求
Spring security借助一系列Servlet Filter来提供各种安全性功能。DelegatingFilterProxy是一个特殊的Servlet Filter,它会将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring应用的上下文。如图所示:
xml实现:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DeleagatingFilterProxy
</filter-class>
</filter>
在这里,我们把<filter-name>设置成了springSecurityFilterChain。这是因为我们马上将Springsecurity配置在Web安全性中,。DelegatingFilterProxy会把过滤逻辑委托给名为springSecurityFilterChain的Filter bean。
Java配置:
import *;
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer{}
编写简单的安全性配置
启用Web安全性功能的最简单配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
}
@EnableWebSecurity注解将会启用Web安全功能。但它本身并没有什么用处。Springsecurity必须配置在一个实现了WebSecurityConfigurer的bean中或者扩展WebSecurityConfigurerAdapter。最为简单的方法还是刚刚的程序最为简单。
为SpringMVC启用Web安全性能的最简单配置
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
}
@EnableWebMvcSecurity注解还配置了一个Spring Mvc参数解析解析器,这样的话处理器方法就能够通过带有@AuthenticationPrincipal租借的参数获得认证用户的principal。
而以上的两个配置会将应用严格锁定,导致没有人能进入该系统了。这是我们要通过重载WebSecurityConfigurerAdapter的三个configure方法来配置Web安全性。
方法 | 描述 |
---|---|
configure(WebSecurity) | 通过重载,配置Spring Security的Filter链 |
configure(HttpSecurity) | 通过重载,配置如何通过拦截器保护请求 |
configure(AuthenticationManagerBuilder) | 通过重载,配置user-detail服务 |
为了让Springsecurity满足我们应用的需求,还需要再添加一点配置。具体来讲我们需要:
- 配置用户存储;
- 指定哪些请求需要认证,哪些不需要,以及所需要的权限;
- 指定一个自定义的登录页面,替代原先默认的登录页。
选择查询用户详细信息的服务
使用基于内存的用户存储
因为我们的安全配置是扩展了WebSecurityConfigurerAdapter,因此配置用户存储的最简单方式就是重载configure()方法,并以AuthenticationManagerBuilder作为传入参数。通过inMemoryAuthentication()方法,我们可以启用配置并任意填充基于内存的用户存储。
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER","ADMIN");
}
}
方法 | 描述 |
---|---|
accountExpired(boolean) | 定义账号是否已经过期 |
accountLocked(boolean) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority...) | 授予用户一项或多项权限 |
authorities(List<? extends GrantedAuthority>) | 授予用户一项或多项权限 |
authorities(String...) | 授予用户一项或多项权限 |
credentialExpired(boolean) | 定义凭证是否已经过期。 |
disabled(boolean) | 定义账号是否已被禁用 |
password(boolean) | 定义用户的密码 |
roles(String) | 授予用户一项或多项角色 |
基于数据库列表进行认证
为了配置SpringSecurity使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication
()方法,所需的最少配置:
@Autowired
DataSource datasource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource)
}
重写默认的用户查询功能
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username,password,true "+
"from Spitter where username=?")
.authortiesByUsernameQuery(
"select username,'ROLE_USER' from Spitter where username=?"
);
)
}
将默认的SQL查询替换为自定义的设计时,很重要得一点是要遵循查询的基本协议。所有的查询都以用户名为唯一的参数。认证查询会选取用户名、密码以及启用状态信息.
使用转码后的密码借助passwordEncoder()方法制定一个密码转码器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username,password,true "+
"from Spitter where username=?")
.authortiesByUsernameQuery(
"select username,'ROLE_USER' from Spitter where username=?")
.passwordEncoder(new StandardPasswordEncoder("53cr3t"));
}
配置自定义的用户服务
当我们需要认证的用户存储在非关系型数据库中,我们需要提供一个自定义的UserDetailsService接口实现。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
我们需要做的就是实现loadUserByUsername()方法
public class SpitterUserService implements UserDetailsService{
private final SpitterRepository spitterRepository;
public SpitterUserService(SpitterRepository spitterRepository){
this.spitterRepository = spitterRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
Spitter spitter = spitterRepository.findByUsername(username);
if(spitter !=null){
List<GrantedAuthority> authorities =
new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_SPITTER"));
return new User(
spitter.getUsername(),
spitter.getPassword(),
authorities);
throw new UsernameNotFoundException(
"User '" + username + "' not found."
);
}
}
}
为了使用SpitterUserService来认证用户,我们可以通过userDetailsService()方法将其设置到安全配置中:
@Autowired
SpitterRepository spitterRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.userDetailsService(new SpitterUserService(spitterRepository));
}
拦截请求
在应用中不睡所有的请求都需要同等程度的保护,而对每个请求进行安全性控制的关键在于重载configure(HttpSecurity)方法。如下示例:
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/spitters/me").authenticated()
.antMatchers(HttpMethod.POST,"/spittles").authenticated()
.anyRequest().permitAll();
}
antMatchers()方法用来指定路径。
下表例举了用来定义如何保护路径的配置方法
方法 | 能够做什么 |
---|---|
access(String) | 如果给定的SpEL表达式计算结果为true,就允许访问 |
anonymous() | 允许匿名用户访问 |
anthenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
fullyAuthenticated() | 如果用户是完整认证的,就允许访问 |
hasAnyAuthority(String…) | 如果用户具备给定权限中某一个的话,就允许访问 |
hasAnyRole(String…) | 如果用户具备给定角色中某一个的话,就允许访问 |
hasAuthority(String) | 如果用户具备给定权限的话,就允许访问 |
hasIpAddress(String) | 如果请求来自指定IP地址,就允许访问 |
hasRole(String) | 如果用户具备给定角色的话,就允许访问 |
not() | 对其他访问方法的结果求反 |
permitAll() | 无条件允许访问 |
rememberMe(String) | 如果用户是通过Remember-me功能认证的,就允许访问 |
使用Spring表达式进行安全保护
Spring Security支持的所有SpEI表达式
安全表达式 | 计算结果 |
---|---|
hasRole([role]) | 当前用户是否拥有指定角色。 |
hasAnyRole([role1,role2]) | 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。 |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
Principle | 代表当前用户的principle对象 |
authentication | 直接从SecurityContext获取的当前Authentication对象 |
permitAll | 总是返回true,表示允许所有的 |
denyAll | 总是返回false,表示拒绝所有的 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isRememberMe() | 表示当前用户是否是通过Remember-Me自动登录的 |
isAuthenticated() | 表示当前用户是否已经登录认证成功了。 |
isFullyAuthenticated() | 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。 |
强制通道的安全性
使用HTTP提交数据是—件具有风险的事情。如果使用HTTP发送无关紧要的信息,这可能不是什么大问题。但是如果你通过HTTP发送诸如密码和信用卡号这样的敏感信息的话,那你就是在找麻烦了。通过HTTP发送的数据没有经过加密,黑客就有机会拦截请求并且能够看到他
们想看的数据。这就是为什么敏感信息要通过HTTPS来加密发送的原因。
传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道。
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequest()
.antMatchers("/spitters/me").authenticated()
.antMatchers(HttpMethod.POST,"/spittles").authenticated()
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure();//需要HTTPS
}
防止跨站请求伪造
跨站请求伪造(cross-site request forgery,CSRF)的—个简单样例。简单来讲,如果—个站
点欺骗用户提交请求到其他服务器的话,就会发生跨站请求伪造(cross-site request forgery,CSRF)攻击,这可能会带来消极的后果。
Spring Security通过—个同步token的方式来实现CSRF防护的功能。它将会拦截状态变化的请求(例如,非GET、HEAD、OPTIONS和TRACE的请求)并检查CSRF
token。如果请求中不包含CSRF token的话,或者token不能与服务器端的token相匹配,请求将会失败,并抛出
CsrfException异常。这意味着在你的应用中,所有的表单必须在—个" csrf"域中提交token,而且这个token必须
要与服务器端计算并存储的token—致,这样的话当表单提交的时候,才能进行匹配。
Spring Security巳经简化了将token放到请求的属性中这—任务。如果使用JSP作为页面模板的话:
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
禁用SpringSecurity的CSRF防护功能:
@Override
protected void configure(HttpSecurity http) throws Exception{
http
```
.csrf()
.disabled();
}
认证用户
启用HTTP BASIC认证
HTTP Basic认证(HTTP Basic Authentication)会直接通过HTTP请求本身,对要访问应用程序的用户进行认证。你可能在以前见过HTTP
Basic认证。当在Web浏览器中使用时,它将向用户弹出—个简单的模态对话框。
但这只是Web浏览器的显示方式。本质上,这是—个HTTP 401响应,表明必须要在请求中包含
—个用户名和密码。在REST客户端向它使用的服务进行认证的场景中,这种方式比较适合。
如果要启用HTTP Basic认证的话,只需在configure()方法所传入的HttpSecurity对象上调用httpBasic()即可。另外,还可以通过调用realmName()方法指定域。如下是在 Spring Security中启用HTTP Basic认证的典型配置:
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.loginPage("/login")
.and()
.httpBasic()
.realmName("Spittr")
.and()
```
}
启用Remember-me功能
站在用户的角度来讲,如果应用程序不用每次都提示他们登录是更好的。这就是为什么许多站点提供了Remember-me功能,你只要登录过一次,应用就会记住你,当再次回到应用的时候你就不需要登录了。
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.loginPage("/login")
.and()
.rememberMe()
.tokenvaliditySeconds(2419200)
.key("spitterkey")
```
}
在这里,我们通过—点特殊的配笸就可以启用Remember-me功能。默认情况下,这个功能是通
过在cookie中存储—个token完成的,这个token最多两周内有效。但是,在这里,我们指定这个
token最多四周内有效(2,419,200秒)。
保护视图
略
小结
对于许多应用而言,安全性都是非常重要的切面。Spring Security提供了—种简单、灵活且强大
的机制来保护我们的应用程序。
借助于—系列Servlet Filter,Spring Security能够控制对Web资源的访问,包括Spring MVC控制器。借助于Spring
Security的Java配笸模型,我们不必直接处理Filter,能够非常简沽地声明Web安全性功能。
当认证用户时,Spring Security提供了多种选项。我们探讨了如何基于内存用户库、关系型数据
库和LDAP目录服务器来配笸认证功能。如果这些可选方案无法满足认证需求的话,我们还学
习了如何创建和配笸自定义的用户服务。