Spring Security 集成CAS实现SSO登录

CAS是一种单点登录SSO的实现,由于最近要接入集团项目的SSO登录,因此对CAS做了点研究。CAS分为Server端和Client端,Client是我们自己的应用,我们的目的是在Spring Security安全框架下整合CAS Cliend端,访问已有的CAS Server来实现SSO登录及登出的功能。网上关于CAS的集成有很多内容,这里只做了一个整理及应用,主要代码来自与参考资料。

项目环境:
1. SpringBoot
2. Spring Security
3. Spring Security-CAS

流程

CAS

添加引用

添加相关的项目引用,这里省略了其他的引用,只列出了Spring-Security以及Spring-Security-CAS的引用。

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

新建application.properties

配置我们的CAS Server地址以及我们的应用程序地址,新增了一个application.properties文件。

#CAS服务地址
server.host.url= http://192.168.1.100/opcnet-sso
#CAS服务登录地址
server.host.login_url=${server.host.url}/login?appid=eqms
#CAS服务登出地址
server.host.logout_url=${server.host.url}/logout?service=${app.server.host.url}?appid=eqms
#应用访问地址
app.server.host.url=http://localhost:8090
#应用登录地址
app.login.url=/login
#应用登出地址
app.logout.url=/logout

这里将配置文件中配置的参数注入到我们自定义的CasProperties中,方便使用。

@Data
@Component
public class CasProperties {
    /** 
    * CAS的配置参数 
    * @author ChengLi 
    */
    @Value("${server.host.url}")  
    private String casServerUrl;  
  
    @Value("${server.host.login_url}")  
    private String casServerLoginUrl;  
  
    @Value("${server.host.logout_url}")  
    private String casServerLogoutUrl;  
  
    @Value("${app.server.host.url}")  
    private String appServerUrl;  
  
    @Value("${app.login.url}")  
    private String appLoginUrl;  
  
    @Value("${app.logout.url}")  
    private String appLogoutUrl; 
}

配置Spring Security及CAS

这里主要用于Spring Security以及CAS的配置。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private CasProperties casProperties; 
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(casAuthenticationProvider());
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()//配置安全策略
			.anyRequest().authenticated()//其余的所有请求都需要验证
			.and()
			.csrf().disable()
			.logout()
			.permitAll()
			.and().headers().frameOptions().disable()  //定义logout不需要验证
			.and()
		.formLogin();//使用form表单登录
		
		http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint())
			.and()
			.addFilter(casAuthenticationFilter())
			.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
			.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
	}
	
     /**
     * 认证的入口,即跳转至服务端的cas地址
     * Note:浏览器访问不可直接填客户端的login请求,若如此则会返回Error页面,无法被此入口拦截
     */
    @Bean  
    public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {  
        CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();  
        //Cas Server的登录地址
        casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
        //service相关的属性
        casAuthenticationEntryPoint.setServiceProperties(serviceProperties());  
        return casAuthenticationEntryPoint;  
    }  
      
    /**
     * 指定service相关信息
     * 设置客户端service的属性
     * 主要设置请求cas服务端后的回调路径,一般为主页地址,不可为登录地址
     * @return
     */
    @Bean  
    public ServiceProperties serviceProperties() {  
        ServiceProperties serviceProperties = new ServiceProperties();  
        // 设置回调的service路径,此为主页路径
        //Cas Server认证成功后的跳转地址,这里要跳转到我们的Spring Security应用,
        //之后会由CasAuthenticationFilter处理,默认处理地址为/j_spring_cas_security_check
        serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
        // 对所有的未拥有ticket的访问均需要验证
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;  
    }  
      
    /**CAS认证过滤器*/  
    @Bean  
    public CasAuthenticationFilter casAuthenticationFilter() throws Exception {  
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();  
        casAuthenticationFilter.setAuthenticationManager(authenticationManager());  
        //指定处理地址,不指定时默认将会是“/j_spring_cas_security_check” 
        casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());  
        return casAuthenticationFilter;  
    }  
      
    /**
     * 创建CAS校验类
     * Notes:TicketValidator、AuthenticationUserDetailService属性必须设置;
     * serviceProperties属性主要应用于ticketValidator用于去cas服务端检验ticket
     * @return
     */
    @Bean  
    public CasAuthenticationProvider casAuthenticationProvider() {  
    	CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
    	casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
    	casAuthenticationProvider.setServiceProperties(serviceProperties());
    	casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
    	casAuthenticationProvider.setKey("casAuthenticationProviderKey");
    	return casAuthenticationProvider;
    }

    /**用户自定义的AuthenticationUserDetailsService*/  
    @Bean  
    public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService(){  
        return new CustomUserDetailsService();  
    }
    
    /**
     * 配置Ticket校验器
     * @return
     */
    @Bean  
    public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {  
        // 配置上服务端的校验ticket地址
        return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());  
    }  
      
    /**
     * 单点注销,接受cas服务端发出的注销session请求
     * @see SingleLogout(SLO) Front or Back Channel
     * @return
     */
    @Bean  
    public SingleSignOutFilter singleSignOutFilter() {  
        SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();  
        singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());  
        singleSignOutFilter.setIgnoreInitConfiguration(true);  
        return singleSignOutFilter;  
    }  
      
    /**
     * 单点请求CAS客户端退出Filter类
     * 请求/logout,转发至CAS服务端进行注销
     */
    @Bean  
    public LogoutFilter casLogoutFilter() {  
        // 设置回调地址,以免注销后页面不再跳转
        LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());  
        logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());  
        return logoutFilter;  
    }  
}

