一、Spring Security简介
1、一种基于 Spring AOP 和 Servlet 规范中的 Filter 实现的安全框架,能够在Web请求级别和方法调用级别处理身份认证和授权。
Spring Security充分利用了依赖注入和面向切面技术。
2、Spring Security(3.2) 从两个角度来解决安全性问题:
- 使用 Servlet 规范中的Filter保护Web请求并限制URL级别的访问;
- 使用 Spring AOP 保护方法调用 —— 借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法;
二、理解Spring Security的模块
Security 3.2分为11个模块,如下表:
模块 | 描述 |
---|---|
ACL | 支持通过访问控制列表(access control list, ACL)为域对象提供安全性 |
切面(Aspects) | 一个很小的模块,当使用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP |
CAS客户端(CAS Client) | 提供与Jasig的中心认证服务(Central Authentication Service, CAS)进行集成的功能 |
配置(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应用中,还需要添加Web模块
三、代码编写
1、配置过滤器
Spring Security借助一系列Servlet Filter来提供各种安全性功能。此处只需配置DelegatingFilterProxy;
DelegatingFilterProxy是一个特殊的Servlet Filter,它本身所做的工作并不多。只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring应用的上下文中。
(1)xml方式配置
<!-- 在web.xml中进行如下配置-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
(2)java方式
/*
AbstractSecurityWebApplicationInitializer实现了WebApplicationInitializer,
因此Spring会发现它,并用它在Web容器中注册DelegatingFilterProxy。
尽管我们可以重载它的appendFilters()或insertFilters()方法来注册自己选择的Filter,
但是要注册DelegatingFilterProxy的话,我们并不需要重载任何方法。
*/
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
不管我们通过web.xml还是通过AbstractSecurityWebApplicationInitializer的子类来配置DelegatingFilterProxy,它都会拦截发往应用中的请求,并将请求委托给ID为springSecurityFilterChain bean。
2、启用安全性配置
/*
Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中,或者扩展WebSecurityConfigurerAdapter。
在Spring应用上下文中,任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security;
*/
@Configuration
@EnableWebSecurity // 启用任意Web的安全功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
如果应用是使用SpringMVC开发的,使用@EnableWebMvcSecurity代替;
- 能配置一个Spring MVC参数解析器。那样处理器方法就能够通过带有@AuthenticationPrincipal注解的参数获取得认证用户的principal。
- 同时还配置一个bean,在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造(CSRF)token的输入域。
以上@EnableWebSecurity和@EnableWebMvcSecurity的配置都会将应用严格锁定,导致没有人能够进入系统。(没有重载configure方法定义安全细节)
为了指定Web安全的细节,可以通过重载WebSecurityConfigurerAdapter中的方法来实现。
方法 | 描述 |
---|---|
configure(WebSecurity) | 通过重载,配置Spring Security的Filter链 |
configure(HttpSecurity) | 通过重载,配置如何通过拦截器保护请求 |
configure(AuthenticationManagerBuilder) | 通过重载,配置user-detail服务 |
默认的configure(HttpSecurity)实际上等同于如下所示:
/*
此默认配置指定了该如何保护HTTP请求,以及客户端认证用户的方案;
authorizeRequests()和anyRequest().authenticated()就会要求所有进入应用的HTTP请求都要进行认证。
*/
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin() // 设置基于表单的登录
.and()
.httpBasic(); // HTTP Basic方式认证
}
/*
因为没有重载configure(AuthenticationManagerBuilder)方法,所以没有用户存储支撑认证过程。
没有用户存储,实际上就等于没有用户。所以在这里所有的请求都需要认证,但是没有人能够登录成功。
*/
还需要添加配置:
- 配置用户储存;
- 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限;
- 提供一个自定义的登录页面,替代原来简单的默认登录页。
3、配置用户存储
(1)基于内存的用户存储
配置用户存储的最简单方式就是重载configure()方法,并以AuthenticationManagerBuilder作为传入参数。AuthenticationManagerBuilder有多个方法可以用来配置Spring Security对认证的支持。通过inMemoryAuthentication()方法,我们可以启动、配置并任意填充基于内存的用户存储。
@Configuration
@EnableWebSecurity
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");
// roles("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...) | 授予某个用户一项或多项角色 |
(2)基于数据库表进行认证
当用户数据存储在DB中,可以使用jdbcAuthentication()方法进行配置;
/* 这种方式预期数据库中存在某些存储用户数据的表,如下是Spring Security内部当查询用户信息时的sql
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
"select username,password,enabled " +
"from users " +
"where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
"select username,authority " +
"from authorities " +
"where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
"select g.id, g.group_name, ga.authority " +
"from group g, group_members gm, ga.authorities ga " +
"where gm.username = ?"
"and g.id = ga.group_id " +
"and g.id = gm.group_id";
*/
@Autowired
private 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=?")
.authoritiesByUsernameQuery(
"select username, 'ROLE_USER' from Spitter where username=?");
}
关于密码转码器见 P261
(3)其他方式见 P262
4、拦截请求
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。如下的代码片段展现了重载的configure(HttpSecurity)方法,它为不同的URL路径有选择地应用安全性:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitter/me").authenticated() // 指定请求路径需要认证
.antMatchers(HttpMethod.POST, "/spittles").authenticated() //对"/spittles"路径的HTTP POST请求必须经过认证
.anyRequest().permitAll();// anyRequest()说明其他请求都是允许的,不需要认证和任何的权限
}
antMatchers() 方法中设定的路径:
- 支持Ant风格的通配符:antMatchers("/spitters/**")
- 支持指定多个路径:antMatchers("/spitters/**", "spittles/mine")
regexMatchers()方法支持正则定义请求路径:regexMatchers("/spitters/.*")
authenticated():要求在执行该请求时,必须已经登录了应用。如果用户没有认证的话,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登录页面。
permitAll():允许请求没有任何的安全限制。
(1)使用Spring表达式进行安全保护
借助access()方法,将SpEL作为声明访问限制的一种方式:
具有“ROLE_SPITTER”角色才能访问“/spitter/me”URL:.antMatchers("/spitter/me").access("hasRole('ROLE_SPITTER')")
(2)强制通道的安全性
通过configure() 方法中的HttpSecurity对象,借助对象的requiresChannel() 方法,能够为各种为各种URL模式声明所要求的通道。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure(); // 需要 HTTPS
}
requiresInsecure():声明通过HTTP通道
(3)防止跨站请求伪造
跨站请求伪造:一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来消极的后果
5、认证用户
(1)启用HTTP Basic认证
HTTP Basic认证(HTTP Basic Authentication)会直接通过 HTTP请求本身,对要访问应用程序的用户进行认证。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")// 如果不设置,则默认为/login
.and()
.httpBasic() // 启用HTTP Basic认证
.realmName("Spittr") // 执行域
.and()
...
}
(2)启用Remember-me功能
只要登录过一次,应用就会记住你,当再次回到应用的时候你就不需要登录了。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")// 如果不设置,则默认为/login
.and()
.rememberMe() // 启用remember me认证(通过在cookie中存储一个token完成)
.tokenValiditySeconds(2419200) // 指定token有效期
.key("spittrKey"); // token在写入cookie前会MD5哈希,token中的私钥默认名称为”SpringSecured“,此处自定义为spittrKey
}