安全性是超越应用程序功能的一个关注点。应用系统的绝大部分内容都不应该参与到与自己相关的安全性处理中。尽管我们可以直接在应用程序中编写安全性功能相关的代码(这种情况并不少见),但更好的方式还是将安全性相关的关注点与应用程序本身的关注点进行分离。
9.1 Spring Security简介
Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案,它能够在Web请求级别和方法调用级别处理身份认证和授权。因为基于Spring框架,所以Spring Security充分利用了依赖注入(dependency injection,DI)和面向切面的技术。
Spring Security从两个角度来解决安全性问题。它使用Servlet规范中的Filter保护Web请求并限制URL级别的访问。Spring Security还能够使用Spring AOP保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。
9.1.1 理解Spring Security的模块
表9.1 Spring Security的模块
模块 | 描述 |
---|---|
ACL | 支持通过访问控制列表(access control list,ACL)为域对象提供安全性 |
切面(Aspects) | 一个很小的模块,当使用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP |
CAS客户端(CASClient) | 提供与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这两个模块。
9.1.2 过滤Web请求
Spring Security借助一系列Servlet Filter来提供各种安全性功能。DelegatingFilterProxy是一个特殊的Servlet Filter,它本身所做的工作并不多。只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>
注册在Spring应用的上下文中。
在XML中可以使用<filter>
元素配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
在这里,最重要的是<filter-name>
设置成了springSecurityFilterChain
。这是因为我们马上就会将Spring Security配置在Web安全性之中,这里会有一个名为springSecurityFilterChain
的Filter bean,DelegatingFilterProxy会将过滤逻辑委托给它。
如果你希望借助WebApplicationInitializer以Java的方式来配置DelegatingFilterProxy的话,那么我们所需要做的就是创建一个扩展的新类:
package spittr.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
AbstractSecurityWebApplicationInitializer实现了WebApplicationInitializer,因此Spring会发现它,并用它在Web容器中注册DelegatingFilterProxy。尽管我们可以重载它的appendFilters()
或insertFilters()
方法来注册自己选择的Filter,但是要注册DelegatingFilterProxy的话,我们并不需要重载任何方法。
不管我们通过web.xml还是通过AbstractSecurityWebApplicationInitializer的子类来配置DelegatingFilterProxy,它都会拦截发往应用中的请求,并将请求委托给ID为springSecurityFilterChain bean。
springSecurityFilterChain
本身是另一个特殊的Filter,它也被称为FilterChainProxy。它可以链接任意一个或多个其他的Filter。Spring Security依赖一系列Servlet Filter来提供不同的安全特性。但是,你几乎不需要知道这些细节,因为你不需要显式声明springSecurityFilterChain
以及它所链接在一起的其他Filter。当我们启用Web安全性的时候,会自动创建这些Filter。
9.1.3 编写简单的安全性配置
Java配置:
package spittr.config;
import org.springframework.context.annotation.Configuration;
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 {
}
@EnableWebSecurity
注解将会启用Web安全功能。但它本身并没有什么用处,Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中,或者(简单起见)扩展WebSecurityConfigurerAdapter。在Spring应用上下文中,任何实现了WebSecurityConfigurer的bean都可以用来配置Spring Security,
@EnableWebSecurity
可以启用任意Web应用的安全性功能,不过,如果你的应用碰巧是使用Spring MVC开发的,那么就应该考虑使用@EnableWebMvcSecurity
替代它,如程序清单9.2所示。
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
除了其他的内容以外,@EnableWebMvcSecurity
注解还配置了一个Spring MVC参数解析解析器(argument resolver),这样的话处理器方法就能够通过带有@AuthenticationPrincipal
注解的参数获得认证用户的principal
(或username)。它同时还配置了一个bean,在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forgery,CSRF)token输入域。
尽管不是严格要求的,但我们可能希望指定Web安全的细节,这要通过重载WebSecurityConfigurerAdapter中的一个或多个方法来实现。我们可以通过重载WebSecurityConfigurerAdapter的三个configure()
方法来配置Web安全性,这个过程中会使用传递进来的参数设置行为。
表9.2 configuer()
方法
方法 | 描述 |
---|---|
configure(WebSecurity) | 通过重载,配置Spring Security的Filter链 |
configure(HttpSecurity) | 通过重载,配置如何通过拦截器保护请求 |
configure(AuthenticationManagerBuilder) | 通过重载,配置user-detail服务 |
让我们重新看一下上面的程序,可以看到它没有重写上述三个configure()
方法中的任何一个,这就说明了为什么应用现在是被锁定的。尽管对于我们的需求来讲默认的Filter链是不错的,但是默认的configure(HttpSecurity)
实际上等同于如下所示:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
这个简单的默认配置指定了该如何保护HTTP请求,以及客户端认证用户的方案。通过调用authorizeRequests()
和anyRequest().authenticated()
就会要求所有进入应用的HTTP请求都要进行认证。它也配置Spring Security支持基于表单的登录以及HTTP Basic方式的认证。
同时,因为我们没有重载configure(AuthenticationManagerBuilder)
方法,所以没有用户存储支撑认证过程。没有用户存储,实际上就等于没有用户。所以,在这里所有的请求都需要认证,但是没有人能够登录成功。
为了让Spring Security满足我们应用的需求,还需要再添加一点配置。具体来讲,我们需要:
- 配置用户存储
- 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限
- 提供一个自定义的登录页面,替代原来简单的默认登录页
9.2 选择查询用户详细信息的服务
我们没有办法进入应用,即便用户认为他们应该能够登录进去,但实际上却没有允许他们访问应用的数据记录。因为缺少用户存储,现在的应用程序太封闭了,变得不可用。
我们所需要的是用户存储,也就是用户名、密码以及其他信息存储的地方,在进行认证决策的时候,会对其进行检索。
好消息是,Spring Security非常灵活,能够基于各种数据存储来认证用户。它内置了多种常见的用户存储场景,如内存、关系型数据库以及LDAP。但我们也可以编写并插入自定义的用户存储实现。
9.2.1 使用基于内存的用户存储
因为我们的安全配置类扩展了WebSecurityConfigurerAdapter,因此配置用户存储的最简单方式就是重载configure()
方法,并以AuthenticationManagerBuilder作为传入参数。AuthenticationManagerBuilder有多个方法可以用来配置Spring Security对认证的支持。通过inMemoryAuthentication()
方法,我们可以启用、配置并任意填充基于内存的用户存储。
例如,在如程序中,SecurityConfig重载了configure()
方法,并使用两个用户来配置内存用户存储。
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@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");
}
}
我们可以看到,configure()
方法中的AuthenticationManagerBuilder使用构造者风格的接口来构建认证配置。通过简单地调用inMemoryAuthentication()
就能启用内存用户存储。但是我们还需要有一些用户,否则的话,这和没有用户并没有什么区别。
因此,我们需要调用withUser()
方法为内存用户存储添加新的用户,这个方法的参数是username
。withUser()
方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,这个对象提供了多个进一步配置用户的方法,包括设置用户密码的password()
方法以及为给定用户授予一个或多个角色权限的roles()
方法。
在程序中,我们添加了两个用户,user
和admin
,密码均为password
。user
用户具有USER
角色,而admin
用户具有ADMIN
和USER
两个角色。我们可以看到,and()
方法能够将多个用户的配置连接起来。
除了password()
、roles()
和and()
方法以外,还有其他的几个方法可以用来配置内存用户存储中的用户信息。
需要注意的是,roles()
方法是authorities()
方法的简写形式。roles()
方法所给定的值都会添加一个ROLE_
前缀,并将其作为权限授予给用户。实际上,如下的用户配置与下面程序是等价的:
auth
.inMemoryAuthentication()
.withUser("user").password("password").authorities("ROLE_USER")
.and()
.withUser("admin").password("password").authorities("ROLE_USER", "ROLE_ADMIN");
表9.3 配置用户详细信息的方法
方法 | 描述 |
---|---|
accountExpired(boolean) | 定义账号是否已经过期 |
accountLocked(boolean) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority…) | 授予某个用户一项或多项权限 |
authorities(List) | 授予某个用户一项或多项权限 |
authorities(String…) | 授予某个用户一项或多项权限 |
credentialsExpired(boolean) | 定义凭证是否已经过期 |
disabled(boolean) | 定义账号是否已被禁用 |
password(String) | 定义用户的密码 |
roles(String…) | 授予某个用户一项或多项角色 |
9.2.2 基于数据库表进行认证
用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication()
方法,所需的最少配置如下所示:
package spittr.config;
import javax.sql.DataSource;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
}
我们必须要配置的只是一个DataSource,这样的话,就能访问关系型数据库了。在这里,DataSource是通过自动装配的技巧得到的。
重写默认的用户查询功能
尽管默认的最少配置能够让一切运转起来,但是它对我们的数据库模式有一些要求。它预期存在某些存储用户数据的表。更具体来说,下面的代码片段来源于Spring Security内部,这块代码展现了当查找用户信息时所执行的SQL查询语句:
public static final String DEF_USERS_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 groups g, group_members gm, group_authorities ga " + "where gm.username = ? "
+ "and g.id = ga.group_id " + "and g.id = gm.group_id";
在第一个查询中,我们获取了用户的用户名、密码以及是否启用的信息,这些信息会用来进行用户认证。接下来的查询查找了用户所授予的权限,用来进行鉴权,最后一个查询中,查找了用户作为群组的成员所授予的权限。
如果你能够在数据库中定义和填充满足这些查询的表,那么基本上就不需要你再做什么额外的事情了。但是,也有可能你的数据库与上面所述并不一致,那么你就会希望在查询上有更多的控制权。如果是这样的话,我们可以按照如下的方式配置自己的查询:
package spittr.config;
import javax.sql.DataSource;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
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=?");
}
}
在本例中,我们只重写了认证和基本权限的查询语句,但是通过调用groupAuthoritiesByUsername()
方法,我们也能够将群组权限重写为自定义的查询语句。
将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限查询会选取零行或多行数据,每行数据中都会包含群组ID、群组名称以及权限。
使用转码后的密码
看一下上面的认证查询,它会预期用户密码存储在了数据库之中。这里唯一的问题在于如果密码明文存储的话,会很容易受到黑客的窃取。但是,如果数据库中的密码进行了转码的话,那么认证就会失败,因为它与用户提交的明文密码并不匹配。
为了解决这个问题,我们需要借助passwordEncoder()
方法指定一个密码转码器(encoder):
@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=?")
.passwordEncoder(new StandardPasswordEncoder("53cr3t"));
}
passwordEncoder()
方法可以接受Spring Security中PasswordEncoder接口的任意实现。Spring Security的加密模块包括了三个这样的实现:BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。
上述的代码中使用了StandardPasswordEncoder,但是如果内置的实现无法满足需求时,你可以提供自定义的实现。PasswordEncoder接口非常简单:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
不管你使用哪一个密码转码器,都需要理解的一点是,数据库中的密码是永远不会解码的。所采取的策略与之相反,用户在登录时输入的密码会按照相同的算法进行转码,然后再与数据库中已经转码过的密码进行对比。这个对比是在PasswordEncoder的matches()
方法中进行的。
9.2.3 基于LDAP进行认证
为了让Spring Security使用基于LDAP的认证,我们可以使用ldapAuthentication()
方法。这个方法在功能上类似于jdbcAuthentication()
,只不过是LDAP版本。如下的configure()
方法展现了LDAP认证的简单配置:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userSearchFilter("(uid={0})").groupSearchFilter("member={0}");
}
方法userSearchFilter()
和groupSearchFilter()
用来为基础LDAP查询提供过滤条件,它们分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP层级结构的根开始。但是我们可以通过指定查询基础来改变这个默认行为:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userSearchBase("ou=people").userSearchFilter("(uid={0})").groupSearchBase("ou=groups")
.groupSearchFilter("member={0}");
}
userSearchBase()
属性为查找用户提供了基础查询。同样,groupSearchBase()
为查找组指定了基础查询。我们声明用户应该在名为people
的组织单元下搜索而不是从根开始。而组应该在名为groups
的组织单元下搜索。
配置密码比对
基于LDAP进行认证的默认策略是进行绑定操作,直接通过LDAP服务器认证用户。另一种可选的方式是进行比对操作。这涉及将输入的密码发送到LDAP目录上,并要求服务器将这个密码和用户的密码进行比对。因为比对是在LDAP服务器内完成的,实际的密码能保持私密。
如果你希望通过密码比对进行认证,可以通过声明passwordCompare()
方法来实现:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userSearchBase("ou=people").userSearchFilter("(uid={0})").groupSearchBase("ou=groups")
.groupSearchFilter("member={0}").passwordCompare();
}
默认情况下,在登录表单中提供的密码将会与用户的LDAP条目中的userPassword
属性进行比对。如果密码被保存在不同的属性中,可以通过passwordAttribute()
方法来声明密码属性的名称:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userSearchBase("ou=people").userSearchFilter("(uid={0})").groupSearchBase("ou=groups")
.groupSearchFilter("member={0}").passwordCompare().passwordEncoder(new Md5PasswordEncoder())
.passwordAttribute("passcode");
}
在本例中,我们指定了要与给定密码进行比对的是passcode
属性。另外,我们还可以指定密码转码器。在进行服务器端密码比对时,有一点非常好,那就是实际的密码在服务器端是私密的。但是进行尝试的密码还是需要通过线路传输到LDAP服务器上,这可能会被黑客所拦截。为了避免这一点,我们可以通过调用passwordEncoder()
方法指定加密策略。
在本示例中,密码会进行MD5加密。这需要LDAP服务器上密码也使用MD5进行加密。
引用远程的LDAP服务器
默认情况下,Spring Security的LDAP认证假设LDAP服务器监听本机的33389端口。但是,如果你的LDAP服务器在另一台机器上,那么可以使用contextSource()
方法来配置这个地址:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapAuthenticationProviderConfigurer = auth
.ldapAuthentication();
ldapAuthenticationProviderConfigurer.userSearchBase("ou=people").userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups").groupSearchFilter("member={0}").contextSource()
.url("ldap://habuma.com:389/dc=habuma,dc=com");
ldapAuthenticationProviderConfigurer.passwordCompare().passwordEncoder(new Md5PasswordEncoder())
.passwordAttribute("passcode");
}
contextSource()
方法会返回一个ContextSourceBuilder对象,这个对象除了其他功能以外,还提供了url()
方法用来指定LDAP服务器的地址。
配置嵌入式的LDAP服务器
如果你没有现成的LDAP服务器供认证使用,Spring Security还为我们提供了嵌入式的LDAP服务器。我们不再需要设置远程LDAP服务器的URL,只需通过root()
方法指定嵌入式服务器的根前缀就可以了:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapAuthenticationProviderConfigurer = auth
.ldapAuthentication();
ldapAuthenticationProviderConfigurer.userSearchBase("ou=people").userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups").groupSearchFilter("member={0}").contextSource()
.root("dc=habuma,dc=com");
ldapAuthenticationProviderConfigurer.passwordCompare().passwordEncoder(new Md5PasswordEncoder())
.passwordAttribute("passcode");
}
当LDAP服务器启动时,它会尝试在类路径下寻找LDIF文件来加载数据。LDIF(LDAP Data Interchange Format,LDAP数据交换格式)是以文本文件展现LDAP数据的标准方式。每条记录可以有一行或多行,每项包含一个名值对。记录之间通过空行进行分割。
如果你不想让Spring从整个根路径下搜索LDIF文件的话,那么可以通过调用ldif()
方法来明确指定加载哪个LDIF文件:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapAuthenticationProviderConfigurer = auth
.ldapAuthentication();
ldapAuthenticationProviderConfigurer.userSearchBase("ou=people").userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups").groupSearchFilter("member={0}").contextSource()
.root("dc=habuma,dc=com")
.ldif("classpath:users.ldif");
ldapAuthenticationProviderConfigurer.passwordCompare().passwordEncoder(new Md5PasswordEncoder())
.passwordAttribute("passcode");
}
在这里,我们明确要求LDAP服务器从类路径根目录下的users.ldif
文件中加载内容。如果你比较好奇的话,如下就是一个包含用户数据LDIF文件,我们可以使用它来加载嵌入式LDAP服务器:
9.2.4 配置自定义的用户服务
假设我们需要认证的用户存储在非关系型数据库中,如Mongo或Neo4j,在这种情况下,我们需要提供一个自定义的UserDetailsService接口实现。
UserDetailsService接口非常简单:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
我们所需要做的就是实现loadUserByUsername()
方法,根据给定的用户名来查找用户。loadUserByUsername()
方法会返回代表给定用户的UserDetails对象。如下的程序清单展现了一个UserDetailsService的实现,它会从给定的SpitterRepository实现中查找用户。
package spittr.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import spittr.Spitter;
import spittr.data.SpitterRepository;
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()
方法将其设置到安全配置中:
package spittr.config;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import spittr.data.SpitterRepository;
import spittr.security.SpitterUserService;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SpitterRepository spitterRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new SpitterUserService(spitterRepository));
}
}
userDetailsService()
方法(类似于jdbcAuthentication()
、ldapAuthentication
以及inMemoryAuthentication()
)会配置一个用户存储。不过,这里所使用的不是Spring所提供的用户存储,而是使用UserDetailsService的实现。
另外一种值得考虑的方案就是修改Spitter,让其实现UserDetails。这样的话,loadUserByUsername()
就能直接返回Spitter对象了,而不必再将它的值复制到User对象中。
9.3 拦截请求
在任何应用中,并不是所有的请求都需要同等程度地保护。有些请求需要认证,而另一些可能并不需要。有些请求可能只有具备特定权限的用户才能访问,没有这些权限的用户会无法访问。
例如,考虑Spittr应用的请求。首页当然是公开的,不需要进行保护。类似地,因为所有的Spittle都是公开的,所以展现Spittle的页面不需要安全性。但是,创建Spittle的请求只有认证用户才能执行。同样,尽管用户基本信息页面是公开的,不需要认证,但是,如果要处理/spitters/me
请求,并展现当前用户的基本信息时,那么就需要进行认证,从而确定要展现谁的信息。
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)
方法。如下的代码片段展现了重载的configure(HttpSecurity)
方法,它为不同的URL路径有选择地应用安全性:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/spitters/me").authenticated().antMatchers(HttpMethod.POST, "/spittles")
.authenticated().anyRequest().permitAll();
}
configure()
方法中得到的HttpSecurity对象可以在多个方面配置HTTP的安全性。在这里,我们首先调用authorizeRequests()
,然后调用该方法所返回的对象的方法来配置请求级别的安全性细节。其中,第一次调用antMatchers()
指定了对/spitters/me
路径的请求需要进行认证。第二次调用antMatchers()
更为具体,说明对/spittles
路径的HTTP POST请求必须要经过认证。最后对anyRequests()
的调用中,说明其他所有的请求都是允许的,不需要认证和任何的权限。
antMatchers()
方法中设定的路径支持Ant风格的通配符。在这里我们并没有这样使用,但是也可以使用通配符来指定路径,如下所示:
.antMatchers("/spitters/**").authenticated();
也可以:
.antMatchers("/spitters/**", "/spittles/mine").authenticated();
antMatchers()
方法所使用的路径可能会包括Ant风格的通配符,而regexMatchers()
方法则能够接受正则表达式来定义请求路径。例如,如下代码片段所使用的正则表达式与/spitters/**
(Ant风格)功能是相同的:
.regexMatchers("/spitters/.*").authenticated();
除了路径选择,我们还通过authenticated()
和permitAll()
来定义该如何保护路径。authenticated()要求在执行该请求时,必须已经登录了应用。如果用户没有认证的话,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登录页面。同时,permitAll()
方法允许请求没有任何的安全限制。
除了authenticated()
和permitAll()
以外,还有其他的一些方法能够用来定义该如何保护请求。
表9.4 保护路径的配置方法
方法 | 能够做什么 |
---|---|
access(String) | 如果给定的SpEL表达式计算结果为true,就允许访问 |
anonymous() | 允许匿名用户访问 |
authenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
fullyAuthenticated() | 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问 |
hasAnyAuthority(String…) | 如果用户具备给定权限中的某一个的话,就允许访问 |
hasAnyRole(String…) | 如果用户具备给定角色中的某一个的话,就允许访问 |
hasAuthority(String) | 如果用户具备给定权限的话,就允许访问 |
hasIpAddress(String) | 如果请求来自给定IP地址的话,就允许访问 |
hasRole(String) | 如果用户具备给定角色的话,就允许访问 |
not() | 对其他访问方法的结果求反 |
permitAll() | 无条件允许访问 |
rememberMe() | 如果用户是通过Remember-me功能认证的,就允许访问 |
我们所配置的安全性能够不仅仅限于认证用户。例如,我们可以修改之前的configure()
方法,要求用户不仅需要认证,还要具备ROLE_SPITTER
权限:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/spitters/me").hasAnyAuthority("ROLE_SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasAnyAuthority("ROLE_SPITTER").anyRequest().permitAll();
}
作为替代方案,我们还可以使用hasRole()
方法,它会自动使用ROLE_
前缀。
我们可以将任意数量的antMatchers()
、regexMatchers()
和anyRequest()
连接起来,以满足Web应用安全规则的需要。但是,我们需要知道,这些规则会按照给定的顺序发挥作用。所以,很重要的一点就是将最为具体的请求路径放在前面,而最不具体的路径(如anyRequest()
)放在最后面。如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置。
9.3.1 使用Spring表达式进行安全保护
借助access()
方法,我们也可以将SpEL作为声明访问限制的一种方式。例如,如下就是使用SpEL表达式来声明具有ROLE_SPITTER
角色才能访问/spitter/me
URL:
.antMatchers("/spitters/me").access("hasRole('ROLE_SPITTER')")
表9.5 Spring Security安全性相关的SpEL表达式
安全表达式 | 计算结果 |
---|---|
authentication | 用户的认证对象 |
denyAll | 结果始终为false |
hasAnyRole(list of roles) | 如果用户被授予了列表中任意的指定角色,结果为true |
hasRole(role) | 如果用户被授予了指定的角色,结果为true |
hasIpAddress(IP Address) | 如果请求来自指定IP的话,结果为true |
isAnonymous() | 如果当前用户为匿名用户,结果为true |
isAuthenticated() | 如果当前用户进行了认证的话,结果为true |
isFullyAuthenticated() | 如果当前用户进行了完整认证的话(不是通过Remember-me功能进行的认证),结果为true |
isRememberMe() | 如果当前用户是通过Remember-me自动认证的,结果为true |
permitAll | 结果始终为true |
principal | 用户的principal对象 |
如果你想限制/spitter/me
URL的访问,不仅需要ROLE_SPITTER
,还需要来自指定的IP地址,那么我们可以按照如下的方式调用access()
方法:
.antMatchers("/spitters/me").access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.2)")
9.3.2 强制通道的安全性
传递到configure()
方法中的HttpSecurity对象,除了具有authorizeRequests()
方法以外,还有一个requiresChannel()
方法,借助这个方法能够为各种URL模式声明所要求的通道。
作为示例,可以参考Spittr应用的注册表单。尽管Spittr应用不需要信用卡号、社会保障号或其他特别敏感的信息,但用户有可能仍然希望信息是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()
方法,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/spitters/me")
.access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.2)")
.antMatchers(HttpMethod.POST, "/spittles").hasAnyAuthority("ROLE_SPITTER").anyRequest().permitAll()
.and().requiresChannel().antMatchers("/spitter/form").requiresSecure();
}
不论何时,只要是对/spitter/form
的请求,Spring Security都视为需要安全通道(通过调用requiresChannel()
确定的)并自动将请求重定向到HTTPS上。
与之相反,有些页面并不需要通过HTTPS传送。例如,首页不包含任何敏感信息,因此并不需要通过HTTPS传送。我们可以使用requiresInsecure()
代替requiresSecure()
方法,将首页声明为始终通过HTTP传送:
.antMathcers("/").requiresInecure();
9.3.3 防止跨站请求伪造
我们可以回忆一下,当一个POST请求提交到/spittles
上时,SpittleController将会为用户创建一个新的Spittle对象。但是,如果这个POST请求来源于其他站点的话,会怎么样呢?如果在其他站点提交如下表单,这个POST请求会造成什么样的结果呢?
<form input="POST" action="http://www.spittr.com/spittles">
<input type="hidden" name="message" value="I'm stupid!" />
<input type="submit" value="Click here to win a new car!" />
</form>
假设你禁不住获得一辆新汽车的诱惑,点击了按钮——那么你将会提交表单到如下地址http://www.spittr.com/spittles
。如果你已经登录到了spittr.com
,那么这就会广播一条消息,让每个人都知道你做了一件蠢事。
这是跨站请求伪造(cross-site request forgery,CSRF)的一个简单样例。简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来消极的后果。尽管提交I'm stupid!
这样的信息到微博站点算不上什么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一致,这样的话当表单提交的时候,才能进行匹配。
好消息是,Spring Security已经简化了将token放到请求的属性中这一任务。如果你使用Thymeleaf作为页面模板的话,只要<form>
标签的action
属性添加了Thymeleaf命名空间前缀,那么就会自动生成一个_csrf
隐藏域:
<form method="POST" th:action="@{/spittles}">
...
</form>
使用JSP如下:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
更好的功能是,如果使用Spring的表单绑定标签的话,<sf:form>
标签会自动为我们添加隐藏的CSRF token标签。
处理CSRF的另外一种方式就是根本不去处理它。我们可以在配置中通过调用csrf().disable()
禁用Spring Security的CSRF防护功能,如下所示:
http.csrf().disable();
需要提醒的是,禁用CSRF防护功能通常来讲并不是一个好主意。如果这样做的话,那么应用就会面临CSRF攻击的风险。只有在深思熟虑之后,才能使用配置。
9.4 认证用户
如果你使用最简单的Spring Security配置的话,那么就能无偿地得到一个登录页。实际上,在重写configure(HttpSecurity)
之前,我们都能使用一个简单却功能完备的登录页。但是,一旦重写了configure(HttpSecurity)
方法,就失去了这个简单的登录页面。
不过,把这个功能找回来也很容易。我们所需要做的就是在configure(HttpSecurity)
方法中,调用formLogin()
,如下面的程序所示。
请注意,和前面一样,这里调用add()
方法来将不同的配置指令连接在一起。
如果我们访问应用的/login
链接或者导航到需要认证的页面,那么将会在浏览器中展现登录页面。如图9.2所示,在审美上它没有什么令人兴奋的,但是它却能实现所需的功能。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests().antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER").anyRequest().permitAll().and()
.requiresChannel().antMatchers("/spitter/form").requiresSecure();
}
9.4.1 添加自定义的登录页
默认的登录页面:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/Spittr/login' method='POST'>
<table>
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
<tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
<input name="_csrf" type="hidden" value="b9147218-5a1b-4e78-92fb-9b3a242f6312" />
</table>
</form></body></html>
需要注意的一个关键点是<form>
提交到了什么地方。同时还需要注意username
和password
输入域,在你的登录页中,需要同样的输入域。最后,假设没有禁用CSRF的话,还需要保证包含了值为CSRF token的_csrf
输入域。
如下程序清单所展现的Thymeleaf模板提供了一个与Spittr应用风格一致的登录页。
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"></link>
</head>
<body onload='document.f.username.focus();'>
<div id="header" th:include="page :: header"></div>
<div id="content">
<a th:href="@{/spitter/register}">Register</a>
<form name='f' th:action='@{/login}' method='POST'>
<table>
<tr><td>User:</td><td>
<input type='text' name='username' value='' /></td></tr>
<tr><td>Password:</td>
<td><input type='password' name='password'/></td></tr>
<tr><td colspan='2'>
<input name="submit" type="submit" value="Login"/></td></tr>
</table>
</form>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>
需要注意的是,在Thymeleaf模板中,包含了username
和password
输入域,就像默认的登录页一样,它也提交到了相对于上下文的/login
页面上。因为这是一个Thymeleaf模板,因此隐藏的csrf
域将会自动添加到表单中。
9.4.2 启用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().authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER").antMatchers(HttpMethod.POST, "/spittles")
.hasRole("SPITTER").anyRequest().permitAll().and().requiresChannel().antMatchers("/spitter/form")
.requiresSecure();
}
注意,和前面一样,在configure()
方法中,通过调用add()
方法来将不同的配置指令连接在一起。
在httpBasic()
方法中,并没有太多的可配置项,甚至不需要什么额外配置。HTTP Basic认证要么开启要么关闭。所以,与其进一步研究这个话题,还不如看看如何通过Remember-me功能实现用户的自动认证。
9.4.3 启用Remember-me功能
为了启用这项功能,只需在configure()
方法所传入的HttpSecurity对象上调用rememberMe()
即可。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/login").and().rememberMe().tokenValiditySeconds(2419200).key("spittrKey").and()
.httpBasic().realmName("Spittr").and().authorizeRequests().antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER").anyRequest().permitAll().and()
.requiresChannel().antMatchers("/spitter/form").requiresSecure();
}
在这里,我们通过一点特殊的配置就可以启用Remember-me功能。默认情况下,这个功能是通过在cookie中存储一个token完成的,这个token最多两周内有效。但是,在这里,我们指定这个token最多四周内有效(2,419,200秒)。
存储在cookie中的token包含用户名、密码、过期时间和一个私钥——在写入cookie前都进行了MD5哈希。默认情况下,私钥的名为SpringSecured,但在这里我们将其设置为spitterKey,使它专门用于Spittr应用。
如此简单。既然Remember-me功能已经启用,我们需要有一种方式来让用户表明他们希望应用程序能够记住他们。为了实现这一点,登录请求必须包含一个名为remember-me的参数。在登录表单中,增加一个简单复选框就可以完成这件事情:
<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label></td></tr>
9.4.4 退出
其实,按照我们的配置,退出功能已经启用了,不需要再做其他的配置了。我们需要的只是一个使用该功能的链接。
退出功能是通过Servlet容器中的Filter实现的(默认情况下),这个Filter会拦截针对/logout
的请求。因此,为应用添加退出功能只需添加如下的链接即可(如下以Thymeleaf代码片段的形式进行了展现):
<a th:href="@{/logout}">Logout</a>
当用户点击这个链接的时候,会发起对/logout
的请求,这个请求会被Spring Security的LogoutFilter所处理。用户会退出应用,所有的Remember-me token都会被清除掉。在退出完成后,用户浏览器将会重定向到/login?logout
,从而允许用户进行再次登录。
如果你希望用户被重定向到其他的页面,如应用的首页,那么可以在configure()
中进行如下的配置:
http.formLogin().loginPage("/login").and().logout().logoutSuccessUrl("/")
9.5 保护视图
当为浏览器渲染HTML内容时,你可能希望视图中能够反映安全限制和相关的信息。一个简单的样例就是渲染用户的基本信息(比如显示“您已经以……身份登录”)。或者你想根据用户被授予了什么权限,有条件地渲染特定的视图元素。
9.5.1 使用Spring Security的JSP标签库
表9.6 Spring Security通过JSP标签库在视图层上支持安全性
JSP标签 | 作用 |
---|---|
<security:accesscontrollist> | 如果用户通过访问控制列表授予了指定的权限,那么渲染该标签体中的内容 |
<security:authentication> | 渲染当前用户认证对象的详细信息 |
<security:authorize> | 如果用户被授予了特定的权限或者SpEL表达式的计算结果为true,那么渲染该标签体中的内容 |
为了使用JSP标签,我们需要声明它:
<% @taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
访问认证信息的细节
借助Spring Security JSP标签库,所能做到的最简单的一件事情就是便利地访问用户的认证信息。例如,对于Web站点来讲,在页面顶部以用户名标示显示“欢迎”或“您好”信息是很常见的。这恰恰是<security:authentication>
能为我们所做的事情。例如:
Hello <security:authentication property="principal.username" />!
其中,property
用来标示用户认证对象的一个属性。可用的属性取决于用户认证的方式。但是,我们可以依赖几个通用的属性,在不同的认证方式下,它们都是可用的。
表9.7 <security:authentiation>
认证详情
认证属性 | 描述 |
---|---|
authorities | 一组用于表示用户所授予权限的GrantedAuthority对象 |
Credentials | 用于核实用户的凭证(通常,这会是用户的密码) |
details | 认证的附加信息(IP地址、证件序列号、会话ID等) |
principal | 用户的基本信息对象 |
在我们的示例中,实际上渲染的是principal
属性中嵌套的username
属性。
当像前面示例那样使用时,<security:authentication>
将在视图中渲染属性的值。但是如果你愿意将其赋值给一个变量,那只需要在var
属性中指明变量的名字即可。例如,如下展现了如何将其设置给名为loginId
的属性:
<security:authentication property="principal.username" var="loginId" />
这个变量默认是定义在页面作用域内的。但是如果你愿意在其他作用域内创建它,例如请求或会话作用域(或者是能够在javax.servlet.jsp.PageContext中获取的其他作用域),那么可以通过scope
属性来声明。例如,要在请求作用域内创建这个变量,那可以使用<security:authentication>
按照如下的方式来设置:
<security:authentication property="principal.username" var="loginId" scope="request" />
条件性的渲染内容
有时候视图上的一部分内容需要根据用户被授予了什么权限来确定是否渲染。对于已经登录的用户显示登录表单,或者对还未登录的用户显示个性化的问候信息都是毫无意义的。
Spring Security的<security:authorize>
JSP标签能够根据用户被授予的权限有条件地渲染页面的部分内容。例如,在Spittr应用中,对于没有ROLE_SPITTER
角色的用户,我们不会为其显示添加新Spitter记录的表单。下面展现了如何使用<security:authorize>
标签来为具有ROLE_SPITTER
角色的用户显示Spitter表单。
<sec:authorize access="hasRole('ROLE_SPITTER)">
<s:url value="/spittles" var="spittle_url"></s:url>
<sf:form modelAttribute="spittle" action="${spittle_url}">
<sf:label path="text">
<s:message code="label.spittle" text="Enter spittle:"></s:message>
</sf:label>
<sf:textarea path="text" rows="2" cols="40" />
<sf:errors path="text"></sf:errors>
<br />
<div class="spitItSubmit">
<input type="submit" value="Spit it!"
class="status-btn round-btn disabled" />
</div>
</sf:form>
</sec:authorize>
access
属性被赋值为一个SpEL表达式,这个表达式的值将确定<security:authorize>
标签主体内的内容是否渲染。这里我们使用了hasRole('ROLE_SPITTER')
表达式来确保用户具有ROLE_SPITTER
角色。但是,当你设置access
属性时,可以任意发挥SpEL的强大威力。
借助于这些可用的表达式,可以构造出非常有意思的安全性约束。例如,假设应用中有一些管理功能只能对用户名为habuma
的用户可用。也许你会像这样使用isAuthenticated()
和principal
表达式:
<sec:authorize access="isAuthenticated() and principal.username=='habuma'">
...
</sec:authorize>
但是我构造的这个示例还有一件事让人很困惑。尽管我想限制管理功能只能给habuma
用户,但使用JSP标签表达式并不见得理想。确实,它能在视图上阻止链接的渲染。但是没有什么可以阻止别人在浏览器的地址栏手动输入/admin
这个URL。
根据我们在本章前面所学,这是一个很容易解决的问题。在安全配置中,添加一个对antMatchers()
方法的调用将会严格限制对/admin
这个URL的访问。
.antMatchers("/admin").access("isAuthenticated() and principal.username=='habuma'")
现在,管理功能已经被锁定了。URL地址得到了保护,并且到这个URL的链接在用户没有授权使用的情况下不会显示。但是为了做到这一点,我们需要在两个地方声明SpEL表达式——在安全配置中以及在<security:authorize>
标签的access
属性中。有没有办法消除这种重复性,并且还要确保只有规则条件满足的情况下才渲染管理功能的链接呢?
这是<security:authorize>
的url
属性所要做的事情。它不像access
属性那样明确声明安全性限制,url
属性对一个给定的URL模式会间接引用其安全性约束。鉴于我们已经在Spring Security配置中为/admin
声明了安全性约束,所以我们可以这样使用url
属性:
<security:authorize url="/admin">
<spring:url value="/admin" var="admin_url" />
<br />
<a href="${admin_url}">Admin</a>
</security:authorize>
9.5.2 使用Thymeleaf的Spring Security方言
表9.8 Thymeleaf的安全方言提供的标签
属性 | 作用 |
---|---|
sec:authentication | 渲染认证对象的属性。类似于Spring Security的<sec:authentication/> JSP标签 |
sec:authorize | 基于表达式的计算结果,条件性的渲染内容。类似于Spring Security的<sec:authorize/> JSP标签 |
sec:authorize-acl | 基于表达式的计算结果,条件性的渲染内容。类似于Spring Security的<sec:accesscontrollist/> JSP标签 |
sec:authorize-expr | sec:authorize属性的别名 |
sec:authorize-url | 基于给定URL路径相关的安全规则,条件性的渲染内容。类似于Spring Security的<sec:authorize/> JSP标签使用url属性时的场景 |
为了使用安全方言,我们需要确保Thymeleaf Extras Spring Security已经位于应用的类路径下。然后,还需要在配置中使用SpringTemplateEngine来注册SpringSecurity Dialect。@Bean
方法声明了SpringTemplateEngine bean,其中就包含了SpringSecurityDialect。
@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.addDialect(new SpringSecurityDialect());
return templateEngine;
}
安全方言注册完成之后,我们就可以在Thymeleaf模板中使用它的属性了。首先,需要在使用这些属性的模板中声明安全命名空间:
<html xmlns="http://www.w3.org/1999/xthml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
</html>
在这里,标准的Thymeleaf方法依旧与之前一样,使用th
前缀,安全方言则设置为使用sec
前缀。
这样我们就能在任意合适的地方使用Thymeleaf属性了。比如,假设我们想要为认证用户渲染Hello
文本。如下的Thymeleaf模板代码片段就能完成这项任务:
<div sec:authorize="isAuthenticated()">
Hello <span sec:authentication="name"></span>
</div>
sec:authorize
属性会接受一个SpEL表达式。如果表达式的计算结果为true,那么元素的主体内容就会渲染。在本例中,表达式为isAuthenticated()
,所以只有用户已经进行了认证,才会渲染<div>
标签的主体内容。就这个标签的主体内容部分而言,它的功能是使用认证对象的name
属性提示Hello
文本。
你可能还记得,在Spring Security中,借助<sec:authorize>
JSP标签的url
属性能够基于给定URL的权限有条件地渲染内容。在Thymeleaf中,我们可以通过sec:authorize-url
属性完成相同的功能。例如,如下Thymeleaf代码片段所实现的功能与之前<sec:authorize>
JSP标签和url属性所实现的功能是相同的:
<span sec:authorize-url="/admin"><br /><a th:href="@{/admin}">Admin</a></span>