加载用户信息

在这里,我们通过Ticket验证后响应的accoun来获取用户信息。这里需要实现UserDetailsService接口,或实现AuthenticationUserDetailsService接口

public class CustomUserDetailsService implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {  
    private Logger LOGGER = LoggerFactory.getLogger(StoryUserDetailsService.class);
	
    @Autowired
    private UserService userService;
	
    @Override  
    public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException {  
        UserAuthRespDTO loginUserVO = null;
        String account =token.getName();
 	try {
 	    //根据用户账号查询用户信息
 	    loginUserVO = userService.loginAccount(account);
 	    if(loginUserVO == null) {
 		throw new UsernameNotFoundException("用户名["+account+"]不存在!");
 	    }
 	} catch (StoryServiceException e) {
 	    LOGGER.error("loadUserByUsername error, Account:{}", account, e);
 	    throw new UsernameNotFoundException("用户名["+account+"]认证失败!", e);
 	}
 	return convertToStoryPrincipal(loginUserVO);
    }

    private StoryPrincipal convertToStoryPrincipal(UserAuthRespDTO loginUserVO) {
	StoryLoginUser storyLoginUser = new StoryLoginUser();
	BeanUtils.copyProperties(loginUserVO, storyLoginUser);
	return new StoryPrincipal(storyLoginUser, loginUserVO.getPassword());
    }
}

自定义实体类,包含了我们的用户信息,以及权限信息等,需要实现继承UserDetails

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import com.google.common.collect.Lists;
/**
 * 认证实体类
 */
public class StoryPrincipal implements UserDetails {
	private static final long serialVersionUID = 1L;
	private StoryLoginUser storyLoginUser;
	private String password;

	public StoryPrincipal() {
		// TODO Auto-generated constructor stub
	}

	public StoryPrincipal(StoryLoginUser spiritLoginUser, String password) {
		this.spiritLoginUser = spiritLoginUser;
		this.password = password;
	}

	/**
	 * 当前认证实体的所有权限
	 */
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<GrantedAuthority> grantedAuthoritys = Lists.newLinkedList();
		Set<String> authoritys = this.getStoryLoginUser().getAuthoritys();
		if (!CollectionUtils.isEmpty(authoritys)) {
			grantedAuthoritys = AuthorityUtils.createAuthorityList(authoritys.toArray(new String[authoritys.size()]));
		}
		return grantedAuthoritys;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return spiritLoginUser.getAccount();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

	public StoryLoginUser getStoryLoginUser() {
		return spiritLoginUser;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (this.getClass() == obj.getClass()) {
			return getUsername().equals(((StoryPrincipal) obj).getUsername());
		}
		return false;
	}

	@Override
	public int hashCode() {
		return getUsername().hashCode();
	}

	@Override
	public String toString() {
		return "StoryPrincipal [spiritLoginUser=" + spiritLoginUser + "]";
	}
}

这里是我们登录后用于存放用户登录信息的类,可以根据实际需要来定义。

/**
 * 当前登录用户
 */
public class StoryLoginUser implements Serializable {
	private static final long serialVersionUID = -5339236104490631398L;
	private Long id;
	private String account;
	private String name;
	private String email;
	private String avatar;
	private String status;
	private Set<String> authoritys;
	public StoryLoginUser() {
		// 默认构造函数
	}
	// ...
}

至此,我们的集成就结束了。

参考资料

  1. 玩转Spring Boot 使用Spring security 集成CAS

发表评论

电子邮件地址不会被公开。 必填项已用*标注