Spring Security调研记录【七】--核心模型与实现

网上有很多关于Spring Security文章中,都认为Spring Security(相对于shiro)过于复杂,个人认为复杂的是Spring Security的官方文档而不是Spring Security本身。

        Spring Security满足了用户认证与授权的几乎所有应用场景,在其核心模型下,扩展随心所欲!


   一、认证与权限过程模型及Spring Security的处理过程

         我们普遍的认证与授权过程如下图所示。


         ①是否已登录?

在Spring Security中,接口AuthenticationTrustResolver有一个方法isAnonymous(Authentication),用于判断当前用户是否为匿名用户(匿名用户则为未登录用户)。

AuthenticationTrustResolver接口的实现类由ExceptionTranslationFilter过滤器调用。

ExceptionTranslationFilter在处理AccessDeniedException异常时,如果当前用户为匿名则调用“②登录入口”;否则调用“⑤无权访问处理”。


   ②登录入口:

Spring Security中,登录入口由接口AuthenticationEntryPoint提供,如果你不想提供一个登录界面,而是通过json返回一个特定字符串,指示客户端提供登录入口,只要提供一个的AuthenticationEntryPoint实现类组装到Spring Security的上下文即可,在第三部分会一个具体实现。

AuthenticationEntryPointExceptionTranslationFilter调用,默认实现是提供一个登录页面。


         ③登录认证是否通过?:

Spring Security中,登录认证由过滤器UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter)提供。过滤器通过判断当前Url是否为/login(可配置),是则处理。

AbstractAuthenticationProcessingFilter委托AuthenticationManager进行用户登录认证,成功则调用AuthenticationSuccessHandler进行处理,否则调用AuthenticationFailureHandler进行处理

AuthenticationSuccessHandler默认处理是只记录Session,结束当前过滤器处理,进行下一个过滤器处理。

AuthenticationFailureHandler默认处理是抛出AuthenticationException异常,由后面的ExceptionTranslationFilter统一处理。ExceptionTranslationFilter对于AuthenticationException异常,会调用AuthenticationEntryPoint进行处理

如果希望登录成功或失败通过Json返回客户端,就可以重实现接口AuthenticationSuccessHandler和AuthenticationFailureHandler,组装到Spring Security的上下文。

具体情况请见第二部分。


   ④是否有权访问?:

Spring Security中,是否有权访问资源的检查是在FilterSecurityInterceptor过滤器中,FilterSecurityInterceptor委托AccessDecisionManager进行权限检查。

具体情况请见第二部分。


         ⑤无权访问处理:

如果无权访问,则抛出AccessDeniedException异常。AccessDeniedException由ExceptionTranslationFilter统一处理,ExceptionTranslationFilter会委托接口AccessDeniedHandler进行处理。

AccessDeniedHandler的默认实现会抛出403错误。如希望在无权访问某资源时,返回json信息而不是403错误,可通过重实现AccessDeniedHandler,并组装到Spring Security上下文中即可。


  二、核心模型

核心模型总结为如下三张图,Filter、Authentication、Access

1、过滤器模式

Spring Security整体是通过管道(过滤器)模式实现功能。过滤器以如下顺序执行:


1)ChannelProcessingFilter

官方解释:“because it might need to redirect to a different protocol”
未明,暂且不理。


2)SecurityContextPersistenceFilter

本过滤器作用为从Session中加载SecurityContext (可获得Authentication),保存到SecurityContextHolder中,在处理完成后,把SecurityContextHolder中的SecurityContext 保存到session中。

如果要实现session集中化存储或缓存,则需要修改本过滤器。


3)ConcurrentSessionFilter

4)AbstractAuthenticationProcessingFilter

5)SecurityContextHolderAwareRequestFilter

6)JaasApiIntegrationFilter

7)RememberMeAuthenticationFilter

8)AnonymousAuthenticationFilter

9)ExceptionTranslationFilter

10)FilterSecurityInterceptor



2、登录模型



1)AbstractAuthenticationProcessingFilter

         AbstractAuthenticationProcessingFilter只对特定的Url进行处理,如:/login(可配置),通过把userName和password信息封装到Authentication中,委托AuthenticationManager进行认证。如果认证成功则调用AuthenticationSuccessHandler进行处理,否则调用AuthenticationFailureHandler进行处理。

2)AuthenticationManager

