我们在开发Web应用时,通常希望保护某些资源(页面或数据),对这些资源做安全控制,比如:未登录的用户访问资源时,自动跳转至登录页面;拥有管理员身份的用户,可以访问某些管理界面。
本文介绍一种基于Spring Security的动态配置权限的方法,使用的是JavaConfig的方式。
Spring Security是一个功能强大且可高度自定义的身份验证和访问控制框架。具有以下特性:
- 对身份验证和授权的全面和可扩展的支持
- 防止会话固定,点击劫持,跨站点请求伪造等攻击
- Servlet API集成
- 可选与Spring Web MVC集成
- 还有很多啊..
项目中引入Spring Security依赖
我创建的是Maven项目,在pom文件中添加如下依赖:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security.version}</version>
</dependency>
在web应用中启用Spring Security
Servlet3.0+
如果应用部署在支持Servlet3.0+环境下,可以使用AbstractSecurityWebApplicationInitializer启用Spring Security,代码清单如下:
1 | public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { |
1 | public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { |
以上,可以理解为,Spring自动注册了某些filter。
Servlet2.5
一些主流的中间件,仅支持Servlet2.5,比如WebLogic、Web Sphere。如果应用需要部署在这些中间件下,则不能使用以上方式,需要在web.xml中自行配置filter。代码清单如下:1
2
3
4
5
6
7
8
9<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
基于Spring Security进行安全配置
启用Spring Security配置
首先创建一个继承WebSecurityConfigurerAdapter抽象类的类,这样就获得了Spring Security提供的默认配置。然后使用@Configuration注解使这个类成为Spring的一个配置类,Spring启动时会读取类中的配置。最后使用@EnableWebSecurity注解启用Spring Security。
WebSecurityConfigurerAdapter抽象类,提供了一个方便的、开箱即用的Spring Security配置。该配置创建一个名为springSecurityFilterChain的Servlet过滤器,它负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。想要实现自定义配置,只需要覆写相应的方法。
1 |
|
接下来,将介绍Spring Security的具体配置,比如如何配置允许哪些用户登录,如何配置用户权限等。
具体配置
实现自定义配置,需要覆写基类中的configure方法。1
2
3
4
protected void configure(HttpSecurity http) throws Exception {
// TODO:这里写你的具体配置
}
登录配置
假设你有如下需求:
- Web应用需要支持表单登录
- 登录界面或登录请求使用的是一个自定义的url
- 登录请求中,提交的用户名、密码字段名是自定义的
- 登录请求提交的参数中除用户名密码以外,还需要验证其他参数
- 自定义登录成功或失败的后续处理:比如登录成功时,先提示消息,再跳转到首页。
以下代码可以实现上述需求:
1 | http.formLogin() |
接下来,详细说明每行配置的作用。
formLogin
formLogin方法告知Spring Security开启表单登录,即以一种用户名密码登录的方式。
loginPage和loginProcessingUrl
loginPage指定登陆页面的url是/login,这个配置的作用是当用户未登录时,请求将重定向到该url上,现象是跳转回登录页面。
loginProcessingUrl指定登录请求的url是/system/login.{ext},可以通过Ajax的方式将用户名和密码提交到该url上,即一次登录请求。注意,Spring Security默认登录请求是POST类型。1
2
3
4$.ajax({
url : "/system/login.json",
type : "POST"
});
以上两个url中,都可以使用通配符,比如我使用的”{ext},也就是说,可以url可以是/system/login.json或/system/login.xml,Spring Security会处理符合这个url的所有请求。
usernameParameter和passwordParameter
指定登录请求中,提交的用户名、密码字段名分别是user_id和user_password。1
2
3
4
5
6
7
8$.ajax({
url : "/system/login.json",
type : "POST",
data : {
'user_id': userName,
'user_password' : password
}
});
authenticationDetailsSource
使用一个自定义的AuthenticationDetailsSource,指定登录请求中,获取哪些数据作为凭证,供验证器验证。
Spring Security通过WebAuthenticationDetails类,记录web请求的客户端ip地址和sessionId。它在获取IP地址时,只是简单的调用了HttpServletRequest的getRemoteAddr方法,在集群环境下,这样获取不能获取到客户端的真实IP,因此我覆写了获取客户端ip地址部分代码,代码清单如下:1
2
3
4
5
6
7
8public class GetIpWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
public GetIpWebAuthenticationDetailsSource() {
}
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
// 根据请求内容 构造本次请求的详细信息
return new GetIpWebAuthenticationDetails(context);
}
}
1 | public class GetIpWebAuthenticationDetails extends WebAuthenticationDetails { |
还可以在自定义的WebAuthenticationDetails实现类中,添加更多的属性,用来记录需要验证的参数
successHandler和failureHandler
successHandler方法指定了登录成功处理器,这里我返回了一个登陆成功的提示信息,同时将用户信息存入session中。
failureHandler方法指定了登录失败处理器,这里我返回了一个登录失败的具体的提示信息,比如用户不存在、密码错误。实际上是不推荐这样做,提示信息应该是用户名或密码错误。1
2
3
4
5
6
7
8
9
10
11
12public class SupportAjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final static String RIGHT_CREDENTIALS = "rightCredentials";
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 直接返回成功信息
SecurityHandlerUtil.successResponse(response, RIGHT_CREDENTIALS);
UserContext user = new UserContext();
user.setOperId(authentication.getName());
// 用户信息存入session
request.getSession().setAttribute("user", user);
}
}
1 | public class SupportAjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler { |
登录数据源配置
一般来说,用户都是存在表中,Spring Security提供了多种数据源,开箱即用。这里我通过扩展DaoAuthenticationProvider,获取基类的所有功能,并新增了一个IP白名单的功能,只有IP白名单内的客户端可以使用正确的用户名密码登录。此外,还需要指定从哪个表里查询,查询哪些字段等。为此,Spring Security规定了一个UserDetailsService接口,并提供了一系列开箱即用的实现类,而这里我使用了自己的实现类。
配置登录数据源时,需要覆写configure的另外一个重载方法,代码清单如下:
1 |
|
1 | "apiPlatUserService") ( |
1 | public class IpAuthenticationProvider extends DaoAuthenticationProvider { |
登出配置
假设你有如下需求:
- Web应用需要支持登出
- 登出请求使用的是一个自定义的url
- 自定义登出成功或失败的后续处理:比如登出成功时,先提示消息,再跳转到登录页
以下代码可以实现上述需求:
1 | http.logout() |
接下来,详细说明每行配置的作用。
logout
logout方法告知Spring Security支持登出处理,登出时,Spring Security自动清除用户缓存并使session失效。
logoutUrl
logoutUrl指定登出请求的url是/system/logout.{ext},可以通过Ajax的方式请求该url上,即一次登出请求。注意,Spring Security默认登出请求是POST类型。1
2
3
4$.ajax({
url : "/system/logout.json",
type : "POST"
});
资源访问限制
一般来说,web应用都会做角色-菜单配置,即拥有某身份的用户可以访问响应的资源。而这个配置,通常不是硬编码的,可能使在表中配置的,也可能是在配置文件中配置的。代码清单如下:
1 | ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http.authorizeRequests(); |
其中,securityConfigCache是一个由Spring管理的bean,缓存了表中的配置。在初始化该bean时,从表中查询角色-菜单配置,构造一个资源-权限映射。
自定义异常处理器
1 | http.exceptionHandling() |
accessDeniedHandler
accessDeniedHandler方法指定了拒绝请求处理器。也就是指定当用户无权限访问资源时,如何对请求做出响应。
请求大体分为两种,一种是请求页面,一种是请求数据。而对这两种请求的拒绝处理也是不一样的,请求页面时,需要将请求重定向到登录界面,提示用户登录;请求数据时,则返回一个错误信息,提示用户无权访问。代码清单如下:
1 | public class UnauthorizedAccessDeniedHandler implements AccessDeniedHandler { |
1 |
|
authenticationEntryPoint
authenticationEntryPoint方法指定了用户未登录时的入口点。由于用户尚未通过身份验证,因此需要返回一个响应,指示用户必须先进行身份验证。响应同样是根据请求类型决定,如果请求页面,将重定向到登录页;如果请求数据,将返回错误消息。
1 | public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { |
自定义错误信息数据源
1 |
|
攻击保护配置
同源访问
1 | http.headers() |
防crsf
1 | http.crsf(); |
session保护
1 | http.sessionManagement() |