一、cas原理分析
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。CAS是一种基于http协议的B/S应用系统单点登录实现方案,认识CAS之前首先要熟悉http协议、Session与Cookie等Web开发基本知识。
1.1 Cas登录:
两次前端跳转、一次后端验证
1.1.1 首次访问应用A
第一次跳转:
客户端访问应用系统A,应用系统判断Session发现未登录,返回302跳转到sso登录页面,并传递service参数给sso,该service参数有两个作用:(回跳、认证)
1、 service一般传递应用系统url地址,用于sso认证通过后回跳到应用系统A;
2、service参数同时会被cas服务端的作为cas客户端的唯一标记记录下来,用于后期匹配相应的认证凭据;
第二次跳转:
浏览器显示登录页面,用户输入账号密码登录成功后,sso会返回302跳转回到原来请求的应用系统页面,并携带ticket参数,作为认证票据,同时通过Set-Cookie向浏览器记录TGT,(TGT的作用将在下一个应用系统需要登录的时候体现出作用,是避免重复登录的关键)
后台进行一次票据验证:
应用系统接收到带有ticket的请求后,从后台直接向sso服务器发起一个http请求,将service和ticket作为参数,用于验证ticket的有效性;如果ticket有效,sso服务器将返回该ticket对应的登录用户名。
图例:
1.1.2 访问A登陆后首次访问应用B
访问应用B系统,根据session判断未登录,重定向到CAS Serve,根据Cookie里面的TGT找到对应的用户信息,携带ticket重定向会应用系统B,应用系统B接收到带有ticket的请求后,从后台直接向sso服务器发起一个http请求,将service和ticket作为参数,用于验证ticket的有效性;如果ticket有效,sso服务器将返回该ticket对应的登录用户名
二、实现cas客户端
yaml配置:
security:
cas:
server:
host: https://localhost:8443/cas #cas服务器的地址
login: ${security.cas.server.host}/login #单点登录地址
logout: ${security.cas.server.host}/logout #单点登出店址
service:
webHost: http://localhost:8090 #应用系统的地址
login: /login #应用系统的登录入口
logout: /logout #应用系统的登出入口
2.1 参数配置类:
- CAS认证中心参数配置类
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
public class CasServerConfig {
@Value("${security.cas.server.host}")
private String host;
@Value("${security.cas.server.login}")
private String login;
@Value("${security.cas.server.logout}")
private String logout;
}
- 应用服务器参数配置类
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
public class CasServiceConfig {
@Value("${security.cas.service.webHost}")
private String webHost;
@Value("${security.cas.service.login}")
private String login;
@Value("${security.cas.service.logout}")
private String logout;
private Boolean sendRenew = false;
}
2.2 CAS配置
- CAS配置类
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
@Configuration
public class SecurityConfiguration {
@Autowired
private CasServerConfig casServerConfig;
@Autowired private CasServiceConfig casServiceConfig;
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService(
this.casServiceConfig.getWebHost() + this.casServiceConfig.getLogin()); //回跳地址
serviceProperties.setSendRenew(this.casServiceConfig.getSendRenew());//是否敏感,即登录不同的应用系统是否还要重新登录
serviceProperties.setAuthenticateAllArtifacts(true); //是否对没有ticket的访问需要验证
return serviceProperties;
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter(
AuthenticationManager authenticationManager, ServiceProperties serviceProperties) {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager);
casAuthenticationFilter.setServiceProperties(serviceProperties);
casAuthenticationFilter.setFilterProcessesUrl(
this.casServiceConfig.getLogin());
casAuthenticationFilter.setContinueChainBeforeSuccessfulAuthentication(false);
casAuthenticationFilter.setAuthenticationSuccessHandler(
new UrlAuthenticationSuccessHandler("/"));
return casAuthenticationFilter;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint(
ServiceProperties serviceProperties) {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(this.casServerConfig.getLogin());
entryPoint.setServiceProperties(serviceProperties);
return entryPoint;
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(this.casServerConfig.getHost());
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider(
AuthenticationUserDetailsService userDetailsService,
ServiceProperties serviceProperties,
Cas20ServiceTicketValidator ticketValidator) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setKey("casProvider");
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setAuthenticationUserDetailsService(userDetailsService);//自己的userDetailService
return provider;
}
@Bean
public LogoutFilter logoutFilter() {
String logoutRedirectPath =
this.casServerConfig.getLogout() + "?service=" + this.casServiceConfig.getWebHost();
LogoutFilter logoutFilter =
new LogoutFilter(logoutRedirectPath, new CasSecurityContextLogoutHandler());
logoutFilter.setFilterProcessesUrl(this.casServiceConfig.getLogout());
return logoutFilter;
}
}
- 认证成功处理类
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collection;
@Slf4j(topic = "c.successHandler")
public class UrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public UrlAuthenticationSuccessHandler() {
super();
}
public UrlAuthenticationSuccessHandler(String defaultTargetUrl) {
super(defaultTargetUrl);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
}
@Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
StringBuffer targetUrl = null;
//根据自己需求写
HttpSession session = request.getSession();
targetUrl = (StringBuffer) session.getAttribute("REQUEST_URL");
log.debug("目标路径为{}", targetUrl);
return targetUrl.toString();
}
}
- 自定义过滤器(存储要访问的路径)
mport com.hzx.hzxy_grid.domain.SecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collection;
@Component
@Slf4j
public class HttpParamsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpSession session = request.getSession();
StringBuffer requestURL = request.getRequestURL();
log.info("请求地址:" + requestURL);
chain.doFilter(request, response);
}
@Override
public void destroy() {}
}
- 单点登出过滤器
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class CasSecurityContextLogoutHandler implements LogoutHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private boolean invalidateHttpSession = true;
private boolean clearAuthentication = true;
public CasSecurityContextLogoutHandler() {
}
public void logout(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (this.invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
this.logger.debug("Invalidating session: " + session.getId());
//发布退出登录事件,自己增加,监听此事件处理一些事情
session.invalidate(); //session失效
// LogoutEvent event = new LogoutEvent();
// event.setSessionId(session.getId());
// EventPublisherUtil.publish(event);
}
}
if (this.clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication((Authentication)null);
}
SecurityContextHolder.clearContext();
}
public boolean isInvalidateHttpSession() {
return this.invalidateHttpSession;
}
public void setInvalidateHttpSession(boolean invalidateHttpSession) {
this.invalidateHttpSession = invalidateHttpSession;
}
public void setClearAuthentication(boolean clearAuthentication) {
this.clearAuthentication = clearAuthentication;
}
}
- springsecurity配置类
import com.hzx.hzxy_grid.config.custom.CustomFilter;
import com.hzx.hzxy_grid.config.custom.CustomUrlDecisionManager;
import com.hzx.hzxy_grid.handler.CustomizeAccessDeniedHandler;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsUtils;
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
public class CasWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CasAuthenticationEntryPoint casAuthenticationEntryPoint;
@Autowired
private CasAuthenticationProvider casAuthenticationProvider;
@Autowired
private CasAuthenticationFilter casAuthenticationFilter;
@Autowired
private LogoutFilter logoutFilter;
@Autowired
private CasServerConfig casServerConfig;
@Autowired
private HttpParamsFilter httpParamsFilter;
@Autowired
private CustomFilter customFilter;
@Autowired
private CustomUrlDecisionManager customUrlDecisionManager;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/swagger-resources
public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
filterSecurityInterceptor.setSecurityMetadataSource(customFilter);
filterSecurityInterceptor.setAuthenticationManager(authenticationManager());
filterSecurityInterceptor.setAccessDecisionManager(customUrlDecisionManager);
return filterSecurityInterceptor;
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider);
}
@Bean
public ServletListenerRegistrationBean
singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean servletListenerRegistrationBean =
new ServletListenerRegistrationBean<>();
servletListenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
return servletListenerRegistrationBean;
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- CustomFilter类
@Component
@Slf4j(topic = "c.CustomUrl")
public class CustomFilter implements FilterInvocationSecurityMetadataSource {
//路径匹配
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private MenuService menuService;
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
// 获取请求的Url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List
- CustomDecisionManager
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("用户未登录,请登录");
} else
return;
}
//判断是否有对应的权限信息
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if(authority.getAuthority().equals(needRole))
return;
}
}
throw new AccessDeniedException("用户无权限访问");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class> aClass) {
return false;
}
}
- 未授权无法访问CustomizeAccessDeniedHandler
import com.hzx.hzxy_grid.domain.R;
import com.hzx.hzxy_grid.enums.Status;
import com.hzx.hzxy_grid.util.ResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse,R.error().status(Status.UNAUTHORIZED));
}
}
- AuthenticationUserDetailsService
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.hzx.hzxy_grid.domain.Admin; import com.hzx.hzxy_grid.domain.Role; import com.hzx.hzxy_grid.domain.SecurityUser; import com.hzx.hzxy_grid.mapper.AdminMapper; import com.hzx.hzxy_grid.mapper.RoleMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Slf4j(topic = "c.UserDetailServiceImpl") @Service public class MyUserDetailServiceImpl implements AuthenticationUserDetailsService{ @Autowired private RoleMapper roleMapper; @Autowired private AdminMapper adminMapper; @Autowired private RedisTemplate redisTemplate; @Override public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { String username = token.getName(); Admin admin = adminMapper.selectOne(new LambdaQueryWrapper ().eq(Admin::getUsername, username)); log.info("admin:{}", admin); if (admin == null) throw new UsernameNotFoundException("用户名不存在"); SecurityUser securityUser = new SecurityUser(); List permissionList = null; //如果redis不存在该用户的权限 if (!redisTemplate.hasKey("permission_" + admin.getUid())) { //添加权限 List roleList = roleMapper.getRolesByAdminId(admin.getUid()); permissionList = roleList.stream().map(role -> role.getRoleName()).collect(Collectors.toList()); securityUser.setPermissionValueList(permissionList); securityUser.setCurrentUserInfo(admin); log.info("从数据库中查询到用户的权限为{}", permissionList); } else { permissionList = (List ) redisTemplate.opsForValue().get("permission_" + admin.getUid()); log.info("从redis中查询到用户的权限为{}",permissionList); } securityUser.setPermissionValueList(permissionList); securityUser.setCurrentUserInfo(admin); log.info("用户的权限为{}", permissionList); return securityUser; } }
- SecurityUser
import lombok.Data;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
public class SecurityUser implements UserDetails, CredentialsContainer {
//当前登录用户
private transient Admin currentUserInfo;
//当前权限
private List permissionValueList;
public SecurityUser() {
}
public SecurityUser(Admin admin) {
if (admin != null) {
this.currentUserInfo = admin;
}
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void eraseCredentials() {
}
}



