一、简介
Spring Security是Spring Framework的一个子项目,之前叫做Acegi Security,用于保护各种Java应用,是一个权限管理和访问控制框架,在基于Java的Web应用中使用广泛。
Spring Security能以声明的方式来保护Web应用,比如限制URL的访问,这对于SpringSecurity来说只需要简单的配置即可。
Spring Security通过一系列的Servlet过滤器(即Filter)为Web应用程序提供多种安全服务。
二、环境搭建
1、添加Spring + SpringMVC + Spring Security 的相关jar包
commons-logging-1.0.4.jar
spring-aop-4.3.0.RELEASE.jar
spring-beans-4.3.0.RELEASE.jar
spring-context-4.3.0.RELEASE.jar
spring-core-4.3.0.RELEASE.jar
spring-expression-4.3.0.RELEASE.jar
spring-security-acl-3.1.0.RELEASE.jar
spring-security-config-3.1.0.RELEASE.jar
spring-security-core-3.1.0.RELEASE.jar
spring-security-taglibs-3.1.0.RELEASE.jar
spring-security-web-3.1.0.RELEASE.jar
spring-web-4.3.0.RELEASE.jar
spring-webmvc-4.3.0.RELEASE.jar
2、在web.xml中配置Spring的容器监听器和Spring容器配置文件的位置
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
3、在web.xml中配置web请求的过滤器:其实就是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>
4、在Spring的容器配置文件applicationContext.xml中配置SpringSecurity的请求拦截和用户授权
a、配置需要受保护的资源,以及访问该资源需要的权限
b、配置用户:用户名、密码、该用户所具有的权限
<!-- 配置需要保护的资源,以及访问该资源所需要的权限 -->
<security:http auto-config="true">
<security:intercept-url pattern="/user.jsp" access="ROLE_USER"/>
<security:intercept-url pattern="/admin.jsp" access="ROLE_ADMIN"/>
<!-- 配置登出的请求链接 -->
<security:logout logout-url="/security-logout"/>
</security:http>
<!-- 配置用户:用户名、密码和权限 -->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN"/>
<security:user name="user" password="user" authorities="ROLE_USER"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
有了上面的配置后,在我们首次访问/user.jsp或者admin.jsp时,会弹出Spring Security为我们提供的登录页面,我们使用配置的用户登录就可以访问该用户能访问的资源了。
5、登出:Spring Security为我们提供了登出功能,即配置中的<security:logout logout-url="/security-logout">,这里的logout-url属性的值可以任意,不需要我们手写登出逻辑,在我们需要登出的地方直接访问该处配置的logout-url即可
<a href="security-logout">Logout</a>
登出的效果(由Security框架提供):有时会重定向到访问根目录/请求
配置解释:
①auto-cnotallow=“true”:开启自动配置,开启后会自动配置好典型 web 应用程序所需的基本安全服务,包括
1️⃣基于表单的登录服务:为用户提供了用于登入应用程序的逻辑,包含登录表单的页面
2️⃣登出服务:提供了让用户能够登出应用程序的处理程序,这个程序被映射为配置的 URL(即logout-url的值)
3️⃣匿名登录:为匿名用户分配一个安全主题并授权,这样可以像普通用户一样来处理匿名用户
4️⃣“记住我”功能:能在多个浏览器会话中记住用户的身份,通常是在用户的浏览器中保存 Cookie
5️⃣Servlet API 集成:可以通过类似HttpServletRequest.isUserInRole() 和 HttpServletRequest.getUserPrincipal() 这样的标准 API 在 web 应用程序中访问安全信息
②<security:http> 配置元素中,可以通过一个或多个 <security:intercept-url> 元素来限制对特定 URL 的访问,每个 <security:intercept-url> 元素指定了 URL 模式以及访问这些 URL 所必须的访问属性,access 属性指定需要的权限, 这些权限标示符都是以 ROLE_ 开头的,这与Spring Security中的 Voter 机制有着直接的联系,只有包含了特定前缀的字符串才会被Spring Security处理。在实际应用中,URL 后通常要带上一个统配符(*),否则这个 URL 模式无法匹配带有参数的 URL。Spring Security采用的是一种就近原则,当用户访问的url资源满足多个intercepter-url时,系统将使用第一个符合条件的intercept-url进行权限控制
③在 <security:authentication-provider> 元素中配置身份验证服务:
Spring Security 支持多种用户身份验证方式,包括根据数据库进行验证或直接在 <security:user-service>中定义用户信息,authorities属性定义了当前用户登陆之后将会拥有的权限,用户可以同时拥有多个权限
三、自定义登入页面
Spring Security为我们提供了登入页面,但是过于简单,我们有时可能需要自定义登入页面。步骤如下:
1、在applicationContext.xml中配置Spring Security时配置自定义登录页面的相关信息,在<security:form-login>中配置
<!-- 配置需要保护的资源,以及访问该资源所需要的权限 -->
<security:http auto-config="true">
<security:intercept-url pattern="/user.jsp" access="ROLE_USER"/>
<security:intercept-url pattern="/admin.jsp" access="ROLE_ADMIN"/>
<!-- 配置登录页面:表示配置的是根目录下的login.jsp为登录页面,配置之后可以使用自定义的登录页面 -->
<security:form-login login-page="/login.jsp"
login-processing-url="/security-login"
username-parameter="username"
password-parameter="password"/>
<!-- 配置登出 -->
<security:logout logout-url="/security-logout"/>
</security:http>
2、编写登录页面:登录页面的编写并不是随意的,而要和配置的信息保持一致,登录页面form的action的值要和login-processing-url的值一致,登录form表单登录名和密码框中的name值要和配置信息中username-parameter和password-parameter的值保持一致
<form action="security-login" method="POST">
USER:<input type="text" name="username"/>
<br/><br/>
PASS:<input type="password" name="password"/>
<br/><br/>
<input type="submit" value="SUBMIT"/>
</form>
3、登录的其他配置:
<security:form-login login-page="/login.jsp"
login-processing-url="/security-login"
username-parameter="username"
password-parameter="password"
default-target-url="/list.jsp"
always-use-default-target="true"
authentication-success-handler-ref="authenticationSuccessHandler"
/>
default-target-url:配置登录成功后的响应页面,但若登录之前访问的是受保护的资源,则登录成功后响应的还是之前访问的资源
always-use-default-target:登录成功时总是响应default-target-url中的配置(默认情况下响应的是之前访问的受保护的目标资源,有了该配置之后登录之后总是响应list.jsp页面,再次访问受保护的资源时不必登录可以直接访问)
authentication-success-handler-ref:该配置是说在登录成功之后会执行处理器类authenticationSuccessHandler的onAuthenticationSuccess方法,有了该配置后会忽略default-target-url和always-use-default-target这两个配置,在该配置之前必须先配置authenticationSuccessHandler这个bean,该bean必须实现接口AuthenticationSuccessHandler:在重写onAuthenticationSuccess()方法的时候可以指定操作完成后转发的页面
<bean id="authenticationSuccessHandler" class="com.bdm.security.MyAuthenticationSuccessHandler"/>
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest arg0, HttpServletResponse arg1, Authentication arg2)
throws IOException, ServletException {
System.out.println("AuthenticationSuccessHandler");
//转发到哪个请求
arg0.getRequestDispatcher("/list.jsp").forward(arg0, arg1);
}
}
同样登出页面也有一些额外的配置信息。
四、自定义权限认证
上面配置的用户权限认证管理器中的用户和权限是通过<security:user>标签写死的,在实际的开发中肯定不能这么干,实际开发时我们可以自定义一个UserDetailsService接口的实现类,在该类实现的loadUserByUsername()方法中从数据库查询登录用户的密码、权限等信息,UserDetailsService接口是Spring Security定义的接口,主要是实现其中的loadUserByUsername()方法,并返回一个UserDetails对象:返回的UserDetails中封装了用户的用户名、密码、权限等信息,用于校验和授权等
public class MyUserDetailsService implements UserDetailsService {
@Override
@SuppressWarnings("deprecation")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.据 username从数据库中查询用户信息
System.out.println("从数据表中获取 [" + username + "] 对应的记录");
// 2.若没有对应的记录, 则抛出 UsernameNotFoundException 异常
if ("Tom".equals(username)) {
throw new UsernameNotFoundException(username);
}
// 3.若存在, 则创建 User 对象并返回
// 以下属性都是从数据表中获取的
// 密码
String password = null;
if ("admin".equals(username)) {
// 用户admin的密码123456经md5盐值加密之后的密码
password = "b594510740d2ac4261c1b2fe87850d08";
}
if ("user".equals(username)) {
// 用户user的密码123456经md5盐值加密之后的密码
password = "e14576586777603bd62a8ade7d10661a";
}
// 该用户是否可用
boolean enabled = true;
// 该账号是否没有过期
boolean accountNonExpired = true;
// 该账号对应的凭证是否过期
boolean credentialsNonExpired = true;
// 账号是否没有被锁定
boolean accountNonLocked = true;
// 该用户具备的权限: 集合类型, 其中是 GrantedAuthorityImpl 类型的对象
// 该类的构造器中需要传入权限的名字
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new GrantedAuthorityImpl("ROLE_USER"));
if ("admin".equals(username)) {
authorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
}
User user = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
authorities);
return user;
}
}
有了上述的接口后,每次有用户通过Spring Security的登录表单登录时都会调用上述的方法根据用户名查询用户的密码、权限等信息,但需要让Spring Security知道是哪个类,因此还需要做如下配置:
<!-- 认证管理器配置用户:用户名、密码和权限 -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userDetailsService">
<security:password-encoder hash="md5">
<!-- 盐值的user-property属性的值是UserDetails类的一个属性 -->
<security:salt-source user-property="username" />
</security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<bean id="userDetailsService" class="com.bdm.security.MyUserDetailsService"/>
配置说明
:
1️⃣在<security:authentication-provider>标签中指定了UserDetailsService的实现类,告诉Spring Security 调用哪个类进行用户信息的查询
2️⃣<security:password-encoder>标签的hash属性指定密码的加密算法,<security:salt-source>节点的user-property指定加密的盐值(加盐值的目的是即使两个用户的密码相同时在加密之后的密码也不同,这样更安全),user-property属性的值必须是UserDetails类的一个属性,一般用username
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
3️⃣通过Md5盐值加密之后的密码是无法倒推出原始密码的,这也是为什么现在的密码丢失后无法找回而必须重置的原因
4️⃣在填写用户注册信息的时候,也必须采用同样的方式对用户输入的密码进行加密,即使用的加密算法要相同,加密时的盐值也要是同一个属性,否则无法进行密码比对,下面是使用Md5PasswordEncoder加密的方式:Md5PasswordEncoder是Spring Security提供的一个加密工具
public static void main(String[]args){
Md5PasswordEncoder passwordEncoder = new Md5PasswordEncoder();
String result = passwordEncoder.encodePassword("123456", "user");
System.out.println(result);
}
用户认证和访问控制的流程大体如下:
①用户注册,注册时将填写的密码使用Md5PasswordEncoder(也可以是其他算法,但要保证和Spring Security使用相同的算法和盐值)进行加密,防止密码泄露,并将加密后的密码串保存至数据库(这样即使别人看到了数据库中保存的密码也无法推测出原始密码,比较安全),同时保存的还有权限等信息(权限也有可能是后续添加的)
②在用户登录时输入的密码是加密之前的密码,Spring Security会将该密码再次使用相同的加密算法和相同的盐值进行加密,得到加密后的密码串,并将加密后的密码串和从数据库中查询出的密码串进行比对,以确认输入的密码是否正确,若校验通过则再进行授权等操作
五、自定义受保护的资源和对应的访问权限
上面采用配置的方式配置了受保护的资源,将每一个受保护的资源和访问所需的权限都在<security:intercept-url>标签中一一列举了出来,虽然我们可以采用通配的方式来配置,但实际开发中也很少会这么做,我们可以自定义一个类来完成这个操作。
1、创建类 FilterInvocationSecurityMetadataSourceMapBuilder,在该类中定义一个buildRequestMap()方法返回一个LinkedHashMap,在该Map中存放受保护的资源和访问该资源需要的权限
public class FilterInvocationSecurityMetadataSourceMapBuilder {
public LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> buildRequestMap() {
LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();
AntPathRequestMatcher key = null;
Collection<ConfigAttribute> val = null;
// 可以通过查询数据库的方式来初始化资源 和资源对应的权限
key = new AntPathRequestMatcher("/admin.jsp");
val = new ArrayList<>();
val.add(new SecurityConfig("ROLE_ADMIN"));
requestMap.put(key, val);
key = new AntPathRequestMatcher("/user.jsp");
val = new ArrayList<>();
val.add(new SecurityConfig("ROLE_USER"));
requestMap.put(key, val);
return requestMap;
}
}
2、配置自定义的资源和权限工厂类:将Spring Security默认的构造器的入参改为我们自定义的工厂方法的返回值
<bean id="filterInvocationSecurityMetadataSourceMapBuilder" class="com.bdm.security.FilterInvocationSecurityMetadataSourceMapBuilder" />
<bean id="securityMetadataSource" class="org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource">
<constructor-arg>
<bean factory-bean="filterInvocationSecurityMetadataSourceMapBuilder"
factory-method="buildRequestMap"></bean>
</constructor-arg>
</bean>
3、自定义一个Bean的后置处理器:由于Spring管理的Bean都是单例的,即这个自定义的Bean的后置处理器也是单例的,因此才可以通过这种方式为FilterSecurityInterceptor设置SecurityMetadataSource为我们自定义的
public class MyBeanPostProcessor implements BeanPostProcessor {
private FilterSecurityInterceptor filterSecurityInterceptor;
private DefaultFilterInvocationSecurityMetadataSource metadataSource;
private boolean isSetter = false;
/**
* 该方法在每个 bean 调用 init-method 之后都会被调用
* arg0: bean 的实例
* arg1: bean 在 IOC容器中的id
*/
@Override
public Object postProcessAfterInitialization(Object arg0, String arg1) throws BeansException {
if (arg0 instanceof FilterSecurityInterceptor) {
this.filterSecurityInterceptor = (FilterSecurityInterceptor) arg0;
}
if (arg1.equals("securityMetadataSource")) {
this.metadataSource = (DefaultFilterInvocationSecurityMetadataSource) arg0;
}
if (this.filterSecurityInterceptor != null && this.metadataSource != null && !isSetter) {
this.filterSecurityInterceptor.setSecurityMetadataSource(metadataSource);
isSetter = true;
}
return arg0;
}
@Override
public Object postProcessBeforeInitialization(Object arg0, String arg1) throws BeansException {
return arg0;
}
}
4、在容器中配置自定义的Bean后置处理器
<!-- 配置后置处理器 -->
<bean class="com.bdm.security.MyBeanPostProcessor" />
六、在页面上使用Spring Security的标签库
1、在jsp页面中导入Spring Security的标签库
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
在SpringSecurity的源码包中将类ExpressionEvaluationUtils拷贝到新建的包org.springframework.web.util中,并注释掉如下代码:跟版本有关
/*
if (sc.getMajorVersion() >= 3) {
// We're on a Servlet 3.0+ container: Let's check what the application declares...
if (sc.getEffectiveMajorVersion() > 2 || sc.getEffectiveMinorVersion() > 3) {
// Application declares Servlet 2.4+ in its web.xml: JSP 2.0 expressions active.
// Skip our own expression support in order to prevent double evaluation.
return false;
}
}
*/
2、对资源添加保护:隐藏当前权限访问不到的资源
<security:authorize ifAllGranted="ROLE_ADMIN">
<h4>List Jsp</h4>
<a href="admin.jsp">To Admin Page</a>
<br/><br/>
</security:authorize>
<security:authorize ifAllGranted="ROLE_USER">
<a href="user.jsp">To User Page</a>
<br/><br/>
</security:authorize>
使用<security:authorize>标签封装被保护的资源,ifAllGranted属性表示只有当授权了该属性中所有的权限时才会显示该资源,否则该资源的超链接会被隐藏。另外还有其他的属性如ifAnyGranted属性表示只要授权其中的一个权限即可显示该资源的超链接,ifNotGranted属性表示没有授权某个属性时就会显示资源的超链接
3、显示用户登录的欢迎信息:这样会显示登录者的姓名信息,此处的principal就是UserDetailsService类中loadUserByUsername()方法的返回值对应的User对象
WELCOME:<security:authentication property="principal.username"></security:authentication>