AuthenticationManager里配置了一个AuthenticationProvider列表,循环调用其中的AuthenticationProvider进行认证,如果有一个认证成功则返回,如果所以AuthenticationProvider都认证失败则认为失败。

        3)AuthenticationProvider

AuthenticationProvider默认实现,通过UserDetailsService加载UserDetails,对UserDetails与Authentication的credentials进行比较。

4)UserDetailsService

本接口只有一个方法:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

如需要通过Spring Security与已有的用户管理系统对接,只要重实现该接口即可。


3、权限检测与异常转换处理模型


1)ExceptionTranslationFilter

         Spring Security 在认证与权限检查过程中,都不立刻进行处理,而都是抛出相应的异常,由ExceptionTranslationFilter对不同异常调用不同接口进行处理

AccessDeniedExceiption异常,对于非匿名用户,AccessDeniedHandler进行处理,否则由AuthenticationEntryPoint进行处理。

AuthenticationException异常,则由AuthenticationEntryPoint进行处理。

        AuthenticationTrustResolver接口用于判断当前用户是否为匿名用户。


2)FilterSecurityInterceptor

FilterSecurityInterceptor主要进行权限检查工作,如果需要再次认证,则也调用AuthenticationManager进行认证,但一般不用再次认证。

FilterSecurityInterceptor,通过FilterInvocationSecurityMetadataSource,获取当前Url资源需要的角色信息ConfigAttribute;同时把该ConfigAttribute和当前用户的Authentication,传递予AccessDecisionManager进行权限检查。

如果希望Url资源的角色要求可通过第三方系统获取,只要重实现FilterInvocationSecurityMetadataSource接口即可。

       

  三、实例

实现通过username从第三方系统获取用户信息UserDetails(用户名、密码、是否超期、是否被锁、用户角色列表)

实现同第三方获取Url资源所需要的角色列表。

1、web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:jsp="http://java.sun.com/xml/ns/javaee/jsp"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>SpringSecurityTest</display-name>
	
	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring-context.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> 
	
	<servlet>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
	<error-page>
		<error-code>500</error-code>
		<location>/errorpage.jsp</location>
	</error-page>
	<error-page>
		<error-code>400</error-code>
		<location>/errorpage.jsp</location>
	</error-page>
	<error-page>
		<error-code>404</error-code>
		<location>/errorpage.jsp</location>
	</error-page>
</web-app>


2、spring-security-context.xml配置

<?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.xsd">
	<http pattern="/**/*.css" security="none" />
	<http pattern="/**/*.js" security="none" />

	<http pattern="/security/**" access-decision-manager-ref="accessDecisionManager" once-per-request="false">
		<form-login login-page="/security/login"
			login-processing-url="/security/loginprocess" default-target-url="/security/index"
			always-use-default-target="false" authentication-failure-url="/security/login?error=wrong_login_data"
			username-parameter="username" password-parameter="password" />
		<logout logout-url="/security/logout" />
		<intercept-url pattern="/security/login" access="permitAll()" />
		<intercept-url pattern="/security/logout" access="permitAll()" />
		<intercept-url pattern="/security/**" access="hasRole('NORMALUSER')" />
	    <custom-filter ref="winssageFilterSecurityInterceptor"
			before="FILTER_SECURITY_INTERCEPTOR" /> 
		<csrf disabled="true" />
	</http>

	<global-method-security pre-post-annotations="enabled" />

	<beans:bean name="bcryptEncoder"
		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

	<authentication-manager alias="authenticationManager">
		<authentication-provider user-service-ref='myUserDetailsService'>
			<password-encoder ref="bcryptEncoder" />
		</authentication-provider>
	</authentication-manager>

	<beans:bean id="myUserDetailsService"
		class="com.winssage.spring.security.userdetails.WinssageUserDetailsService">
		<beans:property name="bcryptPasswordEncoder" ref="bcryptEncoder" />
	</beans:bean>

	<beans:bean id="winssageFilterSecurityInterceptor"
		class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
		<beans:property name="securityMetadataSource" ref="securityMetadataSource" />
	</beans:bean>

	<beans:bean id="accessDecisionManager"
		class="org.springframework.security.access.vote.AffirmativeBased">
		<beans:constructor-arg name="decisionVoters">
			<beans:list>
				<beans:bean class="org.springframework.security.access.vote.RoleVoter">
					<beans:property name="rolePrefix" value="ROLE_" />
				</beans:bean>
				<beans:bean
					class="org.springframework.security.access.vote.AuthenticatedVoter" />
				<beans:bean
					class="org.springframework.security.web.access.expression.WebExpressionVoter" />
			</beans:list>
		</beans:constructor-arg>
	</beans:bean>

	<beans:bean id="securityMetadataSource"
		class="com.winssage.spring.security.access.intercept.WinssageSecurityMetadataSource">
		<beans:constructor-arg ref="securityMetadataSourceAdapter" />
	</beans:bean>

	<beans:bean id="securityMetadataSourceAdapter"
		class="com.winssage.spring.security.DefaultSecurityMetadataSourceAdapter">
	</beans:bean>
