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() {
// 默认构造函数
}
// ...
}
至此,我们的集成就结束了。