spring security OAuth实践

@[TOC](spring security OAuth实践(未完待续))

参考文章

核心filter

spring security维护了一个FilterChainProxy,这个类会依次调用spring security过滤器链,默认的过滤器链是11个,加上自定义的(MyusernamePasswordAuthentication是自定义过滤器,用于代替默认的UsernamePasswordAuthentication)一个是十二个,如图,调用顺序从0-12
在这里插入图片描述

SecurityContextPersistenceFilter

整个Spring Security 过滤器链的开端,它有两个作用:一是当请求到来时,检查Session中是否存在SecurityContext,如果不存在,就创建一个新的SecurityContext。二是请求结束时将SecurityContext放入 session中,并清空 SecurityContextHolder

UsernamePasswordAuthenticationFilter

继承自抽象类 AbstractAuthenticationProcessingFilter,当进行表单登录时,该Filter将用户名和密码封装成一个 UsernamePasswordAuthentication进行验证。
改filter主要校验表单参数,之后封装成一个 UsernamePasswordAuthentication,如果需要修改spring security自动生成的表单元素(如添加一个验证码参数),需要自定义一个UsernamePasswordAuthenticationFilter,在过滤器配置自定义filter以替换UsernamePasswordAuthenticationFilter实现表单校验构造UsernamePasswordAuthentication

AnonymousAuthenticationFilter

匿名身份过滤器,当前面的Filter认证后依然没有用户信息时,该Filter会生成一个匿名身份——AnonymousAuthenticationToken。一般的作用是用于匿名登录。

ExceptionTranslationFilter

异常转换过滤器,用于处理 FilterSecurityInterceptor抛出的异常。
过滤器链经过此filter时会进行try…catch…,如果后面的filter抛出异常,会在此处捕获并处理(如后面的FilterSecurityInterceptor投票后抛出SpringSecurityException)

FilterSecurityInterceptor

过滤器链最后的关卡,从 SecurityContextHolder中获取 Authentication,比对用户拥有的权限和所访问资源需要的权限。

认证过程

在这里插入图片描述
认证过程之后再补充
如果需要自定义认证过程需要自定义LoginAuthenticationProvide实现AuthenticationProvider接口(AuthenticationProvider默认的实现是DaoAuthenticationProvider)
重写authenticatesupports方法
authenticate方法参考DaoAuthenticationProvider类的authenticate方法,认证成功返回一个UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities())对象,认证失败抛出AuthenticationException异常,AuthenticationException异常是一个spring security的抽象异常,它有很多子类实现,其中之一就是UsernameNotFoundExceptionBadCredentialsException


读取保存在session中的认证信息

SecurityContextPersistenceFilter

当我们填写表单完毕后,点击登录按钮,请求先经过 SecurityContextPersistenceFilter 过滤器,在前面就曾提到,该Filter有两个作用,其中之一就是在请求到来时,创建 SecurityContext安全上下文,我们来看看它内部是如何做的,部分源码如下:

public class SecurityContextPersistenceFilter extends GenericFilterBean {

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";
	/**安全上下文存储的仓库*/
	private SecurityContextRepository repo;

	private boolean forceEagerSessionCreation = false;
	/**使用HttpSession来存储 SecurityContext*/
	public SecurityContextPersistenceFilter() {
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		/**如果是第一次请求,request中肯定没有FILTER_APPLIED属性*/
		if (request.getAttribute(FILTER_APPLIED) != null) {
			// ensure that filter is only applied once per request
			/** 确保每个请求只应用一次过滤器*/
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();
		/**
		* 在request 设置 FILTER_APPLIED 属性为 true,
		* 这样同一个请求再次访问时,就直接进入后续Filter的操作
		*/
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
		/**
		* 封装 requset 和 response 
		*/
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		/**
		*  从存储安全上下文的仓库中载入 SecurityContext 安全上下文,
		*  其内部是从 Session中获取上下文信息
		*/
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			/**
			* 安全上下文信息设置到 SecurityContextHolder 中,
			* 以便在同一个线程中,后续访问 SecurityContextHolder 
			* 能获取到 SecuritContext*/
		    SecurityContextHolder.setContext(contextBeforeChainExecution);

			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			/**
			* 请求结束后,清空安全上下文信息
			*/
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			/**
			* 将安全上下文信息存储到 Session中,相当于登录态的维护
			*/
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}

	public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
		this.forceEagerSessionCreation = forceEagerSessionCreation;
	}
}

请求到来时,利用HttpSessionSecurityContextRepository读取安全上下文。我们这里是第一次请求,读取的安全上下文中是没有 Authentication身份信息的,将安全上下文设置到 SecurityContextHolder之后,进入下一个过滤器。

请求结束时,同样利用HttpSessionSecurityContextRepository该存储安全上下文的仓库将认证后的SecurityContext放入 session中,这也是登录态维护的关键,具体调用的是 SecurityContextRepositorysaveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

HttpSessionSecurityContextRepository

