一、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;
}
/***
* CAS认证过滤器
* @param authenticationManager
* @param serviceProperties
* @return
*/
@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;
}
/**
* CAS入口
* @param serviceProperties
* @return
*/
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint(
ServiceProperties serviceProperties) {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(this.casServerConfig.getLogin());
entryPoint.setServiceProperties(serviceProperties);
return entryPoint;
}
/**
* CASticket验证
* @return
*/
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator(this.casServerConfig.getHost());
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider(
AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService,
ServiceProperties serviceProperties,
Cas20ServiceTicketValidator ticketValidator) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setKey("casProvider");
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setAuthenticationUserDetailsService(userDetailsService);//自己的userDetailService
return provider;
}
/**
* 登出过滤器
* @return
*/
@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);
}
/**
* 认证成功后
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
}
/**
*返回一个认证成功后的路径
* @param request
* @param response
* @return
*/
@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/**",
"/swagger-ui.html",
"/v2/api-docs",
"/webjars/**",
"/v3/api-docs",
// "/login",
"/static/**",
"/api/**"
);
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.csrf().disable();
http.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest)
.permitAll()
.anyRequest()
.authenticated(); // 所有资源都需要登陆后才可以访问。
http.logout().permitAll(); // 不拦截注销
http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint)
.accessDeniedHandler(new CustomizeAccessDeniedHandler())
;
// 单点注销的过滤器,必须配置在SpringSecurity的过滤器链中,如果直接配置在Web容器中,貌似是不起作用的。我自己的是不起作用的。
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setArtifactParameterName(this.casServerConfig.getHost());
http.addFilter(casAuthenticationFilter)
.addFilterBefore(logoutFilter, LogoutFilter.class)
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class);
http.addFilterBefore(httpParamsFilter, FilterSecurityInterceptor.class);
http.addFilterBefore(filterSecurityInterceptor(), FilterSecurityInterceptor.class);
http.antMatcher("/**");
}
/**
* 权限拦截
*
* @return
* @throws Exception
*/
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>
singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> servletListenerRegistrationBean =
new ServletListenerRegistrationBean<>();
servletListenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
return servletListenerRegistrationBean;
}
/**
* AuthenticationManager
*
* @return
* @throws Exception
*/
@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<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
// 获取请求的Url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> menuWithRoles = menuService.getMenuWithRoles();
for (Menu menu : menuWithRoles) {
//访问的路径是否跟菜单的里的路径相同
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
String[] roles = menu.getRoleList().stream().map(Role::getRoleName).toArray(String[]::new);
log.info("需要的权限为{}", roles);
System.out.println(roles);
return SecurityConfig.createList(roles);
}
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
- CustomDecisionManager
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> 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<CasAssertionAuthenticationToken> {
@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<Admin>().eq(Admin::getUsername, username));
log.info("admin:{}", admin);
if (admin == null)
throw new UsernameNotFoundException("用户名不存在");
SecurityUser securityUser = new SecurityUser();
List<String> permissionList = null;
//如果redis不存在该用户的权限
if (!redisTemplate.hasKey("permission_" + admin.getUid())) {
//添加权限
List<Role> 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<String>) 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<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(Admin admin) {
if (admin != null) {
this.currentUserInfo = admin;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> 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() {
}
}