Spring security的过滤器链中有许多过滤器,我们主要熟悉:
ChannelProcessingFilter,HttpSessioncontextIntegrationFilter,LogoutFilter,AuthenricationProcessingFilter,
RememberMeProcessingFilter,FilterSecurityInterceptor
这些Filter并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给认证管理器(AuthenticationManager)和决策管理器(DecisionManager)。其实这两个管理也不做事,认证管理器把任务交给了Provider,而决策管理器则把任务交给了Voter。而DaoAuthenticationProvider也是不直接操作数据库的,它把任务委托给了UserDetailService。
Spring Security作为权限管理框架,其内部机制可分为两大部分:
认证授权(authorization)是指,根据用户体统的身份凭证,生成权限实体,并为之授予相应的权限。
权限校验authentication是指,用户请求访问被保护资源时,将被保护资源所需的权限和用户权限实体所用户的权限二者进行对比,如果校验通过则用户可以访问被保护资源,否则拒绝访问。
1使用spring-security-2.0.4的HelloWorld
spring-security-2.0.4的下载地址:http://www.springsource.com/products/spring-community-download
拷贝spring-security-core-2.0.4.jar到web应用项目的lib目录中。
首先我们在web.xml添加过滤器和配置Spring的容器:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/conf/spring/applicationContext*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<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配置文件applicationContext-security.xml中,下面是applicationContext-security.xml的内容,它的头部与其他Spring配置文件的头部不同,这样是为了省略书写security命名空间元素的前缀:
<?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-2.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
<http auto-config='true' access-denied-page="/accessdeny.jsp">
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
</http>
<authentication-provider>
<user-service>
<user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="user" password="user" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</beans:beans>
在applicationContext-security.xml文件中,首先声明使用Spring Security提供的命名空间,然后直接在里面书写配置。
<http>元素配置如何拦截用户请求。auto-config='true'的默认配置相当于:
<http>
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login />
<anonymous />
<http-basic />
<logout />
<remember-me />
</http>
access-denied-page属性:访问拒绝时转向的页面。上例中当访问某个页面权限不足被拒绝时跳转到/accessdeny.jsp上。
<intercept-url>用来判断用户需要具有何种权限才能访问对应的url资源,可以在pattern中指定一个特定的url资源,也可以使用通配符指定一组类似的url资源。例子中定义的两个intercepter-url,第一个用来控制对/admin.jsp的访问,第二个使用了通配符/**,说明它将控
制对系统中所有url资源的访问。("**"来替代任意数目的目录,"*"字符匹配零个或多个字符,?只匹配一个字符)
在实际使用中,Spring Security采用的是一种就近原则,就是说当用户访问的url资源满足多个intercepter-url时,系统将使用第一个符合条件的intercept-url进行权限控制。
access的值一般是一个逗号分隔的角色队列。
user-service中定义了两个用户,admin和user。为了简便起见,我们使用明文定义了两个用户对应的密码,这只是为了当前演示的方便,之后的例子中我们会使用Spring Security提供的加密方式,避免用户密码被他人窃取。
authorities,这里定义了这个用户登录之后将会拥有的权限,它与上面intercept-url中定义的权限内容一一对应。每个用户可以同时拥有多个权限,多个权限使用逗号分隔。
上面我们完成了一个简单的Spring security的配置,最后我们在项目中添加两个文件/admin.jsp、/index.jsp,这当我们在浏览器中访问index.jsp时需要ROLE_USER权限,访问admin.jsp则需要ROLE_ADMIN权限。
2使用数据库管理用户权限
为了从数据库中获取用户权限信息,我们所需要的仅仅是修改上面配置文件的authentication-provider部分。将user-service替换为jdbc-user-service。如:
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>
可以看到我们还需要配置一个dataSource我们可以使用其他配置文件配置的c3p0 dataSource,也可以自己在applicationContext-security.xml中再添加一个datSource,如:
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<beans:property name="url" value="jdbc:oracle:thin:@10.0.0.12:1521:dgzz"/>
<beans:property name="username" value="dgzz03"/>
<beans:property name="password" value="dgzz03"/>
</beans:bean>
数据库表结构:
Spring Security默认情况下需要两张表,用户表和角色表,用户角色连接表。
用户表(user)包含的至少包含的字段:id,username,password,status,descn
角色表(role)包含的至少包含的字段:id,name,descn
用户角色连接表(user_role)至少包含的字段:user_id,role_id
Spring Security所需要的数据只是为了处理两种情况,一是判断登录用户是否合法,二是判断登陆的用户是否有权限访问受保护的系统资源。
处理用户登录:
select username,password,status as enabled from user where username=?
检验用户权限:
用户登陆之后,系统需要获得该用户的所有权限,根据用户已被赋予的权限来判断哪些系统资源可以被用户访问。以下SQL就可以获得当前用户所拥有的权限:
select u.username,r.roletype as authority
from dgzz_user_info u
join dgzz_user_role_rlt_inf ur
on u.userid=ur.userid
join dgzz_role_info r
on r.roleid=ur.roleid
where u.username=?
我们将前面定义的<authentication-provider>元素注释掉,使用下面的内容
将这两条SQL语句配置到xml中,就可以让Spring Security从我们自定义的表结构中提取数据了,最终配置如下:
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="select username,passwd,status as enabled from dgzz_user_info where username=?"
authorities-by-username-query="select u.username,r.roletype as authority
from dgzz_user_info u
join dgzz_user_role_rlt_inf ur
on u.userid=ur.userid
join dgzz_role_info r
on r.roleid=ur.roleid
where u.username=?"
/>
</authentication-provider>
<authentication-provider>元素是配置DaoAuthenticationProvider的简化形式。
dataSource,指向数据源的配置。
users-by-username-query,为根据用户名查找用户,系统通过传入的用户名查询当前用户的登录名,密码和是否被禁用这一状态。
authorities-by-username-query,为根据用户名查找权限,系统通过传入的用户名查询当前用户已被授予的所有权限。这些配置会调用JdbcDaoImpl.java。
在JdbcDaoImpl.java的源代码中,会使用ResultSet rs=statement.executeQuery("...")来执行上面配置的语句并获得返回的结果。以下是其源代码中的内容:
String username = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3)
String roleName = rolePrefix + rs.getString(2);
可以看出上面的查询语句应该和其对应,查询语句中username,passwd,status需要按照从左到右的顺序放置,roleName对应的位置是2,所以username,authority也是从左到右的顺序放置。注意如果enabled对应字段的值为0,则getBoolean会返回false,大于0返回true。
自定义登录页面
自己实现一个/login.jsp放在web项目的根目录下。修改applicationContext-security.xml文件,在其中的http标签中添加一个form-login标签:
<http auto-config='true' access-denied-page="/error.jsp">
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/user.jsp" access="ROLE_USER" />
<intercept-url pattern="/login.jsp" filters="none" />
<intercept-url pattern="/index.jsp" filters="none" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.jsp" login-processing-url="/j_security_check" default-target-url="/index.jsp" authentication-failure-url="/login.jsp?login_error=1" always-use-default-target="true"/>
<logout logout-url="/j_security_logout" logout-success-url="/index.jsp" invalidate-session="true"/>
</http>
filters="none"是为了让没登陆的用户也可以访问login.jsp,这是因为配置文件中的"/**"配置,要求用户访问任意一个系统资源时,必须拥有ROLE_USER角色,/login.jsp也不例外,如果我们不为/login.jsp单独配置访问权限,会造成用户连登陆的权限都没有,这是不正确的。
login-page,表示用户登陆时显示我们自定义的login.jsp
login-processing-url,指定login.jsp中的form提交的地址。默认是/j_spring_security_check,这里我们修改为/j_security_check。
authentication-failure-url,表示用户登陆失败时,跳转到哪个页面。当用户输入的登录名和密码不正确时,系统将再次跳转到/login.jsp,并添加一个login_error=1参数作为登陆失败的标识。
default-target-url,表示登陆成功时,跳转到哪个页面。
always-use-default-target,指定了是否(true/false)在身份验证通过后总是跳转到default-target-url属性指定的URL,如果为false则当访问某个权限下的页面,登录成功后跳转到之前要访问的页面而不是总是跳转到default-target-url。
logout-url:指定了用于响应退出系统请求的URL。其默认值为:/j_spring_security_logout。这里修改为/j_security_logout。那么我们可通过一个链接来退出登录:
<a href="${pageContext.request.contextPath}/j_security_logout">退出登录</a>
logout-success-url:退出系统后转向的URL。
invalidate-session:指定在退出系统时是否要销毁Session(true/false)。
登录页面中的参数配置
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<form action="${pageContext.request.contextPath}/j_security_check" method="POST">
用户名:<input type="text" name="j_username" /> <br/>
密码:<input type="password" name="j_password" /><br/>
<input type="checkbox" name="_spring_security_remember_me"/>两周之内不用输入密码<br/>
<input name="submit" type="submit" value="登 录">
<input name="reset" type="reset" value="重 置">
</form>
<br/>
<c:if test="${param['login_error']==1}">
登录失败
</c:if>
</body>
</html>
/j_security_check,提交登录信息的URL地址,与<http>元素的login-processing-url的值保持一致。这里用的是绝对路径,避免登录页面存放的页面可以能带来的问题。
j_username,输入登陆名的参数名称。
j_password,输入密码的参数名称。
_spring_security_remember_me,选择是否允许自动登录的参数名称。可以直接把这个参数设置成一个checkbox,无需设置value,Spring security会自行判断它是否被选中。
3使用数据库管理资源
国内对权限系统的基本要求是将用户权限和被保护的资源放在数据库里进行管理。
数据库表的结构
使用五张表:用户表(user),角色表(role),用户角色连接表(user_role)同上
resc资源表(resc)包含的字段:id,name,res_type,res_string,descn
资源角色连接表(resc_role)包含的字段:resc_id,role_id
4获取当前用户信息
如果只想从页面上显示当前登陆的用户名,可以直接使用Spring Security提供的taglib。
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<div>username : <sec:authentication property="name"/></div>
因为这里引入了标签,所以需要导入spring-security-taglibs-2.0.4.jar,否则会报错。
如果想在程序中获得当前登录用户对应的对象:
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
如果想获得当前登陆用户所拥有的所有权限:
GrantedAuthority[] authorities = userDetails.getAuthorities();
5Spring Security中的过滤器
Spring Security一启动就会包含一批负责各种安全管理的过滤器。如:
HttpSessionContextIntegrationFilter
LogoutFilter
AuthenticationProcessingFilter
DefaultLoginpageGeneratingFilter
BaseProcessingFilter
SecurityContextHolderAwareRequestFilter
RememberMeProcessingFilter
AnonymousProcessingFilter
ExceptionTranslationFilter
SessionFixationProtectionFilter
FilterSecurityInterceptor
HttpSessionContextIntegrationFilter
位于过滤器顶端,第一个被启用的过滤器。在执行其他过滤器之前,率先判断用户的session中是否已经存在一个SecurityContext了。如果存在,就把SecurityContext拿出来,放到SecurityContextHolder中,供Spring Security的其他部分使用。如果不存在,就创建一个SecurityContext出来,还是放到SecurityContextHolder中,供Spring Security的其他部分使用。
另外在所有过滤器执行完毕后,改过滤器清空SecurityContextHolder,因为SecurityContextHolder是基于ThreadLocal的,如果在操作完成后清空ThreadLocal,会受到服务器的线程池机制的影响。
LogoutFilter
只处理注销请求,默认为/j_Spring_security_logout
用途是在用户发送注销请求时,销毁用户session,清空SecurityContextHolder,然后重定向到注销成功页面。可以与rememberMe之类的机制结合,在注销的同时清空用户cookie。
AuthenticationProcessingFilter
处理form登录的过滤器,与form登录有关的所有操作都是在此进行的。默认情况下只处理/j_spring_security_check请求,这个请求应该是用户使用form登陆后的提交地址。此过滤器执行的基本操作时,通过用户名和密码判断用户是否有效,如果登录成功跳转到成功页面(可能是将要访问的页面,也可能是默认的成功页面),如果登录失败,就跳转到失败页面。
DefaultLoginPageGeneratingFilter
此过滤器用来生成一个默认的登录页面,默认的访问地址为/spring_security_login,这个默认的登录页面虽然支持用户输入用户名,密码,也支持rememberMe功能,但是界面太简单,不可能直接用在实际项目中。
BasicProcessingFilter
此过滤器用于进行basic验证,功能与AuthenticationProcessingFilter类似,只是验证的方式不同。
SecurityContextHolderAwareRequestFilter
此过滤器用来包装客户的请求。目的是在原始请求的基础上,为后续程序提供一些额外的数据。比如getRemoteUser()时直接返回当前登陆的用户名之类的。
RememberMeProcessingFilter
此过滤器实现RememberMe功能,当用户cookie中存在rememberMe的标记,此过滤器会根据标记自动实现用户登陆,并创建SecurityContext,授予对应的权限。
AnonymousProcessingFilter
为了保证操作统一性,当用户没有登陆时,默认为用户分配匿名用户的权限。
ExceptionTranslationFilter
此过滤器的作用是处理中FilterSecurityInterceptor抛出的异常,然后将请求重定向到对应页面,或返回对应的响应错误代码。
SessionFixationProtectionFilter
防御会话伪造攻击。
FilterSecurityInterceptor
用户的权限控制都包含在这个过滤器中。
如果用户尚未登陆,则抛出AuthenticationCredentialsNotFoundException“尚未认证异常”。
如果用户已登录,但是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。
如果用户已登录,也具有访问当前资源的权限,则放行。
6管理会话
多个用户不能使用同一个账号同时登陆系统
添加监听器
在web.xml中添加一个监听器,这个监听器会在session创建和销毁的时候通知Spring Security。
<listener>
<listener-class>org.springframework.security.ui.session.HttpSessionEventPublisher</listener-class>
</listener>
这种监听session生命周期的监听器主要用来收集在线用户的信息,比如统计在线用户数之类的事。
添加过滤器
在xml中添加控制同步session的过滤器:
<http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<concurrent-session-control/>
</http>
因为Spring Security的作者不认为控制会话是一个大家都经常使用的功能,所以concurrent-sessioncontrol没有包含在默认生成的过滤器链中,在我们需要使用它的时候,需要自己把它添加到http元素中。
这个concurrent-session-control对应的过滤器类是org.springframework.security.concurrent.ConcurrentSessionFilter,它的排序代码是100,它会被放在过滤器链的最顶端,在所有过滤器使用之前起作用。
控制策略
默认情况下,后登陆的用户会把先登录的用户踢出系统。
后面的用户禁止登录
如果不想让之前登录的用户被自动踢出系统,需要为concurrent-session-control设置一个参数:
<http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<concurrent-session-control max-sessions="1" exception-if-maximum-exceeded="true"/>
</http>
exception-if-maximum-exceeded属性:这个参数用来空值是否在会话数目超过最大限制时抛出异常,默认值是false,也就是不抛出异常,而是把之前的session销毁掉,所以之前登录的该用户就会被踢出系统了。现在我们把这个参数改为true,再使用同一个账号同时登录系统,就不能登录了。但是这样也会出现某些问题比如有人登录了系统却没有logout就退出了系统,那么它只能等到session过期自动销毁之后,才能再次登录系统。
max-sessions属性:允许用户帐号登录的次数。
7Spring Security与CAS
首先我们配置好CAS Server,并在CAS Server上面启用SSL,该CAS Server的登录地址为https://localhost:8443/cas/login或http://localhost:8080/cas/login
我们再在自己的项目(项目名mySpring)中配置Spring Security并修改上面的applicationContext-security.xml,让它通过CAS服务器进行登录。
首先要添加CAS的插件和CAS客户端的依赖库:
aopalliance-1.0.jar
spring-security-cas-client-2.0.4.jar
cas-client-core-3.1.10.jar
修改applicationContext-security.xml
先添加一个entry-point-ref引用CAS提供的casProcessingFilterEntryPoint,这样在验证用户登录时就用上CAS提供的机制了。
<http auto-config='true' entry-point-ref="casProcessingFilterEntryPoint">
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/index.jsp" access="ROLE_USER" />
<intercept-url pattern="/" access="ROLE_USER" />
<logout logout-success-url="/cas-logout.jsp"/>
</http>
再修改注销页面,将注销请求转发给CAS处理,当我们点击Logout of CAS链接就可以进行注销。
<a href="https://localhost:8443/cas/logout">Logout of CAS</a>
然后提供userService和authenticationManager,二者会被注入到CAS的类中用来进行登录之后的用户授权。
<authentication-provider>
<user-service id="userService">
<user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="user" password="user" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
<authentication-manager alias="authenticationManager"/>
我们将用户信息直接写在了配置文件中,之后cas的类就可以通过id获得userService,以此获得其中定义的用户信息和对应的权限对于authenticationManager来说,我们没有创建一个新实例,而是使用了"别名"(alias),这是因为在之前的namespace配置时已经自动生成了authenticationManager的实例,cas只需要知道这个实例的别名就可以直接调用。
在applicationContext-security.xml中创建cas的filter, entryPoint, serviceProperties和authenticationProvider。
<beans:bean id="casProcessingFilter" class="org.springframework.security.ui.cas.CasProcessingFilter">
<custom-filter after="CAS_PROCESSING_FILTER"/>
<beans:property name="authenticationManager" ref="authenticationManager"/>
<!--登录失败后的页面-->
<beans:property name="authenticationFailureUrl" value="/casfailed.jsp" />
<beans:property name="defaultTargetUrl" value="/" />
</beans:bean>
<beans:bean id="casProcessingFilterEntryPoint"
class="org.springframework.security.ui.cas.CasProcessingFilterEntryPoint">
<!-- 指定到CAS Server那边的登录页 -->
<beans:property name="loginUrl" value="https://localhost:8443/cas/login" />
<beans:property name="serviceProperties" ref="casServiceProperties" />
</beans:bean>
<beans:bean id="casServiceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
<!-- 如果验证成功将重定向到我们请求的路径(在mySpring项目中),j_spring_cas_security_check是有Spring security指定的-->
<beans:property name="service" value="http://localhost:8088/mySpring/j_spring_cas_security_check"/>
<beans:property name="sendRenew" value="false"/>
</beans:bean>
<!--进行CAS认证-->
<beans:bean id="casAuthenticationProvider"
class="org.springframework.security.providers.cas.CasAuthenticationProvider">
<custom-authentication-provider />
<!-- 表示可以使用前面配置的userService中的用户进行认证 -->
<beans:property name="userDetailsService" ref="userService" />
<beans:property name="serviceProperties" ref="casServiceProperties" />
<beans:property name="ticketValidator">
<beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<beans:constructor-arg index="0" value="https://localhost:8443/cas" />
</beans:bean>
</beans:property>
<!-- 指定Sprint security的项目名 -->
<beans:property name="key" value="mySpring" />
</beans:bean>
casProcessingFilter最终要放到Spring Security的安全过滤器链中才能发挥作用。这里使用的customer-filter就会把它放到CAS_PROCESSING_FILTER位置的后面。这个位置具体是在LogoutFilter和AuthenticationProcessingFilter之间,这样既不会影响注销操作,又可以在用户进行表单登陆之前拦截用户请求进行cas认证了。
当用户尚未登录时,会跳转到这个cas的登录页面进行登录。
用户在cas登录成功后,再次跳转回原系统时请求的页面。CasProcessingFilter处理这个请求,从cas获得已登录的用户信息,并对用户进行授权。
使用custom-authentication-provider之后,Spring Security其他的权限模块会从这个bean中获得权限验证信息。
Cas20ServiceTicketValidator作用是系统需要验证当前用户的tickets是否有效。
最后我们启动CAS Server和Spring Security所在的web项目mySpring,当我们访问
http://localhost:8088/mySpring/index.jsp
由于我们前面配置了<intercept-url>元素,则访问index.jsp需要ROLE_USER的权限
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/index.jsp" access="ROLE_USER" />
那么就会被重定向到我们配置的https://localhost:8443/cas/login上去进行认证,认证完成casAuthenticationProvider判断权限验证是否符合ROLE_USER,如果符合就会重定向我们之前请求的页面http://localhost:8088/mySpring/index.jsp上去。
下面是admin.jsp页面,用来测试得到登录到CAS Server的用户名和向CAS Server注销的链接:
<%@ page language="java" pageEncoding="UTF-8"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
<title>my CAS</title>
</head>
<body>
<div>username : <sec:authentication property="name"/></div>
<a href="https://localhost:8443/cas/logout">Logout(注销) of CAS </a>
</body>
</html>
当我们点击"注销",就会向CAS Server注销当前登录的用户并跳转到CAS Server注销后的页面。
8Spring Security标签库
Spring Security提供的标签库,主要的用途是为了在视图层直接访问用户信息,再者就是为了对显示的内容进行权限管理。
配置taglib
如果需要使用taglib,首先需要把spring-security-taglibs-2.0.4.jar放到项目的classpath下,剩下的只要在jsp上添加taglib的定义就可用使用标签库了。
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
authentication
authentication的功能是从SecurityContext中获取一些权限相关的信息。
获得当前登录的用户名:<sec:authentication property="name"/>
获得当前用户所有的权限,把权限列表放到authorities变量中,然后循环输出权限信息:
<sec:authentication property="authorities" var="authorities" scope="page"/>
<c:forEach items="${authorities}" var="quanxian">
${quanxian.authority}
</c:forEach>
authorize
用来判断当前用户的权限,然后根据指定的条件判断是否显示内部的内容。
<sec:authorize ifAllGranted="ROLE_ADMIN,ROLE_USER">
admin and user
</sec:authorize>
<sec:authorize ifAnyGranted="ROLE_ADMIN,ROLE_USER">
admin or user
</sec:authorize>
<sec:authorize ifNotGranted="ROLE_ADMIN">
not admin
</sec:authorize>
ifAllGranted,只有当前用户同时拥有ROLE_ADMIN和ROLE_USER两个权限时,才能显示标签内部内容。
ifAnyGranted,如果当前用户拥有ROLE_ADMIN或ROLE_USER其中一个权限时,就能显示标签内部内容。
ifNotGranted,如果当前用户没有ROLE_ADMIN时,才能显示标签内部内容。
acl/accesscontrollist
用于判断当前用户是否拥有指定的acl权限。
<sec:accesscontrollist domainObject="${item}" hasPermission="8,16">
<a href="message.do?action=remove&id=${item.id}">Remove</a>
</sec:accesscontrollist>
我们将当前显示的对象作为参数传入acl标签,然后指定判断的权限为8(删除)和16(管理),当前用户如果拥有对这个对象的删除和管理权限时,就会显示对应的remove超链接,用户才可以通过此
链接对这条记录进行删除操作。
为不同用户显示各自的登陆成功页面
普通用户登录之后显示普通用户的工作台,管理员登录之后显示后台管理页面。这就可以使用taglib解决:
<sec:authorize ifAllGranted="ROLE_ADMIN">
<%response.sendRedirect("admin.jsp");%>
</sec:authorize>
<sec:authorize ifNotGranted="ROLE_ADMIN">
<%response.sendRedirect("user.jsp");%>
</sec:authorize>
9自动登录
默认策略
在配置文件使用auto-config="true"就会自动启用rememberMe,之后,只要用户在登录时选中checkbox就可以实现下次无需登录直接进入系统的功能。默认有效时间是两周。实际上,Spring Security中的rememberMe是依赖cookie实现的,当用户在登录时选择使用remeberMe,系统就会在登录成功后将为用户生成一个唯一的标识,并将这个标识保存进cookie中。当用户再次访
问系统时,Spring Security将从这个cookie读取用户信息,并加以验证。如果可以证实cookie有效,就会自动将用户登录到系统中,并为用户授予对应的权限。
持久化策略
rememberMe的默认策略会将username和过期时间保存到客户主机上的cookie中,虽然这些信息都已经进行过加密处理,不过我们还可以使用安全级别更高的持久化策略。在持久化策略中,客户主机cookie中保存的不再用username,而是由系统自动生成的序列号,在验证时系统会将客户cookie中保存的序列号与数据库中保存的序列号进行比对,以确认客户请求的有效性,之后在比对成功后才会从数据库中取出对应的客户信息,继续进行认证和授权等工作。这样即使客户本地的cookie遭到破解,攻击者也只能获得一个序列号,而不是用户的登录账号。
如果希望使用持久化策略,我们需要先在数据库中创建rememberMe所需的表:
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);
然后在配置文件中添加dataSource,最后修改http中的配置,为remember-me添加data-source-ref即可
<http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<remember-me data-source-ref="dataSource"/>
</http>
默认策略和持久化策略是不能一起用。
10匿名登录
匿名登录,即用户尚未登录系统,系统会为所有未登录的用户分配一个匿名用户,这个用户也拥有自己的权限,不过他是不能访问任何被保护资源的。
配置文件
在配置文件中使用auto-config="true"就会启用匿名登录功能,在启用匿名登录之后,在启用匿名登录之后,如果我们希望允许未登录就可用访问一些资源,就可用进行如下配置:
<http auto-config='true'>
<intercept-url pattern="/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
</http>
在access中指定IS_AUTHENTICATED_ANONYMOUSLY后,系统就知道此资源可以被匿名用户访问了。
上例中当未登录时访问系统的"/"(表示系统的根目录http://localhost:8088/mySpring),就会被自动赋以匿名用户的身份。
我们使用上面的标签库来输出匿名用户名和匿名用户具有的权限
username : <sec:authentication property="name"/>用户具有的权限:
<sec:authentication property="authorities" var="authorities" scope="page"/>
<c:forEach items="${authorities}" var="quanxian">
${quanxian.authority}
</c:forEach>
会输出:
username : roleAnonymous用户具有的权限: ROLE_ANONYMOUS
这里显示的是分配给所有未登录用户的一个默认用户名roleAnonyMous,拥有的权限是ROLE_ANONYMOUS。
实际上,我们完全可以把匿名用户像一个正常用户那样进行配置,我们可以在配置文件中直接使用ROLE_ANONYMOUS指定它可以访问的资源,如:
<intercept-url pattern="/login.jsp" access="ROLE_ANONYMOUS" />
但是为了明显,推荐还是尽量使用IS_AUTHENTICATED_ANONYMOUSLY。
修改默认用户名
有时候,当一个用户尚未登录系统时,在页面上应当显示用户名的部分显示的是"游客"。这样操作更利于提升客户体验。如果没有使用匿名登录的功能,我们就需要判断当前用户是否登录,然后根据登录情况显示登录用户名或默认的"游客"字符。如果使用了匿名登录就不必去判断用户登录状态,直接显示当前权限主体的名称即可。
匿名用户的默认名称roleAnonymouse,可以通过配置文件来修改这个名称。如:
<http auto-config='true'>
<intercept-url pattern="/" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<anonymous username="Guest"/>
</http>
这样,匿名用户的默认名称就变成了"Guest"。那么我们通过标签输出匿名用户的用户名将是Guest。
匿名用户与未登录用户之间还是有区别的,比如,我们将"/"设置为不需要过滤器过滤,而不是设置匿名用户:
<intercept-url pattern="/" filters="none" />
filters="none"表示我们访问"/"时,是不会使用任何一个过滤器取处理这个请求的,它可以实现无需登录即可访问资源的效果。但是因为没有使用过滤器对请求进行处理,这时SecurityContext内再没有保存任何一个权限主体了,我们也无法从中取得主体名称以及对应的权限信息。
11解决会话伪造
解决会话伪造问题其实很简单,只要在用户登录成功之后,销毁用户的当前session,并重新生成一个session就可以了。Spring Security默认就会启用session-fixation-protection,这会在登录时销毁用户的当前session,然后为用户创建一个新session,并将原有session中的所有属性都复制到新session中。
如果希望禁用session-fixation-protection,可以在http中将session-fixation-protection设置为none。如:
<http auto-config='true' session-fixation-protection="none">
....
</http>
session-fixation-protection的值共有三个可供选择:none,migrateSession和newSession。默认使用的是migrationSession,如同我们上面所讲的,它会将原有session中的属性都复制到新session中。默认使用的是migrationSession,如同我们上面所讲的,它会将原有session中的属性都复制到新session中。newSession会在用户登录时生成新session,但不会复制任何原有属性。none来禁用session-fixation功能的场景。
12切换用户
Spring Security提供了一种称为切换用户的机制,可以使管理免于登录的操作,直接切换当前用户,从而改变当前的操作权限。
在xml中添加SwitchUser的配置:
<beans:bean id="switchUserProcessingFilter"
class="org.springframework.security.ui.switchuser.SwitchUserProcessingFilter">
<!--使用过滤器-->
<custom-filter position="SWITCH_USER_FILTER" />
<beans:property name="userDetailsService"
ref="org.springframework.security.userdetails.memory.InMemoryDaoImpl" />
<beans:property name="targetUrl" value="/index.jsp"/>
</beans:bean>
它需要引用系统中的userDetailsService在切换用户时,根据对应的username获得切换后用户的信息和权限。
还要使用custom-filter将该过滤器放到过滤器链中,注意必须放在用来验证权限的FilterSecurityInterceptor之后,这样可以控制当前用户是否拥有切换用户的权限。
现在,我们可以在系统中使用切换用户这一功能了,我们可以通过/j_spring-security_switch_user?j_username=user切换到j_username指定的用户user,这样可以快捷的获得目标用户的信息和权限。当需要返回管理员用户时,只需要通过/j_spring_security_exit_user就可以还原到切换前的状态。
InMemoryDaoImpl是做userDetailService接口的实现,在使用时需要用户名/密码/用户是否有效/权限作为构造属性,其实上面的<user-service>中已经配置了就已经把用户相关信息加载到内存了,我们需要修改为:
<beans:property name="userDetailsService" ref="userService"/>
让其中<user-serice>的配置中取用户信息。
现在我们调整配置admin用户指定访问admin.jsp。user用户只能访问user.jsp。当我们用admin用户登录访问admin.jsp后,点击<a href="${pageContext.request.contextPath}/j_spring_security_switch_user?j_username=user">切换到user用户</a>
切换到user用户,页面默认会先跳转到/index.jsp(targetUrl属性的配置),然后我们再访问user.jsp就可以,这样我们就成功切换到user用户了。在切换用户后,我们可以看到当前登录用户的权限中多了一个ROLE_PREVIOUS_ADMINISTRATOR,这其中就保存着前用户的权限信息,当我们通过/j_spring_security_exit_user还原到切换前的状态,系统就会从ROLE_PREVIOUS_ADMINISTRATOR中获得原始用户信息,重新进行授权。如:
用户具有的权限: ROLE_USER ROLE_PREVIOUS_ADMINISTRATOR
<a href="${pageContext.request.contextPath}/j_spring_security_exit_user">还原到以前的用户</a>
当我们点击还原到以前的用户时就又还原到admin用户了。
13信道安全
为了加强安全级别,我们可以限制默认资源必须通过https协议才能访问。
<http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" requires-channel="https"/>
<intercept-url pattern="/**" access="ROLE_USER" />
</http>
可以为/admin.jsp单独设置必须使用https才能访问。这里我们可以使用https,http,any三种数值,其中any为默认值,表示无论用户使用何种协议都可以访问资源。
指定http和https的端口
因为http和https的访问端口不同,Spring Security在处理信道安全时默认会使用80/443和8080/8443对访问的网址进行转换。
如果需要对http和https协议监听的端口进行了修改,我们可以使用port-mappings自定义端口映射。
<http auto-config='true'>
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" requires-channel="https"/>
<intercept-url pattern="/**" access="ROLE_USER" />
<port-mappings>
<port-mapping http="9000" https="9443"/>
</port-mappings>
</http>
上述配置文件中,我们定义了9000与9443的映射,现在系统会在强制使用http协议的网址时使用9000作为端口号,在强制使用https协议的网址时使用9443作为端口号,这些端口号会反映在重定向后生成网址中。
14保护方法调用
Spring Security使用AOP对方法调用进行权限控制,所以我们也需要导入aop需要的jar包。
控制全局范围的方法权限
使用global-method-security和protect-point标签管理全局范围的方法权限。这里使用spring-2.0中的aop语法,对getArea中所有以get开头的方法进行权限控制,限制这些方法只能由ROLE_ADMIN调用:
<global-method-security>
<protect-pointcut
expression="execution(* action.area.getArea.get*(..))"
access="ROLE_ADMIN"/>
</global-method-security>
使用annotation控制方法权限
借助JDK5以后支持annotation,我们可以直接在代码中设置某个方法的调用权限。
使用Spring Scurity提供的Secured注解:
首先我们导入spring-security-core-tiger-2.0.4.jar包,然后修改配置文件声明使用注解来控制方法权限。
<global-method-security secured-annotations="enabled"/>
下面我们在getArea中对getNextArea方法进行权限控制,声明只有ROLE_ADMIN权限才能访问getNextArea方法。
@Secured({"ROLE_ADMIN"})
public void getNextArea(){
...
}
@Secured({"ROLE_ADMIN", "ROLE_USER"})表示只要拥有ROLE_ADMIN或ROLE_USER任意一种权限都可以调用这个方法。
15Voter表决者
身份认证只是Spring Security安全保护的第一步,一旦Spring Security弄清用户的身份后,它必须决定是否允许用户访问由它保护的资源。访问决策管理器决定用户是否拥有恰当的权限访问受保护的资源。决策管理器由AccessDecisionManager接口定义的,该接口的decide()方法是完成最终决定的地方。如果它没有抛出一个 AccessDeniedException或者是InsufficientAuthenticationException而返回,则允许访问受保护的资源。否则,访问被拒绝。
决策管理器通过征询一个或多个对某一用户是否有权访问受保护资源进行投票的对象。一旦获得所有的投票结果,决策管理器便将统计得票情况,并做出它的最终决定。
Spring Security带有AccessDecisionManager的三个实现,每一个都采用一种不同的方法来统计得票情况。
AffirmativeBased.java
只要有一个投票者投票赞成授予访问权,就允许访问。
ConsensusBased.java
只要大多数投票这投票造成授予访问权,就允许访问。
UnanimouseBased.java
只有当所有投票者都投票赞成授予访问权时,才允许访问。
在Spring配置文件中,所有的访问决策管理器都是以相同的方式进行配置。如:
<beans:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
<!-- 是否允许所有的投票者弃权,如果为false,表示如果所有的投票者弃权,就禁止访问 -->
<beans:property name="allowIfAllAbstainDecisions" value="false" />
<beans:property name="decisionVoters">
<beans:list>
<!-- RoleVoter默认角色名称都要以ROLE_开头,否则不会被计入权限控制,如果要修改前缀,可以通过对rolePrefix属性进行修改 -->
<beans:bean id="roleVoter" class="org.springframework.security.vote.RoleVoter">
<beans:property name="rolePrefix" value="AUTH_"/>
</beans:bean>
</beans:list>
</beans:property>
</beans:bean>
通过decisionVoters属性,可以为访问决策管理器提供一组投票者,一般只有一个投票者。任何实现AccessDecisionVoter及接口的对象都是一个访问决策投票者,访问决策投票者能够以下面三种方式之一进行投票:
ACCESS_GRANTED——投票者希望允许访问受保护的资源。
ACCESS_DENIED——投票者希望拒绝对受保护资源的访问。
ACCESS_ABSTAIN——投票者保持中立。
你可以自由编写自己的AccessDecision Voter的实现类,Spring Security提供一个很使用的投票者类RoleVoter,它在受保护资源由一个名称以ROLE_打头的配置属性时,RoleVoter参与投票。
RoleVoter决定其投票结果的方式是通过简单地将受保护资源的所有配置属性(以ROLE_作为前缀)与已授予认证用户的所有权限进行比较。如果RoleVoter发现其中由一个是匹配的,则它投ACCESS_GRANTED票。RoleVoter在访问所需的授权不是以ROLE_为前缀时放弃投票。
上面RoleVoter只有在受保护资源具有以ROLE_为前缀的配置属性时才进行投票。然后这个ROLE前缀是指默认值,你可以选择通过rolePrefix属性来覆盖这个默认前缀。
在默认情况下,如果所有投票者全都投弃权票,那么所有的访问决策管理者都将拒绝对资源的访问。不过,你可以通过将访问决策管理者的allowIfAllAbstainDecisions属性设置为true来覆盖这个默认行为。
16安全拦截器
每当用户请求Web应用程序中的一个页面时,那个页面可以是需要保护的,也可以是不需要保护的,在Spring Security中,一个过滤器安全拦截器负责拦截请求,判断某一请求是欧服安全,并且给予身份验证和访问决策管理一个机会来认证用户的身份和验证用户的权限。Spring 安全拦截器主要包括:AbstractSecurityInterceptor总拦截器,AfterInvocationManager后置拦截器,authenticationManager验证管理器。
********************
MD5加密
任何一个企业应用中,都不会把数据库中使用明文来保存密码。最常用的方法是使用MD5算法对密码进行加密。为了使用MD5对密码加密,我们需要修改一下配置文件:
<authentication-provider>
<password-encoder hash="md5"/>
<jdbc-user-service data-source-ref="dataSource"
..../>
</authentication-provider>
盐值加密
虽然MD5算法是不可逆的,但是它对同一字符串计算的结果是唯一的。增加盐值加密,需要修改配置文件:
<authentication-provider>
<password-encoder hash="md5">
<salt-source user-property="username"/>
</password-encoder>
</authentication-provider>
在password-encoder下添加了salt-source,并且指定使用username作为盐值。盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再使用md5对合并后的内容进行演算,这样一来,就算密码是一个很常见的字符串,再加上用户名。
上面使用的md5是Spring security提供的,我们也可以自己定义一个加密类,使用我们自定义的加密类对输入的密码加密后再与数据库的中的密文进行比较。自定义加密类必须实现PasswordEncoder接口,PasswordEncoder接口有两个方法:
encodePassword方法用于对输入的密码进行加密,返回加密后的密码。
isPasswordValid方法用于将加密后的密码与数据库中的密文进行比较,返回true/false。
修改配置文件,使用ref指定到我们定义的加密类上。
<authentication-provider>
<password-encoder ref="myEncoder"/>
<jdbc-user-service data-source-ref="dataSource"
.../>
</authentication-provider>
<!-- 自定义加密类 -->
<beans:bean id="myEncoder" class="pub.MD5">
下面我自定义的一个MD5加密类:
package pub;
/*
* he binbin
*/
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.security.providers.encoding.PasswordEncoder;
public class MD5 implements PasswordEncoder {
public String encodePassword(String rawPass, Object salt){
return encode(rawPass);
}
public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
String pass1 = "" + encPass;
String pass2 = encodePassword(rawPass, salt);
return pass1.equals(pass2);
}
//实现PasswordEncoder接口的encode方法
public String encode(String arg0) {
if (arg0 == null)
return "";
return md5Str(arg0, 0);
}
/**
* 计算消息摘要。
* @param data 计算摘要的数据。
* @param offset 数据偏移地址。
* @param length 数据长度。
* @return 摘要结果。(16字节)
*/
public static String md5Str(String str, int offset)
{
try
{
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] b = str.getBytes("UTF8");
md5.update(b, offset, b.length);
return byteArrayToHexString(md5.digest());
}
catch (NoSuchAlgorithmException ex)
{
ex.printStackTrace();
return null;
}
catch (UnsupportedEncodingException ex)
{
ex.printStackTrace();
return null;
}
}
/**
*
* @param b byte[]
* @return String
*/
public static String byteArrayToHexString(byte[] b)
{
String result = "";
for (int i = 0; i < b.length; i++)
{
result += byteToHexString(b[i]);
}
return result;
}
private static String[] hexDigits = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e",
"f" };
/**
* 将字节转换为对应的16进制明文
* @param b byte
* @return String
*/
public static String byteToHexString(byte b)
{
int n = b;
if (n < 0)
{
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
}
*********************