• 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校验以及权限获取

  • 相关阅读:
    RANSAC算法——RANDOM SAMPLE CONSENSUS
    屏蔽关键词搜索
    全景图拼接——opencv之stitching
    如何 查看本地安装的 .NET Framework(CLR) 版本 方法二亲测可行
    js判断是否是ie浏览器且给出ie版本
    js判断文件大小利用img 的 dynsrc,兼容IE10 及以上
    Java的——String类、StringBuffer和StringBuilder、不可变和可变字符序列使用陷阱
    windows.onload和body的onload属性的区别
    jquery中class选择器的用法,选择两个class
    #JS# 如何判断一个字符串是否为日期格式
  • 原文地址:https://www.cnblogs.com/question-sky/p/7063568.html
Copyright © 2020-2023  润新知