zoukankan      html  css  js  c++  java
  • Springboot security cas整合方案-实践篇

    承接前文Springboot security cas整合方案-原理篇,请在理解原理的情况下再查看实践篇

    maven环境

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!-- 添加spring security cas支持 -->
            <dependency>
            	<groupId>org.springframework.security</groupId>
            	<artifactId>spring-security-cas</artifactId>
            </dependency>
    

    cas基础配置

    包含配置文件以及对应的VO类

    1. src/main/resources/application-cas.yml
        cas:
         server:
           host:
            url: http://192.168.1.101/cas #cas服务地址
            login_url: /login #登录地址
            logout_url: /logout #注销地址
    
        app:
         server:
          host:
            url: http://localhost:8080/web-cas #本应用访问地址
         login:
            url: /login/cas	#本应用登录地址
         logout:
            url: /logout #本应用退出地址
    
    1. 对应的VO类,应用@Component注解加载
        @Component
        public class AcmCasProperties {
    
    	@Value("${cas.server.host.url}")
    	private String casServerPrefix;
    
    	@Value("${cas.server.host.login_url}")
    	private String casServerLoginUrl;
    
    	@Value("${cas.server.host.logout_url}")
    	private String casServerLogoutUrl;
    
    	@Value("${app.server.host.url}")
    	private String appServicePrefix;
    
    	@Value("${app.login.url}")
    	private String appServiceLoginUrl;
    
    	@Value("${app.logout.url}")
    	private String appServiceLogoutUrl;
    
    	public String getCasServerPrefix() {
    		return LocalIpUtil.replaceTrueIpIfLocalhost(casServerPrefix);
    	}
    
    	public void setCasServerPrefix(String casServerPrefix) {
    		this.casServerPrefix = casServerPrefix;
    	}
    
    	public String getCasServerLoginUrl() {
    		return casServerLoginUrl;
    	}
    
    	public void setCasServerLoginUrl(String casServerLoginUrl) {
    		this.casServerLoginUrl = casServerLoginUrl;
    	}
    
    	public String getCasServerLogoutUrl() {
    		return casServerLogoutUrl;
    	}
    
    	public void setCasServerLogoutUrl(String casServerLogoutUrl) {
    		this.casServerLogoutUrl = casServerLogoutUrl;
    	}
    
    	public String getAppServicePrefix() {
    		return LocalIpUtil.replaceTrueIpIfLocalhost(appServicePrefix);
    	}
    
    	public void setAppServicePrefix(String appServicePrefix) {
    		this.appServicePrefix = appServicePrefix;
    	}
    
    	public String getAppServiceLoginUrl() {
    		return appServiceLoginUrl;
    	}
    
    	public void setAppServiceLoginUrl(String appServiceLoginUrl) {
    		this.appServiceLoginUrl = appServiceLoginUrl;
    	}
    
    	public String getAppServiceLogoutUrl() {
    		return appServiceLogoutUrl;
    	}
    
    	public void setAppServiceLogoutUrl(String appServiceLogoutUrl) {
    		this.appServiceLogoutUrl = appServiceLogoutUrl;
    	}
    
        }
    
    1. 其中用到了LocalIpUtil工具类,主要是替换localhost或者域名为真实的ip
        public class LocalIpUtil
        {
          private static Logger logger = LoggerFactory.getLogger(LocalIpUtil.class);
          private static final String WINDOWS = "WINDOWS";
    
          public static void main(String[] args)
          {
            String url = "http://127.0.0.1:8080/client1";
    
            System.out.println(replaceTrueIpIfLocalhost(url));
          }
    
          public static String replaceTrueIpIfLocalhost(String url) {
            String localIp = getLocalIp();
    
            if ((url.contains("localhost")) || (url.contains("127.0.0.1"))) {
              url = url.replaceAll("localhost", localIp).replaceAll("127.0.0.1", localIp);
            }
            return url;
          }
    
          private static String getLocalIp()
          {
            String os = System.getProperty("os.name").toUpperCase();
            String address = "";
            if (os.contains("WINDOWS"))
              try {
                address = InetAddress.getLocalHost().getHostAddress();
              } catch (UnknownHostException e) {
                logger.error("windows获取本地IP出错", e);
              }
            else {
              address = getLinuxIP();
            }
            return address;
          }
    
          private static String getLinuxIP()
          {
            String address = "";
            try
            {
              Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces();
              InetAddress ip = null;
              while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = (NetworkInterface)allNetInterfaces.nextElement();
                if ((netInterface.isUp()) && (!netInterface.isLoopback()) && (!netInterface.isVirtual()))
                {
                  Enumeration addresses = netInterface.getInetAddresses();
                  while (addresses.hasMoreElements()) {
                    ip = (InetAddress)addresses.nextElement();
                    if ((!ip.isLoopbackAddress()) && 
                      (ip != null) && ((ip instanceof Inet4Address)))
                      address = ip.getHostAddress();
                  }
                }
              }
            } catch (SocketException e) {
              logger.error("linux获取本地IP出错", e);
            }
            return address;
      }
    

    Springboot 应用cas配置

    src/main/resources/application.yml应用application-cas.yml

    	spring:
    	  profiles:
    	    active: cas
    

    Springboot 配置cas过滤链

    这里采用@Configuration@Bean注解来完成,包括LogoutFilterSingleSignOutFilterticket校验器service配置对象cas凭证校验器ProviderCasAuthenticationEntryPoint-cas认证入口

    @Configuration
    public class AcmCasConfiguration {
    
    	@Resource
    	private AcmCasProperties acmCasProperties;
    
    	/**
    	 * 设置客户端service的属性
    	 * <p>
    	 * 主要设置请求cas服务端后的回调路径,一般为主页地址,不可为登录地址
    	 * 
    	 * </p>
    	 * 
    	 * @return
    	 */
    	@Bean
    	public ServiceProperties serviceProperties() {
    		ServiceProperties serviceProperties = new ServiceProperties();
    		// 设置回调的service路径,此为主页路径
    		serviceProperties.setService(acmCasProperties.getAppServicePrefix() + "/index.html");
    		// 对所有的未拥有ticket的访问均需要验证
    		serviceProperties.setAuthenticateAllArtifacts(true);
    
    		return serviceProperties;
    	}
    
    	/**
    	 * 配置ticket校验器
    	 * 
    	 * @return
    	 */
    	@Bean
    	public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
    		// 配置上服务端的校验ticket地址
    		return new Cas20ServiceTicketValidator(acmCasProperties.getCasServerPrefix());
    	}
    
    	/**
    	 * 单点注销,接受cas服务端发出的注销session请求
    	 * 
    	 * @see SingleLogout(SLO) Front or Back Channel
    	 * 
    	 * @return
    	 */
    	@Bean
    	public SingleSignOutFilter singleSignOutFilter() {
    		SingleSignOutFilter outFilter = new SingleSignOutFilter();
    		// 设置cas服务端路径前缀,应用于front channel的注销请求
    		outFilter.setCasServerUrlPrefix(acmCasProperties.getCasServerPrefix());
    		outFilter.setIgnoreInitConfiguration(true);
    
    		return outFilter;
    	}
    
    	/**
    	 * 单点请求cas客户端退出Filter类
    	 * 
    	 * 请求/logout,转发至cas服务端进行注销
    	 */
    	@Bean
    	public LogoutFilter logoutFilter() {
    		// 设置回调地址,以免注销后页面不再跳转
    		StringBuilder logoutRedirectPath = new StringBuilder();
    		logoutRedirectPath.append(acmCasProperties.getCasServerPrefix())
    				.append(acmCasProperties.getCasServerLogoutUrl()).append("?service=")
    				.append(acmCasProperties.getAppServicePrefix());
    
    		LogoutFilter logoutFilter = new LogoutFilter(logoutRedirectPath.toString(), new SecurityContextLogoutHandler());
    
    		logoutFilter.setFilterProcessesUrl(acmCasProperties.getAppServiceLogoutUrl());
    		return logoutFilter;
    	}
    
    	/**
    	 * 创建cas校验类
    	 * 
    	 * <p>
    	 * <b>Notes:</b> TicketValidator、AuthenticationUserDetailService属性必须设置;
    	 * serviceProperties属性主要应用于ticketValidator用于去cas服务端检验ticket
    	 * </p>
    	 * 
    	 * @return
    	 */
    	@Bean("casProvider")
    	public CasAuthenticationProvider casAuthenticationProvider(
    			AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService) {
    		CasAuthenticationProvider provider = new CasAuthenticationProvider();
    		provider.setKey("casProvider");
    		provider.setServiceProperties(serviceProperties());
    		provider.setTicketValidator(cas20ServiceTicketValidator());
    		provider.setAuthenticationUserDetailsService(userDetailsService);
    
    		return provider;
    	}
    
    	/**
    	 * ==============================================================
    	 * ==============================================================
    	 */
    
    	/**
    	 * 认证的入口,即跳转至服务端的cas地址
    	 * 
    	 * <p>
    	 * <b>Note:</b>浏览器访问不可直接填客户端的login请求,若如此则会返回Error页面,无法被此入口拦截
    	 * </p>
    	 */
    	@Bean
    	public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
    		CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
    		entryPoint.setServiceProperties(serviceProperties());
    		entryPoint.setLoginUrl(acmCasProperties.getCasServerPrefix() + acmCasProperties.getCasServerLoginUrl());
    
    		return entryPoint;
    	}
    }
    

    下面对上述的AuthenticationUserDetailsService需要手动配置下,用于权限集合的获取

    配置cas获取权限集合的AuthenticationUserDetailsService

    @Component
    public class AcmCasUserDetailService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
    
    	private static final Logger USER_SERVICE_LOGGER = LoggerFactory.getLogger(AcmCasUserDetailService.class);
    
    	@Resource
    	private TSysUserDao tsysUserDAO;
    
    	@Override
    	public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
    		USER_SERVICE_LOGGER.info("校验成功的登录名为: " + token.getName());
    		//此处涉及到数据库操作然后读取权限集合,读者可自行实现
    		SysUser sysUser = tsysUserDAO.findByUserName(token.getName());
    		if (null == sysUser) {
    			throw new UsernameNotFoundException("username isn't exsited in log-cms");
    		}
    		return sysUser;
    	}
    
    }
    

    示例中的SysUser实现了UserDetail接口,实现的方法代码如下

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<GrantedAuthority> auths = new ArrayList<>();
    	    //获取用户对应的角色集合
            List<SysRole> roles = this.getSysRoles();
            for (SysRole role : roles) {
    	        //手动加上ROLE_前缀
                auths.add(new SimpleGrantedAuthority(SercurityConstants.prefix+role.getRoleName()));
            }
            return auths;
        }
    

    FilterSecurityInterceptor配置

    需要配置权限的认证过滤链

    @Component
    public class CasFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
        @Resource
        private FilterInvocationSecurityMetadataSource securityMetadataSource;
    
        @Resource
        public void setMyAccessDecisionManager(AccessDecisionManager myAccessDecisionManager) {
            super.setAccessDecisionManager(myAccessDecisionManager);
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
            invoke(fi);
        }
        private void invoke(FilterInvocation fi) throws IOException, ServletException {
            //fi里面有一个被拦截的url
            //里面调用CasInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
            //再调用CasAccessDecisionManager的decide方法来校验用户的权限是否足够
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
            //执行下一个拦截器
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.afterInvocation(token, null);
            }
        }
    
        @Override
        public void destroy() {
    
        }
    
        @Override
        public Class<?> getSecureObjectClass() {
            return FilterInvocation.class;
        }
    
        @Override
        public SecurityMetadataSource obtainSecurityMetadataSource() {
            return this.securityMetadataSource;
        }
    }
    

    其中还涉及到SecurityMetadataSource-当前访问路径的权限获取AccessDecisionManager-授权处理器

    SecurityMetadataSource-当前访问路径的权限获取

    @Component
    public class CasInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
        private final TSysMenuDao tSysMenuDao;
        private final HashSet<Pattern> patterns;
        
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
        
        @Autowired
        public MyInvocationSecurityMetadataSourceService(TSysMenuDao tSysMenuDao,FilterStatic filterStatic) {
            this.tSysMenuDao = tSysMenuDao;
            patterns = new HashSet<>();
            //可通过配置过滤路径,这里就省略不写了,写法与AcmCasProperties一致
            for (String filter:filterStatic.getStaticFilters()){
               String regex= filter.replace("**","*").replace("*",".*");
               patterns.add(Pattern.compile(regex));
            }
        }
    
    
    
        /**
         * 查找url对应的角色
         */
        public  Collection<ConfigAttribute> loadResourceDefine(String url){
            Collection<ConfigAttribute> array=new ArrayList<>();
            ConfigAttribute cfg;
            SysMenu permission = tSysMenuDao.findMeneRoles(url);
            if (permission !=null) {
                for (String role :permission.getRoles().split(",")){
                    cfg = new SecurityConfig(role);
                    //此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为CasAccessDecisionManager类的decide的第三个参数。
                    array.add(cfg);
                }
                return array;
            }
            return null;
    
        }
    
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            //object 中包含用户请求的request 信息
            HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
            String url = request.getRequestURI();
            url = url.replaceFirst(request.getContextPath(), "");
            logger.info(url);
            
            //将请求的url与配置文件中不需要访问控制的url进行匹配
            Iterator<Pattern> patternIterator=patterns.iterator();
            while (patternIterator.hasNext()){
                Pattern pattern = patternIterator.next();
                Matcher matcher=pattern.matcher(url);
                if (matcher.find())
                    return null;
            }
            return loadResourceDefine(url);
        }
    
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }
    

    AccessDecisionManager-授权处理器

    承接上面的SecurityMetadataSource获取到的权限集合configAttributes,此处对此验证

    @Component
    public class CasAccessDecisionManager implements AccessDecisionManager {
    
    	/**
    	 * @param authentication 当前用户权限信息
    	 * @param o 请求信息
    	 * @param configAttributes 当前访问的url对应的角色
    	 */
        @Override
        public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            //没有角色要求则返回
        	if(null== configAttributes || configAttributes.size() <=0) {
                return;
            }
            //比较当前用户角色和当前访问的url对应的角色,是否拥有对应权限
            ConfigAttribute c;
            String needRole;
            for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
                c = iter.next();
                needRole = c.getAttribute();
                for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
                    if((SercurityConstants.prefix+needRole.trim()).equals(ga.getAuthority())) {
                        return;
                    }
                }
            }
            throw new AccessDeniedException("no right");
        }
    
        @Override
        public boolean supports(ConfigAttribute configAttribute) {
            return true;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return true;
        }
    }
    

    总入口配置

    主要是结合spring security进行相应的设置,因为CasAuthenticationFilter需要设置AuthenticationManager对象,所以放在总入口这里配置

    @Configuration
    @EnableWebSecurity
    //如果依赖数据库读取角色等,则需要配置
    @AutoConfigureAfter(MyBatisMapperScannerConfig.class)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    	/**
    	 * 自定义动态权限过滤器
    	 */
    	@Resource
    	private final CasFilterSecurityInterceptor myFilterSecurityInterceptor;
    	
    	@Resource
    	private final FilterStatic filterStatic;
    
    	/**
    	 * 自定义过滤规则及其安全配置
    	 */
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		// HeadersConfigurer
    		http.headers().frameOptions().disable();
    
    		// CsrfConfigurer
    		http.csrf().disable();
    
    		// ExpressionInterceptUrlRegistry
    		http.authorizeRequests().anyRequest().authenticated().anyRequest().fullyAuthenticated();
    
    		// acm cas策略
    		// 对logout请求放行
    		http.logout().permitAll();
    		// 入口
    		CasAuthenticationEntryPoint entryPoint = getApplicationContext().getBean(CasAuthenticationEntryPoint.class);
    		CasAuthenticationFilter casAuthenticationFilter = getApplicationContext()
    					.getBean(CasAuthenticationFilter.class);
    		SingleSignOutFilter singleSignOutFilter = getApplicationContext().getBean(SingleSignOutFilter.class);
    		LogoutFilter logoutFilter = getApplicationContext().getBean(LogoutFilter.class);
    			/**
    			 * 执行顺序为
    			 * LogoutFilter-->SingleSignOutFilter-->CasAuthenticationFilter-->
    			 * ExceptionTranslationFilter
    			 */
    			http.exceptionHandling().authenticationEntryPoint(entryPoint).and().addFilter(casAuthenticationFilter)
    					.addFilterBefore(logoutFilter, LogoutFilter.class)
    					.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class);
    		} 
    		// addFilter
    	http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
    	}
    
    	@Autowired
    	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    			//放入cas凭证校验器
    			AuthenticationProvider authenticationProvider = (AuthenticationProvider) getApplicationContext()
    					.getBean("casProvider");
    			auth.authenticationProvider(authenticationProvider);
    
    	}
    
    	@Override
    	public void configure(WebSecurity web) throws Exception {
    		// 静态文静过滤
    		String[] filter = filterStatic.getStaticFilters().toArray(new String[0]);
    		web.ignoring().antMatchers(filter);
    	}
    
    	/**
    	 * cas filter类
    	 * 
    	 * 针对/login请求的校验
    	 * 
    	 * @return
    	 */
    	@Bean
    	public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties properties,
    			AcmCasProperties acmCasProperties) throws Exception {
    		CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
    		casAuthenticationFilter.setServiceProperties(properties);
    		casAuthenticationFilter.setFilterProcessesUrl(acmCasProperties.getAppServiceLoginUrl());
    		casAuthenticationFilter.setAuthenticationManager(authenticationManager());
    		casAuthenticationFilter
    				.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/index.html"));
    		return casAuthenticationFilter;
    	}
    }
    

    Springboot启动类配置

    @SpringBootApplication
    @ComponentScan(basePackages = {"com.jingsir.springboot.cas"})
    public class Application extends SpringBootServletInitializer implements EmbeddedServletContainerCustomizer {
        public static void main(String[] args) {
            SpringApplication.run(Application.class);
        }
    
        @Override
        public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) {
            configurableEmbeddedServletContainer.setContextPath("/cas-web");
        }
    }
    

    小结

    当时对CasAuthenticationEntryPoint为何配置的service回调路径不可为本应用的login登录路径有疑惑,因为会被提前拦截显示"401错误"。分析wireshark的抓包后得知结论如下

    • 第一次用户GET请求到casServerLoginUrl,返回登录页面
    • 用户输入账号与密码后POST请求到casServerLoginUrl,其会返回TGC,并不返回ticket(所以此处不可为本应用的登录路径),由于FilterSecurityInterceptor校验仍失败,则仍会由ExceptionTranslationFilter发送GET请求转发至cas登录页面
    • 第二次用户GET请求到casServerLoginUrl,cas服务根据TGC会返回Ticket
    • 客户端拿到Ticket后会路由至cas服务上的/cas/serviceValidate上进行Ticket校验,校验通过后则访问真正的路径。且后面每次的请求都会携带Ticket去cas服务上校验,直至Ticket失效后则再次进行登录
      cas-login

    本文都是通过实例操作后所写的博客,建议理解原理之后再可参照实例来编写,不当之处欢迎指出。

  • 相关阅读:
    Hbase安装配置
    HBASE概念补充
    Hbase概述
    qt creator源码全方面分析(2-8)
    qt creator源码全方面分析(2-7)
    qt creator源码全方面分析(2-6)
    qt creator源码全方面分析(2-5)
    qt creator源码全方面分析(2-3)
    qt creator源码全方面分析(2-2)
    qt creator源码全方面分析(2-4)
  • 原文地址:https://www.cnblogs.com/question-sky/p/7068511.html
Copyright © 2011-2022 走看看