</beans:beans>



3、 WinssageUserDetailsService

package com.winssage.spring.security.userdetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class WinssageUserDetailsService implements UserDetailsService {
	
	
	BCryptPasswordEncoder bcryptPasswordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		
		List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
		grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
		grantedAuths.add(new SimpleGrantedAuthority("ROLE_NORMALUSER"));
		

		boolean enables = true;
		boolean accountNonExpired = true;
		boolean credentialsNonExpired = true;
		boolean accountNonLocked = true;
		String password=(null==bcryptPasswordEncoder)?"123456":bcryptPasswordEncoder.encode("123456");
		User userdetail = new User(username, password, enables,
				accountNonExpired, credentialsNonExpired, accountNonLocked,
				grantedAuths);
		return userdetail;
	}

	public BCryptPasswordEncoder getBcryptPasswordEncoder() {
		return bcryptPasswordEncoder;
	}

	public void setBcryptPasswordEncoder(BCryptPasswordEncoder bcryptPasswordEncoder) {
		this.bcryptPasswordEncoder = bcryptPasswordEncoder;
	}

}


4、WinssageSecurityMetadataSource

package com.winssage.spring.security.access.intercept;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;

public final class WinssageSecurityMetadataSource implements
		FilterInvocationSecurityMetadataSource {
	protected final Log logger = LogFactory.getLog(getClass());
	private SecurityMetadataSourceAdapter adapter = null;

	// ~ Constructors
	// ===================================================================================================
	public WinssageSecurityMetadataSource(SecurityMetadataSourceAdapter adapter) {
		this.adapter = adapter;
	}

	// ~ Methods
	// ========================================================================================================

	public Collection<ConfigAttribute> getAllConfigAttributes() {

		return null;
	}

	public Collection<ConfigAttribute> getAttributes(Object object) {
		Set<ConfigAttribute> resultAttributes = new HashSet<ConfigAttribute>();
		ConfigAttribute resultAttr;

		String url = ((FilterInvocation) object).getRequestUrl();
		
		
		Collection<AccessAttribute> accessAttributes = adapter
				.getAttributes(url);
		if(null==accessAttributes||accessAttributes.size()==0)
			return null;
		
		for (AccessAttribute accessAttribute : accessAttributes) {
			if (null == accessAttribute)
				continue;
			resultAttr = new SecurityConfig(accessAttribute.getAttribute());
			resultAttributes.add(resultAttr);
		}
        <span style="white-space:pre">	</span>return resultAttributes;

	}

	public boolean supports(Class<?> clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}
}

5、DefaultSecurityMetadataSourceAdapter

package com.winssage.spring.security;

import java.util.Collection;
import java.util.HashSet;

import org.springframework.security.access.ConfigAttribute;

import com.winssage.spring.security.access.intercept.AccessAttribute;
import com.winssage.spring.security.access.intercept.DefaultAccessAttribute;
import com.winssage.spring.security.access.intercept.SecurityMetadataSourceAdapter;

public class DefaultSecurityMetadataSourceAdapter implements
		SecurityMetadataSourceAdapter {

	@Override
	public Collection<AccessAttribute> getAttributes(Object object) {
		
		if(!(object instanceof String)) return null;
		
		Collection<AccessAttribute> attributes=new HashSet<AccessAttribute>();
		String url=(String)object;
		if (!url.equals("/security/index")) {
			return null;
		}
		
		AccessAttribute attr = new DefaultAccessAttribute("ROLE_ADMIN");
		attributes.add(attr);
		attr = new DefaultAccessAttribute("ROLE_USER");
		attributes.add(attr);
		return attributes;

	}

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值