Spring Security 集成CAS实现SSO登录
By: Date: 2018年3月26日 Categories: 程序 标签:, , ,

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

发表回复

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