第9章 保护Web应用
Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。 Spring Security 提供了完整的安全性解决方案,它能够在 Web 请
求级别和方法调用级别处理身份认证和授权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入( dependency injection , DI )和
面向切面的技术。
在spring mvc中使用,至少需要以下几个模块
spring-security-core,spring-security-config,spring-security-web。
1、配置DelegatingFilterProxy
在web.xml中配置
使用java类形式的配置
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class WebAppSecurityInitializer extends AbstractSecurityWebApplicationInitializer { }
因为这里我们使用spring mvc程序使用的,所以只需要新建一个类,继承AbstractSecurityWebApplicationInitializer,之后什么都不用干。
2、权限的配置
我们还是同样在配置类中配置,这里不介绍xml配置方式,主要有几点
1)、如果要配置安全性必须要实现WebSecurityConfigurer,但是这里我们直接继承它的一个实现类更加简单WebSecurityConfigurerAdapter。
2)、我们还是和配置spring mvc的策略一样,使用带有Configuration注解的配置类来实现WebSecurityConfigurerAdapter,并且在配置类上还要加入EnableWebSecurity注解,这里我们暂定这个配置类的名字叫SecurityConfig。
3)、将SecurityConfig配置到getRootConfigClasses方法中,这个方法在哪就不需要介绍了吧。
例如:
import ch09.dao.UserDao; import ch09.security.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDao userDao; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserRepository(userDao)); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/login").and() .logout().logoutSuccessUrl("/").and() .csrf().disable() .httpBasic().and() .rememberMe().and() .authorizeRequests() .antMatchers("/").authenticated() .anyRequest().permitAll(); } }
对于这个类的配置大致分为3个部分的配置:
1)、filter链配置
2)、保护请求
3)、如何验证
3、验证部分的配置
主要是配置什么呢?配置登录用户到底从哪里去找,到底怎么找。这里简单提一下有几种方式可以直接从内存中找,也可以到数据库中找,也可以采用LDAP服务器,当然也可以自定以,这里我们主要介绍一下自定义的方式,如果采用前面几种方式对你的数据库或者LDAP服务器都是要有一些特定要求的,比如表名之类的。
我们需要提供一个自定义
的 UserDetailsService 接口实现
import ch09.dao.UserDao; import ch09.domain.User; import ch09.security.model.SimpleUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class UserRepository implements UserDetailsService { private UserDao userDao; public UserRepository(UserDao userDao){ this.userDao = userDao; } public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userDao.getUserByUserName(s); return new SimpleUserDetails(user); } }
有意思的是,不管你数据来自于何方,你只要提供UserDetails的实现就可以了,来接下来让我们看看UserDetails有什么,如果你只需要登录的话,也许你只要关注密码和用户名就行了。
import ch09.domain.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class SimpleUserDetails implements UserDetails { private User user; public SimpleUserDetails(User user){ this.user = user; } //权限列表 public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> grantedAuthorityList = new ArrayList<GrantedAuthority>(); grantedAuthorityList.add(new SimpleGrantedAuthority("ADMIN")); return grantedAuthorityList; } //获取密码 public String getPassword() { return user.getUserPassword(); } //获取用户名 public String getUsername() { return user.getUserName(); } //账户未过期 public boolean isAccountNonExpired() { return true; } //账户未锁定 public boolean isAccountNonLocked() { return true; } //证书未过期 public boolean isCredentialsNonExpired() { return true; } //账户启用 public boolean isEnabled() { return true; } //自定义方法 public User getUser() { return user; } }
来看我们如何使用UserDetailsService的实现类吧:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new UserRepository(userDao)); }
不过有件事情不得不提一下,一般数据库的用户密码是不会存储名文的,为了安全起见都会采用密文,加密方式可能不一样,但是为了安全还是加密一下再存比较好,这样的话我们就需要配置个密码转码器,密码转码器会把前台输入的密码加密后和数据库的密文进行比对,如何配置呢,如图
passwordEncoder() 方法可以接受 Spring Security 中 PasswordEncoder 接口的任意实现。 Spring Security 的加密模块包括了三个这样的实
现: BCryptPasswordEncoder 、 NoOpPasswordEncoder 和 StandardPasswordEncoder 。
4、如何保护请求
先看一下下面简单的配置,在讲怎么配置
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/login").and() .logout().logoutSuccessUrl("/").and() .csrf().disable() .httpBasic().and() .rememberMe().and() .authorizeRequests() .antMatchers("/").authenticated() .anyRequest().permitAll(); }
配置的话分为几个方面:配置路径、强制安全通道https、跨站请求伪造、登录页(记住密码、退出)、 HTTP Basic 认证。
配置路径
需要注意的是,我们可以使用 Spring 表达式进行安全保护
强制安全通道https
跨站请求伪造
简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就
会发生 CSRF 攻击,这可能会带来消极的后果。
从 Spring Security 3.2 开始,默认就会启用 CSRF 防护。
Spring Security 通过一个同步 token 的方式来实现 CSRF 防护的功能。它将会拦截状态变化的请求(例如,
非 GET 、 HEAD 、 OPTIONS 和 TRACE 的请求)并检查 CSRF token 。如果请求中不包含 CSRF token 的话,或者 token 不能与服务器端的 token 相
匹配,请求将会失败,并抛出 CsrfException 异常。
这意味着在你的应用中,所有的表单必须在一个 “_csrf” 域中提交 token ,而且这个 token 必须要与服务器端计算并存储的 token 一致,这样的话
当表单提交的时候,才能进行匹配。
Spring Security 已经简化了将 token 放到请求的属性中这一任务。如果你使用 Thymeleaf 作为页面模板的话,只要 <form> 标签
的 action 属性添加了 Thymeleaf 命名空间前缀,那么就会自动生成一个 “_csrf” 隐藏域:
如果是jsp的话,使用 Spring 的表单绑定标签的话, <sf:form> 标签会自动为我们添加隐藏的 CSRF token 标签。
处理 CSRF 的另外一种方式就是根本不去处理它。我们可以在配置中通过调用 csrf().disable() 禁用 Spring Security 的 CSRF 防护功能。
需要特别说明的一点是,如果开启,使用默认的退出功能时需要post提交且待csrf域才能注销。
spring security集成csrf
进行post等请求时,为了防止csrf攻击,需要获取token才能访问
因此需要添加
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
登录页(记住密码、退出)
如果你使用程序清单 9.1 中最简单的 Spring Security 配置的话,那么就能无偿地得到一个登录页。实际上,在重
写 configure(HttpSecurity) 之前,我们都能使用一个简单却功能完备的登录页。但是,一旦重写了 configure(HttpSecurity) 方
法,就失去了这个简单的登录页面。
不过,把这个功能找回来也很容易。我们所需要做的就是在 configure(HttpSecurity) 方法中,调用 formLogin()。
如何自定义登录页你只需要
http.formLogin().loginPage("/login")
但是需要注意的是,在你定义的页面中用户名的域需要命名为username,密码的域需要命名为password,提交方式post,提交地址/login。用thymeleaf模板的写法是"@{/login}"。
也许有时我们需要记住密码的功能,在配置文件里我们需要如下配置
默认情况下,这个功能是通过在 cookie 中存储一个 token 完成的,这个 token
最多两周内有效。但是,在这里,我们指定这个 token 最多四周内有效( 2,419,200 秒)。
存储在 cookie 中的 token 包含用户名、密码、过期时间和一个私钥 —— 在写入 cookie 前都进行了 MD5 哈希。默认情况下,私钥的名
为 SpringSecured ,但在这里我们将其设置为 spitterKey ,使它专门用于 Spittr 应用。
为了实现这一点,登录请
求必须包含一个名为 remember-me 的参数。在登录表单中,增加一个简单复选框就可以完成这件事情:
当然登录,我们还需要注销
退出功能是通过 Servlet 容器中的 Filter 实现的(默认情况下),这个 Filter 会拦截针对 “/logout” 的请求。因此,为应用添加退出功能只需添加如下
的链接即可(如下以 Thymeleaf 代码片段的形式进行了展现):
HTTP Basic 认证
对于应用程序的人类用户来说,基于表单的认证是比较理想的。但是在第 16 章中,将会看到如何将我们 Web 应用的页面转化为 RESTful API 。
当应用程序的使用者是另外一个应用程序的话,使用表单来提示登录的方式就不太适合了。
HTTP Basic 认证( HTTP Basic Authentication )会直接通过 HTTP 请求本身,对要访问应用程序的用户进行认证。你可能在以前见过 HTTP
Basic 认证。当在 Web 浏览器中使用时,它将向用户弹出一个简单的模态对话框。
如果要启用 HTTP Basic 认证的话,只需在 configure() 方法所传入的 HttpSecurity 对象上调用 httpBasic() 即可。另外,还可以通过
调用 realmName() 方法指定域。如下是在 Spring Security 中启用 HTTP Basic 认证的典型配置:
5、如何获取登录用户的信息
public SimpleUserDetails getSimpleUserDetails(){ Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if(principal instanceof UserDetails){ return (SimpleUserDetails)principal; } return null; }
6、如何保护视图
前面都是如何保护代码,现在看看如何在视图中使用权限吧,这里我们只介绍thymeleaf,先看看官网的模块图
当然要想再页面中使用sping security需要导入相应的包。