Springboot security cas源码陶冶-CasAuthenticationFilter

Springboot security cas整合方案中不可或缺的校验Filter类或者称为认证Filter类,其内部包含校验器、权限获取等,特开辟新地啃啃

继承结构

    - AbstractAuthenticationProcessingFilter
        - CasAuthenticationFilter

其中父类AbstractAuthenticationProcessingFilter#doFilter()是模板处理逻辑方法,而子类主要实现了校验方法CasAuthenticationFilter#attemptAuthentication()方法。下面就对这两块进行代码层面的分析

AbstractAuthenticationProcessingFilter#doFilter-处理逻辑

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		//是否需要验证,这里cas子类对其进行了复写
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		//凭证信息
		Authentication authResult;

		try {
			//调用子类来进行相关的验证操作,供子类复写
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				//返回为空,则校验停止
				return;
			}
			//session策略校验,默认不校验
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			//对其中产生的异常进行页面输出,即直接以页面呈现错误
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			//对其中产生的异常进行页面输出,即直接以页面呈现错误
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		//认证成功后是否还往下走,默认为false
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//直接跳转至配置好的登录成功页面,这里cas子类对其进行了复写
		successfulAuthentication(request, response, chain, authResult);
	}

其中CasAuthenticationFilter对以下方法进行了复写,分别为requiresAuthentication()successfulAuthentication()attemptAuthentication()方法

CasAuthenticationFilter#requiresAuthentication-是否校验判断

   protected boolean requiresAuthentication(final HttpServletRequest request,
			final HttpServletResponse response) {
		//是否与设置的登录路径匹配
		final boolean serviceTicketRequest = serviceTicketRequest(request, response);
		//对含有ticket参数的请求会返回true
		final boolean result = serviceTicketRequest || proxyReceptorRequest(request)
				|| (proxyTicketRequest(serviceTicketRequest, request));
		if (logger.isDebugEnabled()) {
			logger.debug("requiresAuthentication = " + result);
		}
		return result;
	}

对login请求以及token请求则返回true表示需要验证

CasAuthenticationFilter#attemptAuthentication-具体校验处理

        @Override
	    public Authentication attemptAuthentication(final HttpServletRequest request,
			final HttpServletResponse response) throws AuthenticationException,
			IOException {
		// if the request is a proxy request process it and return null to indicate the
		// request has been processed
		//代理服务的请求处理,涉及PGT
		if (proxyReceptorRequest(request)) {
			logger.debug("Responding to proxy receptor request");
			//直接响应输出
			CommonUtils.readAndRespondToProxyReceptorRequest(request, response,
					this.proxyGrantingTicketStorage);
			return null;
		}
		//判断是否对应指定的请求(login请求),支持ant-style方式
		final boolean serviceTicketRequest = serviceTicketRequest(request, response);
        //login请求为"_cas_stateful_",非login请求为"_cas_stateless_"
		final String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER
				: CAS_STATELESS_IDENTIFIER;
		//获取ticket
		String password = obtainArtifact(request);
		
		//passwprd一般不可为空,这在provider处理类中会抛异常
		if (password == null) {
			logger.debug("Failed to obtain an artifact (cas ticket)");
			password = "";
		}

		final UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
		//通过CasAuthenticationProvider来进行具体的校验,包含ticket验证以及当前用户权限集合获取
		return this.getAuthenticationManager().authenticate(authRequest);
	    }

具体的校验通过CasAuthenticationProvider来实现

CasAuthenticationProvider-cas校验器

