Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
官方文档中文翻译:http://www.mossle.com/docs/springsecurity3/html/springsecurity.html
一、基本流程
Spring Security3对(用户登录)验证和(安全资源)授权的基本流程如下图:
1、Spring Security 核心
SecurityContextHolder 、SecurityContext 、Authentication 、GrantedAuthority 、UserDetails
Authentication:代表了Spring Security中的当事人。
SecurityContext:拥有了Authentication、请求相关的信息。
SecurityContextHodler:用于获取SecurityContext。
GrantedAuthority:代表在应用程序中给当事人授予的权限。
UserDetails:用户详细信息。其实就是一个JavaBean。
UserDetailsService:UserDetails相关的业务处理。
这几个是Spring Security的核心,其它的API都是围绕这些API展开的,都是为它们服务的。
2、身份认证Authentication
2.1、一般的身份认证
通常情况下,我们的系统都是这样的:
a. 用户输入用户名、密码登录
b. 系统对用户名、密码进行验证
c. 获取用户上下文信息(角色列表等等)
d. 获取相关操作权限
对于上面说的前三条,用Spring Security来处理,就是:
a. 用户名、密码组合生成一个Authentication对象(也就是UsernamePasswordAuthenticationToken对象)。
b. 生成的这个token对象会传递给一个AuthenticationManager对象用于验证。
c. 当成功认证后,AuthenticationManager返回一个Authentication对象。
d. 接下来,就可以调用
SecurityContextHodler.getContext().setAuthentication(…)。
2.2、Web Application中如何进行身份认证呢?
a. 用户在首页上点击某个链接
b. 后台处理时,先判断是否是要访问一个受保护的资源
c. 如果是受保护的资源,判断用户是否登录,是否有这个资源的访问权限
d. 如果用户没有登录,返回一个登录页面给用户
e. 用户输入username、password,然后登录
f. 接下来开始上面的身份认证过程
在Web Application环境下,将上述1-4过程由AuthenticationEntryPoint来处理。
用户要访问另外的资源时,肯定要判断是否有该资源的访问权限,在判断是否有访问权限前,一般要用户先登录系统(要对用户的身份进行认证)的。如果用户已经成功登录,只需要判断是否有访问权即可。在一般的Web Application中(不使用Spring Security),我们通常会将用户信息存储到HttpSession中。
3、授权Authorization(Access Control)
身份认证Authentication保证了用户可访问系统。权限认证(授权保证了用户可以访问系统中的资源)。
在用户的一次资源访问中,这两个过程是不可少的。Access Control就是决定你这个请求是否被允许,它是在身份认证之后,访问资源之前进行的。
用户请求-->身份认证-->授权-->资源访问-->响应
身份认证过程由AuthenticationManager来处理,授权决定则是由AccessDecisionManager来处理的。
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
Spring在进行授权时,充分利用了Spring的核心之一:AOP。AOP在程序中一般体现为Filter或者是Interceptor,其中更多的是使用Interceptor。
上面的decide方法需要三个参数:
·Authentication就是已经通过认证的Authentication对象。上面的学习已经可以理解。
·Object obj 是代表方法调用(MethodInvocation)或者请求处理(Action的Handler),在接下来的AbstractSecurityInterceptor中说明。
·configAttributes则是相关的特性配置。下面会有说明。
AbstractSecurityInterceptor
Security中关于授权部分使用了AOP,所以就不得不了解一下AbstractSecurityInterceptor。
Interceptor一般都会提供invoke方法。这个类是抽象类,没有提供,使用时用的是它的子类,在子类中提供了invoke方法。而在AccessDecisionManager的decide方法中的第二个参数,就是invoke方法的参数MethodInvocation对象。
ConfigAttribute
其实就是对某个拦截器配置一些访问属性。举个例子:
某个拦截器interceptor,配置访问属性有ROLE_A, ROLE_B,如果一个用户经过认证后他有一个Authentication为ROLE_A,那么他的这个请求就会被拦截器interceptor处理。
再说的通俗点就是:设置某个拦截器能够处理哪些身份的用户的请求。
AbstractSecurityInterceptor的执行流程
1、查找有哪些configAttributes与当前的请求相关联。
2、提交secure object(就是前面说的MethodInvocation对象)、当前的Authentication(就是已经认证好的身份)、以及1中找到的那些configAttributes,提交给AccessDecisionManager用于授权。
3、选择性的改变用户的身份去进行验证。这是因为用户身份的多样性的需要。
4、Secure Object(methodInvocation 对象执行),也就是执行我们在action中写的Handler。
5、如果配置了AfterInvocationManager,那么AfterInvocationManager也会执行的。
这个流程是官方文档中说明的,已经很清楚了。为了更清新的了解这个过程,还是查看一下源码吧:
在AbstractSecurityInterceptor的子类MethodSecurityInterceptor中:
public Object invoke(MethodInvocation mi) throws Throwable {// 预先处理 InterceptorStatusToken token = super.beforeInvocation(mi); Object result; try {// 真实方法调用,也就是我们写的action调用 result = mi.proceed(); } finally { super.finallyInvocation(token); }// afterInvocationManager处理 return super.afterInvocation(token, result); }
这段代码与上面的流程说明对应起来,那就应该是上面说的流程中的1、2、3对应了这段代码中的 预先处理部分。就来看看beforeInvocation:
// 参数object就是方法调用protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + getSecureObjectClass()); }// 收集与方法调用相关的特性配置 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (rejectPublicInvocations) { throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. " + "This indicates a configuration error because the " + "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); }// 取得该用户已验证的身份 Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { // 进行用户授权this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); }// 切换为其他身份 // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
二、Spring Security3的MAVEN坐标
<!-- Spring Security Start --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>3.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>3.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>3.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>3.1.2.RELEASE</version> </dependency> <!-- Spring Security End -->
三、web.xml配置文件
<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 security3 主要配置文件
<?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-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <global-method-security pre-post-annotations="enabled"></global-method-security> <!-- 该路径下的资源不用过滤 --> <http pattern="/include/js/**" security="none" /> <http pattern="/include/css/**" security="none" /> <http pattern="/include/scripts/**" security="none" /> <http pattern="/include/jsp/**" security="none" /> <http pattern="/images/**" security="none" /> <http pattern="/login.jsp" security="none" /> <!--auto-config = true 则使用from-login. 如果不使用该属性 则默认为http-basic(没有session). --> <!-- lowercase-comparisons:表示URL比较前先转为小写。 --> <!-- path-type:表示使用Apache Ant的匹配模式。 --> <!--access-denied-page:访问拒绝时转向的页面。 --> <!-- access-decision-manager-ref:指定了自定义的访问策略管理器。 --> <http use-expressions="true" auto-config="true" access-denied-page="/include/jsp/timeout.jsp"> <!--login-page:指定登录页面。 --> <!-- login-processing-url:指定了客户在登录页面中按下 Sign In 按钮时要访问的 URL。 --> <!-- authentication-failure-url:指定了身份验证失败时跳转到的页面。 --> <!-- default-target-url:指定了成功进行身份验证和授权后默认呈现给用户的页面。 --> <!-- always-use-default-target:指定了是否在身份验证通过后总是跳转到default-target-url属性指定的URL。 --> <form-login login-page="/login.jsp" default-target-url='/system/default.jsp' always-use-default-target="true" authentication-failure-url="/login.jsp?login_error=1" /> <!--logout-url:指定了用于响应退出系统请求的URL。其默认值为:/j_spring_security_logout。 --> <!-- logout-success-url:退出系统后转向的URL。 --> <!-- invalidate-session:指定在退出系统时是否要销毁Session。 --> <logout invalidate-session="true" logout-success-url="/login.jsp" logout-url="/j_spring_security_logout" /> <!-- 实现免登陆验证 --> <remember-me /> <!-- max-sessions:允许用户帐号登录的次数。范例限制用户只能登录一次。 --> <!-- 此值表示:用户第二次登录时,前一次的登录信息都被清空。 --> <!-- exception-if-maximum-exceeded:默认为false, --> <!-- 当exception-if-maximum-exceeded="true"时系统会拒绝第二次登录。 --> <session-management invalid-session-url="/login.jsp" session-fixation-protection="none"> <concurrency-control max-sessions="1" error-if-maximum-exceeded="false" /> </session-management> <custom-filter ref="myFilter" before="FILTER_SECURITY_INTERCEPTOR" /> <session-management session-authentication-strategy-ref="sas" /> </http> <beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <beans:property name="maximumSessions" value="1" /> <!-- 防止session攻击 --> <beans:property name="alwaysCreateSession" value="true" /> <beans:property name="migrateSessionAttributes" value="false" /> <!-- 同一个帐号 同时只能一个人登录 --> <beans:property name="exceptionIfMaximumExceeded" value="false" /> </beans:bean> <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" /> <!-- 事件监听:实现了ApplicationListener监听接口,包括AuthenticationCredentialsNotFoundEvent 事件, --> <!-- AuthorizationFailureEvent事件,AuthorizedEvent事件, PublicInvocationEvent事件 --> <beans:bean class="org.springframework.security.authentication.event.LoggerListener" /> <!-- 自定义资源文件 提示信息 --> <beans:bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <beans:property name="basenames" value="classpath:message_zh_CN"> </beans:property> </beans:bean> <!-- 配置过滤器 --> <beans:bean id="myFilter" class="com.taskmanager.web.security.MySecurityFilter"> <!-- 用户拥有的权限 --> <beans:property name="authenticationManager" ref="myAuthenticationManager" /> <!-- 用户是否拥有所请求资源的权限 --> <beans:property name="accessDecisionManager" ref="myAccessDecisionManager" /> <!-- 资源与权限对应关系 --> <beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" /> </beans:bean> <!-- 实现了UserDetailsService的Bean --> <authentication-manager alias="myAuthenticationManager"> <authentication-provider user-service-ref="myUserDetailServiceImpl"> <!-- 登入 密码 采用MD5加密 --> <password-encoder hash="md5" ref="passwordEncoder"> </password-encoder> </authentication-provider> </authentication-manager> <!-- 验证用户请求资源 是否拥有权限 --> <beans:bean id="myAccessDecisionManager" class="com.taskmanager.web.security.MyAccessDecisionManager"> </beans:bean> <!-- 系统运行时加载 系统要拦截的资源 与用户请求时要过滤的资源 --> <beans:bean id="mySecurityMetadataSource" class="com.taskmanager.web.security.MySecurityMetadataSource"> <beans:constructor-arg name="powerService" ref="powerService"> </beans:constructor-arg> </beans:bean> <!-- 获取用户登入角色信息 --> <beans:bean id="myUserDetailServiceImpl" class="com.taskmanager.web.security.MyUserDetailServiceImpl"> <beans:property name="userService" ref="userService"></beans:property> </beans:bean> <!-- 用户的密码加密或解密 --> <beans:bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" /> </beans:beans>
五、spring security3 主要实现类
1. MySecurityFilter.java 过滤用户请求
public class MySecurityFilter extends AbstractSecurityInterceptor implements Filter { // 与applicationContext-security.xml里的myFilter的属性securityMetadataSource对应, // 其他的两个组件,已经在AbstractSecurityInterceptor定义 private FilterInvocationSecurityMetadataSource securityMetadataSource; @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { // object为FilterInvocation对象 // super.beforeInvocation(fi);//源码 // 1.获取请求资源的权限 //执行 Collection<ConfigAttribute> attributes = //securityMetadataSource.getAttributes(fi); // 2.是否拥有权限 // this.accessDecisionManager.decide(authenticated, fi, attributes); // this.accessDecisionManager.decide(authenticated, fi, attributes); InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return securityMetadataSource; } public void setSecurityMetadataSource( FilterInvocationSecurityMetadataSource securityMetadataSource) { this.securityMetadataSource = securityMetadataSource; } public void init(FilterConfig arg0) throws ServletException { // TODO Auto-generated method stub } public void destroy() { // TODO Auto-generated method stub } @Override public Class<? extends Object> getSecureObjectClass() { //下面的MyAccessDecisionManager的supports方面必须放回true,否则会提醒类型错误 return FilterInvocation.class; } }
2. MyUserDetailServiceImpl.java 点击登入跳转 保存 用户权限
public class MyUserDetailServiceImpl implements UserDetailsService { UserInter userService; public UserInter getUserService() { return userService; } public void setUserService(UserInter userService) { this.userService = userService; } // 登入默认会调整到这里 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("username is " + username); TSysUser users=null; try { users = this.userService.findByUsersLogin(username); } catch (LoginException e) { throw new UsernameNotFoundException(username); } catch (SystemRunException e) { throw new UsernameNotFoundException(username); } if (users == null) { throw new UsernameNotFoundException(username); }else { if(!users.getStatus()){ throw new UsernameNotFoundException("该用户处于锁定状态"); } } Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(users); boolean enables = true; boolean accountNonExpired = true; boolean credentialsNonExpired = true; boolean accountNonLocked = true; User userdetail = new User(users.getCode(), users.getPassword(), enables, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuths); return userdetail; } // 取得用户的权限 private Set<GrantedAuthority> obtionGrantedAuthorities(TSysUser user) { Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>(); //获取用户所属组 Set<TSysUserofusergroup> userGroups = user.getTSysUserofusergroups(); for(TSysUserofusergroup userGroup : userGroups ){ //获取用户所属组 组对象 TSysUsergroup usgroup=userGroup.getTSysUsergroup(); //获取用户组对应 角色集合 Set<TSysCharofgroup> charofgroups=usgroup.getTSysCharofgroups(); for (TSysCharofgroup charofgroup : charofgroups) { TSysCharacter character=charofgroup.getTSysCharacter(); //获取角色对应权限集合 Set<TSysPowersystemofchar> powerSystemofchars= character.getTSysPowersystemofchars(); for(TSysPowersystemofchar powerofchar : powerSystemofchars){ authSet.add( new GrantedAuthorityImpl(powerofchar.getTSysPowersystem().getRoleName())); } } } return authSet; } }
3. MySecurityMetadataSource.java 系统启动加载系统权限 用户登入验证权限
//1 加载资源与权限的对应关系 /** * 该过滤器的主要作用就是通过spring著名的IoC生成securityMetadataSource。 * securityMetadataSource相当于本包中自定义的MyInvocationSecurityMetadataSourceService。 * 该MyInvocationSecurityMetadataSourceService的作用提从数据库提取权限和资源,装配到HashMap中, * 供Spring Security使用,用于权限校验。 * @author sparta 11/3/29 */ public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { //由spring调用 public MySecurityMetadataSource(PowerInter powerService) throws SystemRunException { this.powerService = powerService; loadResourceDefine(); } private PowerInter powerService; private static Map<String, Collection<ConfigAttribute>> resourceMap = null; private RequestMatcher pathMatcher; public Collection<ConfigAttribute> getAllConfigAttributes() { return new ArrayList<ConfigAttribute>(); } public boolean supports(Class<?> clazz) { // TODO Auto-generated method stub return true; } //加载所有资源与权限的关系 private void loadResourceDefine() throws SystemRunException { if (resourceMap == null) { resourceMap = new HashMap<String, Collection<ConfigAttribute>>(); List<TSysPowersystem> resources = this.powerService .findByTSysPowersystem(); for (TSysPowersystem resource : resources) { Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>(); // 以权限名封装为Spring的security Object //resource.getRoleName() 角色名称 可随意 role_admin 或者 admin ConfigAttribute configAttribute = new SecurityConfig(resource.getRoleName()); configAttributes.add(configAttribute); //resource.getInterceptUrl() 格式必须是 拦截的包路径 //或者是 比如 /manager/**/*.jh 或者 /system/manager/**/*.jsp resourceMap.put(resource.getInterceptUrl(), configAttributes); } } } //返回所请求资源所需要的权限 public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { Iterator<String> it = resourceMap.keySet().iterator(); while (it.hasNext()) { String resURL = it.next(); Iterator<String> ite = resourceMap.keySet().iterator(); pathMatcher = new AntPathRequestMatcher(resURL); if (pathMatcher.matches(((FilterInvocation) object).getRequest())) { Collection<ConfigAttribute> returnCollection = resourceMap.get(resURL); return returnCollection; } } return null; } }
4. MyAccessDecisionManager.java 验证是否拥有访问的权限
/** *AccessdecisionManager在Spring security中是很重要的。 * *在验证部分简略提过了,所有的Authentication实现需要保存在一个GrantedAuthority对象数组中。 *这就是赋予给主体的权限。 GrantedAuthority对象通过AuthenticationManager *保存到 Authentication对象里,然后从AccessDecisionManager读出来,进行授权判断。 * *Spring Security提供了一些拦截器,来控制对安全对象的访问权限,例如方法调用或web请求。 *一个是否允许执行调用的预调用决定,是由AccessDecisionManager实现的。 *这个 AccessDecisionManager 被AbstractSecurityInterceptor调用, *它用来作最终访问控制的决定。 这个AccessDecisionManager接口包含三个方法: * void decide(Authentication authentication, Object secureObject, List<ConfigAttributeDefinition> config) throws AccessDeniedException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 从第一个方法可以看出来,AccessDecisionManager使用方法参数传递所有信息,这好像在认证评估时进行决定。 特别是,在真实的安全方法期望调用的时候,传递安全Object启用那些参数。 比如,让我们假设安全对象是一个MethodInvocation。 很容易为任何Customer参数查询MethodInvocation, 然后在AccessDecisionManager里实现一些有序的安全逻辑,来确认主体是否允许在那个客户上操作。 如果访问被拒绝,实现将抛出一个AccessDeniedException异常。 这个 supports(ConfigAttribute) 方法在启动的时候被 AbstractSecurityInterceptor调用,来决定AccessDecisionManager 是否可以执行传递ConfigAttribute。 supports(Class)方法被安全拦截器实现调用, 包含安全拦截器将显示的AccessDecisionManager支持安全对象的类型。 */ public class MyAccessDecisionManager implements AccessDecisionManager { public void decide(Authentication authentication, Object object, ; Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(configAttributes == null) { return; } //所请求的资源拥有的权限(一个资源对多个权限) Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while(iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //访问所请求资源所需要的权限 String needPermission = configAttribute.getAttribute(); System.out.println("needPermission is " + needPermission); //用户所拥有的权限authentication for(GrantedAuthority ga : authentication.getAuthorities()) { if(needPermission.equals(ga.getAuthority())) { return; } } } //没有权限 会跳转到login.jsp页面 throw new AccessDeniedException(" 没有权限访问"); } public boolean supports(ConfigAttribute attribute) { // TODO Auto-generated method stub return true; } public boolean supports(Class<?> clazz) { // TODO Auto-generated method stub return true; } }