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
流程

添加引用
添加相关的项目引用,这里省略了其他的引用,只列出了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() { // 默认构造函数 } // ... }
至此,我们的集成就结束了。