cas校验器,看下主要的实现方法

  • CasAuthenticationProvider#afterPropertiesSet()
    主要是检验必须的属性是否设置
    	public void afterPropertiesSet() throws Exception {
    	//权限获取处理对象
    	Assert.notNull(this.authenticationUserDetailsService,
    			"An authenticationUserDetailsService must be set");
    	//ticket校验器
    	Assert.notNull(this.ticketValidator, "A ticketValidator must be set");
    	//stateless对应的缓存,默认为NullStatelessTicketCache
    	Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
    	//必须设置key
    	Assert.hasText(
    			this.key,
    			"A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
    	//默认为SpringSecurityMessageSource.getAccessor()
    	Assert.notNull(this.messages, "A message source must be set");
    }
    
  • CasAuthenticationProvider#authenticate
    校验处理方法,源码如下
    	//此处传过来的authentication类型为UsernamePasswordAuthenticationToken
    	public Authentication authenticate(Authentication authentication)
    		throws AuthenticationException {
    	//此处为true
    	if (!supports(authentication.getClass())) {
    		return null;
    	}
    
    	if (authentication instanceof UsernamePasswordAuthenticationToken
    			&& (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
    					.equals(authentication.getPrincipal().toString()) && !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
    					.equals(authentication.getPrincipal().toString()))) {
    		// UsernamePasswordAuthenticationToken not CAS related
    		return null;
    	}
    
    	// If an existing CasAuthenticationToken, just check we created it
    	if (authentication instanceof CasAuthenticationToken) {
    		if (this.key.hashCode() == ((CasAuthenticationToken) authentication)
    				.getKeyHash()) {
    			return authentication;
    		}
    		else {
    			throw new BadCredentialsException(
    					messages.getMessage("CasAuthenticationProvider.incorrectKey",
    							"The presented CasAuthenticationToken does not contain the expected key"));
    		}
    	}
    
    	// Ensure credentials are presented,确保ticket不为空,否则将抛出异常
    	if ((authentication.getCredentials() == null)
    			|| "".equals(authentication.getCredentials())) {
    		throw new BadCredentialsException(messages.getMessage(
    				"CasAuthenticationProvider.noServiceTicket",
    				"Failed to provide a CAS service ticket to validate"));
    	}
    
    	boolean stateless = false;
    
    	if (authentication instanceof UsernamePasswordAuthenticationToken
    			&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication
    					.getPrincipal())) {
    		stateless = true;
    	}
    
    	CasAuthenticationToken result = null;
    	//对非login请求的尝试从缓存中获取
    	if (stateless) {
    		// Try to obtain from cache
    		result = statelessTicketCache.getByTicketId(authentication.getCredentials()
    				.toString());
    	}
    
    	if (result == null) {
    		//第一次校验则用ticketValidator去cas服务端进行ticket校验
    		result = this.authenticateNow(authentication);
    		result.setDetails(authentication.getDetails());
    	}
    	//对非login请求的castoken进行缓存
    	if (stateless) {
    		// Add to cache
    		statelessTicketCache.putTicketInCache(result);
    	}
    
    	return result;
    }
    
  • CasAuthenticationProvider#authenticateNow
    实际的校验处理方法,源码如下
    	private CasAuthenticationToken authenticateNow(final Authentication authentication)
    		throws AuthenticationException {
    	try {
    		//TicketValidator一般只需要设置casServerUrlPrefix前缀,实际的请求全路径如下,以Cas20ServiceTicketValidator为例
    		//https://example.casserver.com/cas/serviceValidator?service=https://example.casclient.com/
    		final Assertion assertion = this.ticketValidator.validate(authentication
    				.getCredentials().toString(), getServiceUrl(authentication));
    		//调用authenticationUserDetailsService获取当前用户所拥有的权限
    		final UserDetails userDetails = loadUserByAssertion(assertion);
    		userDetailsChecker.check(userDetails);
    		//组装成CasAuthenticationToken来保存校验信息,供保存至spring的安全上下文中
    		return new CasAuthenticationToken(this.key, userDetails,
    				authentication.getCredentials(),
    				authoritiesMapper.mapAuthorities(userDetails.getAuthorities()),
    				userDetails, assertion);
    	}
    	catch (final TicketValidationException e) {
    		//ticket校验失败则抛出异常,此异常会被父类获取而调用failerhandler将错误写向页面
    		throw new BadCredentialsException(e.getMessage(), e);
    	}
    }
    
  1. CasAuthenticationProvider的必要属性含义
  • authenticationUserDetailsService-权限获取对象
  • ticketValidator-ticket校验器,其中需要设置cas服务端的校验地址前缀casServerUrlPrefix
  • key-设置唯一标识
  1. CasAuthenticationProvider校验过程中如果ticket为空或者ticket校验失败都会由AbstractAuthenticationProcessingFilter类抓取并将错误信息写入到页面中,从而关于ticket的异常信息都会显示至前端页面
  2. CasAuthenticationProvider校验成功后会生成CasAuthenticationToken,且设置authenticatedtrue并保存至spring的安全上下文中,这在FilterSecurityInterceptorFilter类会有所作用

CasAuthenticationFilter#successfulAuthentication-校验成功处理

    	protected final void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		//如果请求含有ticket参数,返回true
		//login请求则直接返回false从而调用父类的successfulAuthentication()来直接响应页面
		boolean continueFilterChain = proxyTicketRequest(
				serviceTicketRequest(request, response), request);
		if (!continueFilterChain) {
			super.successfulAuthentication(request, response, chain, authResult);
			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//保存Authentication凭证信息		
               SecurityContextHolder.getContext().setAuthentication(authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//往下继续走
		chain.doFilter(request, response);
	}

小结

  1. CasAuthenticationFilter的放行策略:非登录请求;非代理接收请求;非ticket请求

  2. 对登录请求的成功处理是直接跳转至指定的页面,可通过SimpleUrlAuthenticationSuccessHandler#setDefaultTargetUrl(String url)设置;
    对非登录请求比如token请求的操作将保存校验通过的Authentication对象至SecurityContextHolder.getContext()上下文中再放行

  3. CasAuthenticationProvider校验过程中如果ticket为空或者ticket校验失败都会由AbstractAuthenticationProcessingFilter类抓取并将错误信息写入到页面中,从而关于ticket的异常信息都会显示至前端页面
    温馨提示:cas服务端登录成功后的service路径不要为login请求,避免token没拿到就被拦截从而输出错误页面

  4. 其中对ticket进行校验的是CasAuthenticationProvider对象,包括ticket校验以及权限获取

posted @ 2017-06-22 10:25  南柯问天  阅读(8671)  评论(0编辑  收藏  举报