HttpSessionSecurityContextRepositorysave()方法

		@Override
		protected void saveContext(SecurityContext context) {
			/**从上下文中获取到认证信息*/
			final Authentication authentication = context.getAuthentication();
			HttpSession httpSession = request.getSession(false);

			// See SEC-776
			/**认证信息为空或者是匿名用户*/
			if (authentication == null || trustResolver.isAnonymous(authentication)) {
				if (logger.isDebugEnabled()) {
					logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
				}

				if (httpSession != null && authBeforeExecution != null) {
					// SEC-1587 A non-anonymous context may still be in the session
					// SEC-1735 remove if the contextBeforeExecution was not anonymous
					httpSession.removeAttribute(springSecurityContextKey);
				}
				return;
			}

			if (httpSession == null) {
				httpSession = createNewSessionIfAllowed(context);
			}

			// If HttpSession exists, store current SecurityContext but only if it has
			// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
			if (httpSession != null) {
				// We may have a new session, so check also whether the context attribute
				// is set SEC-1561
				if (contextChanged(context)
						|| httpSession.getAttribute(springSecurityContextKey) == null) {
					/**设置上下文信息到session的attribute中,保存了认证状态*/	
					httpSession.setAttribute(springSecurityContextKey, context);

					if (logger.isDebugEnabled()) {
						logger.debug("SecurityContext '" + context
								+ "' stored to HttpSession: '" + httpSession);
					}
				}
			}
		}
SecurityContext

SecurityContext是一个接口,有两个方法

  • 获取认证信息和设置认证信息

在这里插入图片描述

UsernamePasswordAuthenticationFilter 的父类是 AbstractAuthenticationProcessingFilter,首先进入父类的 doFilter方法,部分源码如下:

该doFilter方法中一个核心就是调用子类 UsernamePasswordAuthenticationFilterattemptAuthentication方法,该方法进入真正的认证过程,并返回认证后的 Authentication,该方法的源码如下:

该方法中有一个关键点就是 this.getAuthenticationManager().authenticate(authRequest),调用内部的AuthenticationManager去认证,在之前的文章就介绍过AuthenticationManager,它是身份认证的核心接口,它的实现类是 ProviderManager,而 ProviderManager又将请求委托给一个 AuthenticationProvider列表,列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性

下面来分析一个关键的 AuthenticationProvider,它就是 DaoAuthenticationProvider,它是框架最早的provider,也是最最常用的 provider。大多数情况下我们会依靠它来进行身份认证,它的父类是 AbstractUserDetailsAuthenticationProvider ,认证过程首先会调用父类的 authenticate方法,核心源码如下:

从上面一大串源码中,提取几个关键的方法:

retrieveUser(…): 调用子类 DaoAuthenticationProviderretrieveUser()方法获取 UserDetails
preAuthenticationChecks.check(user): 对从上面获取的UserDetails进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
additionalAuthenticationChecks(user,authentication): 对UserDetails附加的检查,对传入的Authentication与获取的UserDetails进行密码匹配
postAuthenticationChecks.check(user): 对UserDetails进行后检查,即检查UserDetails的密码是否过期
createSuccessAuthentication(principalToReturn, authentication, user): 上面所有检查成功后,利用传入的Authentication 和获取的UserDetails生成一个成功验证的Authentication

如何自定义登录security

构造一个org.springframework.security.core.userdetails.User( user.getAccountID() + "|" + user.getUserName(), user.getPassword(), authorities);对象
构造一个UsernamePasswordAuthenticationToken
通过authenticate(usernamePasswordAuthenticationToken)进行认证,认证之后保存在上下文
SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(authentication);
调用结束SecurityContextPersistenceFilter会将上下文保存在session中,这样就将自定义的登录与security的认证兼容了(可以不使用security的认证了)

  UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, password, userDetails.getAuthorities());
        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(authentication);
还有一种不经过security登录认证兼容已有登录

WebSecurityConfigurerAdapter的子类配置中,配置一个自定义filter,插入在
SecurityContextPersistenceFilter前,在这个filter中兼容已有登录状态,将认证信息放在session的attribute中,这样执行到SecurityContextPersistenceFilter时也可以从session中加载到认证信息

 List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username, "", authorities);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, "", userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(usernamePasswordAuthenticationToken);
        request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

http.addFilterBefore(accessTokenFilter, StopSecurityDefaultEndpointFilter.class);

自定义退出登录

退出登录源码参考
security的默认退出登录逻辑在LogoutFilter

维护了一个LogoutSuccessHandler,这个类决定了退出登录成功后的重定向,默认是SimpleUrlLogoutSuccessHandler,handler默认是CompositeLogoutHandler,如果想要修改重定向的方式,可以自定义一个LogoutSuccessHandler配置 在 SecurityConfig中配置,http.logout() .logoutSuccessHandler(logoutSuccessHandler)

自定义未认证处理

实现AccessDeniedHandler接口,重写handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)方法自定义返回值即可

自定义TokenEndPoint异常处理
ProviderManager

ProviderManager
实现WebResponseExceptionTranslator接口,重写translate(Exception e)方法,

获取公钥端点和校验token端点权限配置
security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()")
这两个端点是跨域配置登录后访问的,如果使用的是自定义登录的方式,配置security.addTokenEndpointAuthenticationFilter(MyEndpointFilter)
MyEndpointFilter是自己实现的filter,在此filter中兼容自己的登录方式,把认证信息存储在session的attribute中

 List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username, "", authorities);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, "", userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(usernamePasswordAuthenticationToken);
        request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

这样,这两个端点检查认证的时候就可以识别到用户已经登录了
如果需要修改未登录范湖IDE错误,可以实现AuthenticationEntryPoint接口,实现commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)方法,配置security.authenticationEntryPoint(MyauthenticationEntryPoint)就可以了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值