Spring实战 九 保护Web应用
Spring中提供权限认证的模块就是spring-security
,先导入。
现在dependencyManagement
中写入
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${spring.security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
再在dependencies中定义
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
spring-security
提供了基于Filter链实现的基于请求的安全保护,也通过AOP实现了方法级别的保护。
它定义了一套Filter链对登录的各个过程进行校验,但这些都不用我们管,我们要做的就是在java中提供一些基础的配置即可。
简单介绍Spring Security的原理
Spring Security通过标准的Servlet规范中的Filter来实现安全保护。
就像DispatcherServlet一样,Spring也有一个DelegatingFilterProxy
作为接收所有请求的前端Filter,DelegatingFilterProxy
维护一个FilterChain并在接到请求时调用它们进行各种各样的安全校验。
DelegatingFilterProxy
有一个好处就是它连接了Servlet上下文和Spring应用上下文,让Spring上下文中通过Bean注册的Filter能够被应用扫描到。Servlet并不具备扫描Bean的功能。
这是Spring官方的一张图,Spring的DelegatingFilterProxy
与其它的Servlet和Filter同在Servlet的FilterChain中,而DelegatingFilterProxy
自己又维护了一个FilterChain,DelegatingFilterProxy
会委托这些链中的Filter进行安全校验。
使用Java配置SpringSecurity
创建两个类
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
AbstractSecurityWebApplicationInitializer
用于初始化DelegatingFilterProxy
和默认的FilterChain,它实现自WebApplicationInitializer
所以它的初始化方法能够在Servlet容器时被调用。
SecurityConfig
用于我们来配置一些安全策略,现在我们什么也没配置,默认的策略要求应用中的所有请求都必须通过用户验证,但是我们目前又没有用户,这不是尴尬了??
配置认证管理
我们通过SecurityConfig
中的config
方法来配置一些用户,有三个config
方法,分别用于不同的配置,我们现在选择具有AuthenticationManagerBuilder
参数类型的,这个方法用于进行用户认证管理相关的配置,我们可以在这个方法中指定从哪里存储,如何存储用户信息。
以下是三个config
方法的作用
inMemoryAuthentication
对于测试环境,Spring提供了一套很方便的inMemoryAuthentication认证管理方式,通过inMemoryAuthentication
,我们可以在内存中创建一些用于开发环境的用户。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("user")
.password(new BCryptPasswordEncoder().encode("user"))
.roles("USER").and()
.withUser("admin")
.password(new BCryptPasswordEncoder().encode("admin"))
.roles("USER","ADMIN");
}
如上,AuthenticationManagerBuilder
的建造者模式让我们很方便的指定这些用户信息,我们先用了inMemoryAuthentication
来指定了使用基于内存的认证管理方式,然后我们传入了一个passwordEncoder
指定如何对密码信息进行加密,再然后就是使用withUser
在内存中定义一个用户,并使用password
定义密码,注意这里传入的密码需要是加密过的,并且加密方式要和前面指定的一样,不然登陆的时候的加密方式和你这里存储的加密方式不一样,得到的密文不一样,就登陆不进去。最后使用role
来指定用户的角色。and
可以将两个配置连接起来。
roles
是authorities
方法的简写形式,因为Spring中的角色以ROLE_
开头,如果按原始写法,应该是这样:
但是我们使用role
方法,会自动在前面添加ROLE_
前缀。
UserDetialsService
Spring也提供了基于数据库,基于LDAP的认证管理方式,但是我们这里先不研究,研究一个更加通用的——UserDetialsService
。
创建一个类,实现UserDetialsService
,实现loadUserByUsername
方法,返回一个用户信息,这是Spring与UserDetialsService唯一的约定,至于你UserDetialsService中基于内存管理还是数据库管理还是其他方法,那就和Spring没关系了。
我们这里使用之前的Spittr
应用,将Spittr
的用户系统通过UserDetialsService
集成到Spring Security中
我们的SpitterUserService
将通过用户名加载用户的实现委托给了之前的SpitterRepository
,Service实际没处理啥,就是获取结果,然后添加权限,构造用户对象,返回,或在没找到对象时抛出异常。
public class SpitterUserService implements UserDetailsService {
private final SpitterRepository repository;
public SpitterUserService(SpitterRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Spitter spitter = repository.getOneByUserName(s);
if (spitter!=null) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_SPITTER"));
return new User(spitter.getUserName(),
new BCryptPasswordEncoder().encode(spitter.getPassword()),
authorities);
}
throw new UsernameNotFoundException("User " + s + " not found!");
}
}
需要注意的是,这里我们也使用了BCryptPasswordEncoder
对密码进行加密,如果使用明文密码,可以在密码前面连接{noop}
字符串,这大括号中是加密方式,指定为noop
后Spring就不会对其加密了。
还有持久层中原来没有getOneByUserName
方法,我这里补上。
@Override
public Spitter getOneByUserName(String username) {
return spitterStorage.values().stream()
.filter(spitter -> spitter.getUserName().equals(username))
.findFirst().get();
}
然后,我们需要在SecurityConfig
中配置这个SpitterService
@Autowired
private SpitterRepository repository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new SpitterUserService(repository))
.passwordEncoder(new BCryptPasswordEncoder());
}
自动注入SpitterRepository
,使用userDetailsService
指定通过UserDetialsService进行认证,并将我们的实现类SpitterUserService
传入,而且要指定一致的passwordEncoder
。
现在就将我们Spittr应用中自己的用户系统接入到Spring中了。
拦截请求
应用中有一部分页面是不需要认证的,比如自定义的登录页面。对于Spittr应用,首页不需要认证,Spittle列表页不需要认证。
其实需要认证的只有当用户查看“我”页面时需要他已经登陆,已经拥有一个Spitter身份,再有就是当用户发表Spittle时,需要有一个Spitter身份。
通过实现参数为HttpSecurity的方法来对不同的url应用不同的安全策略。下面对调用authorizeRequests
返回的对象调用antMatchers
来使用ant语法匹配url,还有regexMatchers
使用正则表达式来匹配,authenticated
方法要求指定路径下的用户必须已经通过验证。使用这两个方法的组合,我们对/spitters/me
页面和POST请求的/spittles
页面进行了限制,要求访问的用户必须已经通过验证。对于其它的url,使用anyRequest().permitAll()
来对所有请求允许访问。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitters/me").authenticated()
.antMatchers(HttpMethod.POST,"/spittles").authenticated()
.anyRequest().permitAll();
}
上面的方法中,antMatchers
和anyRequest
用来匹配url,authenticated
和permitAll
用来指定如何保护匹配的请求。Spring提供了如下的保护机制
下面我们改写代码,把authenticated
改为需要拥有ROLE_SPITTER
角色才能够正常访问。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitters/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
.anyRequest().permitAll();
}
使用了hasRole
来指定访问的用户必须拥有SPITTER
角色,当然也可以通过hasAuthority("ROLE_SPITTER")
来访问。
使用SpEL
access
方法可以执行给定的SpEL表达式来进行权限判断,并且提供了差不多的扩展表达式语法
比如我们想对用户角色和IP地址同时进行限制,可以
antMatchers("/spitters/me").access("hasRole('SPITTER') and hasIpAddress('192.168.1.2')");
强制通道安全性
现在默认都是https了,https在传输时对请求进行加密,被恶意窃取的机会更小了。
HttpSecurity
除了authorizeRequests
方法还有一个requiresChannel
方法来指定请求的通道。
下面使用了requiresChannel
来对指定的url进行通道安全性配置,指定了/spitter/form
路径必须经过安全的通道,也就是https。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitters/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure();
}
也可以使用requiresInsecure
来指定只使用http。
csrf
CSRF攻击是一种常见的网络攻击形式,原理就是黑客编写恶意的网页,伪造表单来向其他服务器发送请求,并诱导用户点击提交按钮。比如:
假如这是黑客编写的恶意表单,message
字段是隐藏的,如果你点击按钮,并且你使用过Spittr应用,浏览器中还存在Cookie的话,你就会在Spittr应用中发送一条I'm stupid!
的消息。
想想如果这个表单是转账或者其它的重要表单,造成的后果不可想象。
Spring Security使用同步token实现防csrf攻击,服务器端通过Cookie给客户端发放一个csrf token并设置httponly(这样客户端不能通过js来获取cookie内容),客户端下次请求时不在Cookie中携带这个token,而是通过将这个token添加到请求参数中(因为Cookie会自动携带,而请求参数必须主动携带,这样黑客就没办法进行csrf攻击)
这意味着我们的前端页面的表单或ajax中就必须有一个携带csrf token的域,这个域就是_csrf
。
使用Thymeleaf
模板和jsp的sf:form
标签会自动生成这个隐藏域,如果使用传统jsp,则可以按如下方法
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}">
csrf防护功能是默认开启的,可以这样关闭
认证用户
我们重写HttpSecurity
属性的configure
方法后,Spring默认的认证页面没了。
父类的方法中调用了formLogin
来提供这个认证表单,我们重写了当然没有了,不过可以加回来。
http.formLogin()
.and()
.authorizeRequests()
.antMatchers("/spitters/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure();
自定义登录页面
可以使用loginPage
方法,指定一个Spring控制器路径,在其中返回相应的逻辑视图名。
需要注意的是,如果不用Thymeleaf和sf:form的话,请记得添加csrf隐藏域。
HttpBasic认证
HttpBasic认证是通过返回http 401状态码通知请求端需要认证,在浏览器中的默认行为是浏览器会弹出一个对话框要求用户认证。
可以通过httpBasic
方法启用。
Remember-Me
记住密码Spring也提供了简单的办法。
Spring通过Cookie来存储token来提供RememberMe功能,默认Token存活时间为两周,可以通过tokenValiditySeconds
来指定存活的秒数。
token通过用户名,密码,和过期时间以及一个私钥进行散列生成,默认私钥的名字是SpringSecured,可以通过key
方法指定,如上图。
嘶可以看到服务器端为我们设置的remember-me字段的cookie
rememberme需要用户在请求表单中指明它需要记住密码,可以在表单中添加如下字段供用户选择。
退出
退出只需要用户请求/logout
即可,spring的filter会自动拦截这个请求,做相应的处理。默认情况下,logout后spring会重定向到login?logout
url,可以通过如下方式进行配置。
而且我们可以指定用户退出时过滤器拦截的请求,比如将/logout
替换成/signout
。
保护视图
Spring支持在视图上显示一些认证相关的信息。
可以通过jsp标签库和Thymeleaf的Spring方言。
jsp标签库
安全相关的jsp标签库只有三个标签
使用如下方式声明
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
访问认证信息的细节
我们可以使用authentication
标签来显示一些关于认证信息的细节。
比如使用如下方式我们可以渲染出认证的用户名
Hello <security:authentication property="principal.username"/>!
property表示要渲染的属性,principal表示用户认证对象,它其中有关于认证对象的一些基本信息。其他可用的属性如下
条件性渲染
使用authorize
可以根据用户被授予的权限条件化渲染页面。
比如下面,当在具有ROLE_SPITTER
权限时才渲染发布Spittle的表单
access属性是一个SpEL表达式
Thymeleaf的Spring方言
先引入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
配置Thymeleaf引擎支持SpringSecurity方言
@Bean
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver resolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(resolver);
templateEngine.addDialect(new SpringSecurityDialect());
return templateEngine;
}
然后我们可以这样使用它
<div sec:authorize="isAuthenticated()">
Hello <span sec:authentication="name">someone</span>
</div>