1 Spring Security简介
Spring Security是Spring框架中的独立项目,是一个安全框架,能够为基于Spring的Java EE应用提供声明式的安全访问控制解决方案。它提供了一组可以在Spring应用上下文中配置的安全对象,充分利用了Spring DI(依赖注入)和AOP(面向切面)功能,为应用系统提供声明式的访问控制机制,以减少了企业级开发中需要重复编写大量安全代码的工作。
2 Spring Security 4使用入门
本文基于Spring Security的“4.0.3.RELEASE”版本讲述,应配合Spring主框架“4.2.5. RELEASE”版本使用,具体配置有别于早期的Spring 3.x。
2.1 简单启用Security
下面以Spring MVC项目为例,使用Security框架实现访问控制。
(1)导入依赖
Spring Security是独立于Spring的安全框架,我们可以通过Maven,导入相关的依赖
代码语言:javascript复制 <!-- Spring Security 4 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.0.3.RELEASE</version>
</dependency>
<!-- Spring Security 的JSP标签,页面用到Security标签才需要导入 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.0.3.RELEASE</version>
</dependency>
(2)在web.xml中添加过滤器,启动Spring Security功能
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
......
<!-- 启动Spring容器的监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security.xml</param-value>
</context-param>
......
<!-- Spring Security -->
<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>
......
</web-app>
在上述配置中:
- 监听器“ContextLoaderListener”用于在web项目启动时创建Spring容器,只要使用了Spring的web项目一般都需要配置。值得注意的是其监听器参数“contextConfigLocation”中指定了一个Spring Security的配置文件“classpath:spring-security.xml”,启动Spring容器的同时也加载了Security的配置。
- 过滤器“DelegatingFilterProxy”用于启用Spring Security对web请求的拦截和检验功能。
(3)编写spring-security.xml配置文件,为Security提供配置。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 设置无需安全验证的路径模式 -->
<http pattern="/content/**" security="none" />
<!-- 启用SpEL设置安全模式 -->
<http auto-config="true" use-expressions="true">
<!-- 只允许拥有“ADMIN”权限的用户访问 -->
<intercept-url pattern="/admin/**" access="hasAuthority('管理员')"/>
<!-- 只允许登录用户访问 -->
<intercept-url pattern="/account/modify" access="isAuthenticated()"/>
</http>
<!-- 预定义一些账户 -->
<authentication-manager>
<authentication-provider>
<user-service>
<user name="zhang3" password="123" authorities="管理员"/>
<user name="li4" password="123" authorities="普通用户"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
hasAuthority()是Spring Security 所支持的SpEL表达式,表示“拥有”哪些权限。相关的SpEL还有如下这些。
2.2 指定deny页面
当已登录用户没有访问某些页面的权限时,Spring Security默认会返回Http的403错误。
默认的权限报错界面
如果希望看到自定义的友好界面,则需要在<http>设置<access-denied-handler>
代码语言:javascript复制<http auto-config="true" use-expressions="true">
<!-- 指定没有权限时显示的页面 -->
<access-denied-handler error-page="/deny" />
……
</http>
配置后的授权报错页面
2.3 自定义登录页面
Spring Security默认提供了登录界面和验证功能,但的登录页面比较简陋,基本不符合我们的需求。幸好,我们可以通过配置的方式快速将其替代为自己的登录页面,只要确保其中的提交路径信息正确就可以了。
默认的登录页面
(1)通过<http>配置中的<form-login>元素,可以替换登录页面的相关信息。
代码语言:javascript复制<http auto-config="true" use-expressions="true">
<!-- 指定登录页面路径、登录处理路径 -->
<form-login
default-target-url="/index"
login-page="/login"
authentication-failure-url="/login?error=1"
login-processing-url="/checklogin" />
……
</http>
其中各配置属性的含义如下:
default-target-url 指定登录成功后的默认跳转位置;
login-page 指定登录页面的URL
authentication-failure-url 指定登录验证失败的跳转路径error参数用于标识报错
login-processing-url 指定登录验证功能的提交路径,该功能无需编码
(2)登录页面的实现
代码语言:javascript复制 <form action="${pageContext.request.contextPath}/checklogin" method="post">
<fieldset id="editForm">
<legend>用户登录</legend>
<div>
<label class="fieldname">用户名</label>
<input type="text" name="username" />
</div>
<div>
<label class="fieldname">密码</label>
<input type="password" name="password" />
</div>
<div>
<label class="fieldname"> </label>
<input type="submit" value="登录" />
</div>
<c:if test="${param.error!=null}">
<p style="color:red;text-align: center;line-height: 1;">用户名或密码有误。</p>
</c:if>
<c:if test="${param.logout != null}">
<p style="color:red;text-align: center;line-height: 1;">登录已注销。</p>
</c:if>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</fieldset>
</form>
在上述页面中:
- 登录页面中用户名字段必须为名为“username”,密码字段必须为“password”。
- error参数和logout参数是可选的,用来标识登录失败或已经注销。
- “<input type="hidden" name="
<http auto-config="true" use-expressions="true">
<csrf />
……
</http>
2.4 启用方法级别拦截
对于MVC框架而言,URL请求往往是被映射到Controller类的Action方法上去的,因此就URL进行安全拦截似乎不是最合适的方式。Spring Security不仅仅能提供基于URL路径的HTTP拦截,还可以提供基于方法级别的调用拦截;因此,我们可以把权限控制设置到对应的Controller类或Action方法上去,让权限配置更为灵活。
(1)启用方法级别的权限拦截
需要使用方法级别的拦截,需要设置“<security:global-method-security pre-post-annotations="enabled" />”。
在Spring MVC项目中,我们可以修改MVC配置文件“springmvc-servlet.xml”,添加
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 开启Spring Security 方法级注解配置 -->
<security:global-method-security pre-post-annotations="enabled" />
....
</beans>
(2)使用 @PreAuthorize 注解
@PreAuthorize注解可以应用在类或方法上方,标识该类或该方法执行前需要提供相应的权限。
将权限设置到Action方法上:
代码语言:javascript复制@Controller
@RequestMapping("/account")
public class AccountController {
……
@PreAuthorize("isAuthenticated()")
@RequestMapping("/modify")
public String modify(){
return "account-modify";
}
}
将权限设置到Controller上,则为此控制器的所有Action方法设置权限:
代码语言:javascript复制@Controller
@RequestMapping("/account")
public class AccountController {
……
@PreAuthorize("isAuthenticated()")
@RequestMapping("/modify")
public String modify(){
return "account-modify";
}
}
2.5 自定验证和授权过程
上述示例中使用的用户账号及权限,都是直接配置在配置文件中的,实际开发中,用户账号和权限应该是存放在数据库中的。Spring Security提供了多种方式可以自定义账户和权限信息的来源,其中最简单的方式是重写“UserDetailsService”接口中的“loadUserByUsername”方法,根据用户名编码查询并返回用户账户信息。Spring Security中使用UserDetails接口描述用户信息。
(1)数据库结构
用户表Users
角色标Role
用户-角色 多对多中间表 UserRole
(2)用户信息类(UserDetails接口)的实现如下
代码语言:javascript复制public class UserDetailsImpl implements UserDetails {
……
private Collection<GrantedAuthority> authorities;
private String password;
private String username;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enable;
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public boolean isEnabled() {
return enable;
}
}
(3)用户信息的查询类(UserDetailsService接口)的实现如下。其中调用了数据访问类UserDao查询用户信息和权限。
代码语言:javascript复制@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserDao userDao;
//关键接口方法
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userDao.fetchByUsername(username);
if(user==null)
throw new UsernameNotFoundException("用户名有误。"); //该异常标识失败
UserDetailsImpl details = new UserDetailsImpl(
getAuthoritiesByUserId(user.getId()), //authorities Set<GrantedAuthority>
user.getPassword(),
user.getUsername(),
true, //accountNonExpired
true, //accountNonLocked
true, //credentialsNonExpired
true //enable
);
return details;
}
//根据用户名(用户Id)获取用户权限
private Set<GrantedAuthority> getAuthoritiesByUserId(int userId){
Set<GrantedAuthority> set = new HashSet<GrantedAuthority>();
List<Role> roles = userDao.getRolesByUserId(userId);
for(Role r : roles){
set.add(new SimpleGrantedAuthority(r.getName()));
}
return set;
}
}
(4)修改Spring Security配置文件“spring-security.xml”,更换用户账号的来源。
代码语言:javascript复制 <!-- 管理登录账号 -->
<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
2.6 关于注销
Spring Security还默认提供了注销登录功能(logout),只需在<http>元素下配置<logout>子元素,就能实现注销登录。
代码语言:javascript复制<!-- 指定注销路径 -->
<logout
logout-url="/logout"
logout-success-url="/login?logout=1"
invalidate-session="true" />
上述配置中:
logout-url 指定了注销请求的提交地址
logout-success-url 指定了注销成功后跳转的页面,其中的“logout=1”仅为表示,可选
值得注意的是,如果启用了防范csrf的令牌功能,则必须使用POST方式并且提供防csrf令牌才成功请求logout功能,否则会出现404访问错误,而默认防csrf令牌功能是开启的!
2.7 获取已登录用户信息
(1)使用Spring Security标签获取用户信息
如果只是想从页面上显示当前登陆的用户名,可以直接使用Spring Security提供的taglib。
代码语言:javascript复制<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<div>username : <sec:authentication property="name"/></div>
使用该标签库,则需要导入以下Maven依赖。
代码语言:javascript复制<!-- Spring Security 的JSP标签,页面用到Security标签才需要导入 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.0.3.RELEASE</version>
</dependency>
(2)在Controller中获取用户信息
如果想在程序中获得当前登陆用户对应的对象,可以使用以下代码
代码语言:javascript复制UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
如果想获得当前登陆用户所拥有的所有权限,可以使用以下代码。
代码语言:javascript复制GrantedAuthority[] authorities = userDetails.getAuthorities();
必要时,我们还可以继承并扩展GrantedAuthority和UserDetails类,进一步保存自定义的用户信息。
3 记住我的登录状态
很多时候,用户希望记住自己的登录状态,以便下次免登录。Spring Security内置了remember-me的Cookie机制,只要配置就可实现记住登录状态的功能。
(1)在<http>元素下添加<remember-me>子元素,开启功能。
代码语言:javascript复制 <http auto-config="true" use-expressions="true">
……
<remember-me/>
</http>
(2)在登录表单中添加“name”等于“remember-me”的checkbox。
代码语言:javascript复制<form action="${pageContext.request.contextPath}/account/checklogin" method="post">
…
<label class="fieldname">下次免登录</label>
<input type="checkbox" name="remember-me" />
</form>
至此,当用户选择“记住登录状态”时,登录时将会多产生一个名为“remember-me”的长期cookie,用作下次免登录的标识。
不使用remember-me登录后的cookie
使用remember-me登录后的cookie
4 账户密码的散列加密
在实际应用中,我们往往需要对密码进行散列处理,以免被后台管理人员盗取,也就是说,后台服务器中记录的是经过hash算法加密的密码,而不是明码。
Spring Security内置了密码散列的匹配功能,只要修改<authentication-manager>配置即可。
代码语言:javascript复制<!-- 管理登录账号 -->
<authentication-manager>
<authentication-provider user-service-ref="userDetailsService">
<!-- 密码使用md5散列加密 -->
<password-encoder hash="md5">
<!-- 使用UserDetails的username属性作为盐值 -->
<salt-source user-property="username"/>
</password-encoder>
</authentication-provider>
</authentication-manager>
这时,数据库中保存的数据就不是原来的明码密码了,而是散列之后的密码。
明码的密码值
散列加密的密码值
用户注册时,系统需要用散列算法对密码进行加密,可以使用Spring Security提供的“Md5PasswordEncoder”类实现。
以下为测试代码,使用UserDetails中的username作为密码的“盐值”(salt)。
代码语言:javascript复制public class SpringMd5Test {
@Test
public void test(){
Md5PasswordEncoder md5 = new Md5PasswordEncoder();
String zhang3 = md5.encodePassword("123", "zhang3");
String li4 = md5.encodePassword("123", "li4");
String wang5 = md5.encodePassword("123", "wang5");
System.out.println(zhang3);
System.out.println(li4);
System.out.println(wang5);
}
}
以下是输出的散列码。