spring security集成cas实现单点登录过程
作者:阿文_ing
这篇文章主要介绍了spring security集成cas实现单点登录过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
cas流程
如下
用户发送请求,后台服务器验证ticket(票据信息),未登录时,用户没有携带,所以验证失败,将用户重定向到cas服务器去登录换取票据凭证
用户获取凭证后携带凭证进行请求,后台服务器拿着ticket票据信息去cas服务器验证,验证成功后并获取用户信息,后台服务器再将获取到的用户信息返回给浏览器
下图,路线:1->5->6; 1->2->3->4; 1->2->7->8->9->1;
下面代码解决的问题
1.完成第三方客户的cas登录,
2.并将用户信息同步到自己数据库,
3.同时将登录用户的凭证转换成自己系统识别的凭证返回给自己系统的前端使用
具体代码使用
pom.xml
<!-- spring security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- cas --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId> </dependency>
application.yml
security: cas: server: host: https://test-login.xx.com login: ${security.cas.server.host}/login logout: ${security.cas.server.host}/logout service: webHost: http://test-xx.xx.com login: /login/cas logout: /logout
CasServerConfig CAS服务器配置
@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; }
CasServiceConfig 项目后台服务器配置
@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; }
CasWebSecurityConfiguration配置
@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; private static final String[] AUTH_WHITELIST = { // -- swagger ui "/swagger-resources/**", "/swagger-ui.html", "/v2/api-docs", "/webjars/**", "/v3/api-docs" }; @Override protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable(); http.csrf().disable(); http.authorizeRequests() .requestMatchers(CorsUtils::isPreFlightRequest) .permitAll() .antMatchers("/static/**") .permitAll() .antMatchers(AUTH_WHITELIST) .permitAll() // 不拦截静态资源 // .antMatchers("/config/**").permitAll() // 不拦截动态配置文件 .antMatchers("/api/**") .permitAll() // 不拦截对外API .antMatchers("/login/**") .permitAll() .anyRequest() .authenticated(); // 所有资源都需要登陆后才可以访问。 http.logout().permitAll(); // 不拦截注销 http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint); // 单点注销的过滤器,必须配置在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.antMatcher("/**"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(casAuthenticationProvider); } @Bean public ServletListenerRegistrationBean<CasSignOutHttpSessionListener> singleSignOutHttpSessionListener() { ServletListenerRegistrationBean<CasSignOutHttpSessionListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>(); servletListenerRegistrationBean.setListener(new CasSignOutHttpSessionListener()); return servletListenerRegistrationBean; } /** * 自定义UserDetailsService,授权 * * @return */ @Bean public CustomUserDetailsService customUserDetailsService() { return new CustomUserDetailsService(); } /** * AuthenticationManager * * @return * @throws Exception */ @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
@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()); 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<CasAssertionAuthenticationToken> userDetailsService, ServiceProperties serviceProperties, Cas20ServiceTicketValidator ticketValidator) { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setKey("casProvider"); provider.setServiceProperties(serviceProperties); provider.setTicketValidator(ticketValidator); provider.setAuthenticationUserDetailsService(userDetailsService); return provider; } @Bean public LogoutFilter logoutFilter() { String logoutRedirectPath = this.casServerConfig.getLogout() + "?service=" + this.casServiceConfig.getWebHost(); CasLogoutFilter logoutFilter = new CasLogoutFilter(logoutRedirectPath, new CasSecurityContextLogoutHandler()); logoutFilter.setFilterProcessesUrl(this.casServiceConfig.getLogout()); return logoutFilter; } }
验证成功后处理器UrlAuthenticationSuccessHandler
public class UrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public UrlAuthenticationSuccessHandler() { super(); } public UrlAuthenticationSuccessHandler(String defaultTargetUrl) { super(defaultTargetUrl); } @Override protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) { if (isAlwaysUseDefaultTargetUrl()) { return this.getDefaultTargetUrl(); } // Check for the parameter and use that if available String targetUrl = null; if (this.getTargetUrlParameter() != null) { targetUrl = request.getParameter(this.getTargetUrlParameter()); if (StringUtils.hasText(targetUrl)) { logger.debug("Found targetUrlParameter in request: " + targetUrl); return targetUrl; } } //根据自己需求写 if (!StringUtils.hasText(targetUrl)) { HttpSession session = request.getSession(); targetUrl = (String) session.getAttribute(HttpParamsFilter.REQUESTED_URL); int firstUrl = targetUrl.indexOf("?url"); if (firstUrl > 0) { targetUrl = targetUrl.substring(firstUrl + 5); } int secondUrl = targetUrl.indexOf("?url"); if (firstUrl >= 0 && secondUrl >= 0 && firstUrl != secondUrl) targetUrl = targetUrl.substring(secondUrl + 5); } if (!StringUtils.hasText(targetUrl)) { targetUrl = this.getDefaultTargetUrl(); logger.debug("Using default Url: " + targetUrl); } //验证成功后,请求后面的ticket信息去除掉 if (targetUrl.contains("ticket")) { targetUrl = targetUrl.substring(0, targetUrl.indexOf("?ticket")); } //根据自己需求进行判断 if (targetUrl.contains("#")) { targetUrl += targetUrl.substring(targetUrl.indexOf("#")); } return targetUrl; } }
退出登录处理器 CasSecurityContextLogoutHandler
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(); 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; } }
增加自己拦截器,处理自己的逻辑HttpParamsFilter
@Component @Slf4j public class HttpParamsFilter implements Filter { public static final String REQUESTED_URL = "CasRequestedUrl"; @Autowired private CasServiceConfig casServiceConfig; //下面三个依赖我这里主要做了凭证转换,可删掉或替换成自己的逻辑 //我自己的服务,可替代成自己的或者删除掉 @Autowired private TenantTokenService tenantTokenService; //我自己的服务,可替代成自己的,或者删除 @Autowired private TenantManager tenantManager; //我自己的服务,可替代成自己的,或者删除 @Autowired private UserManager userManager; @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(); // String requestPath = request.getServletPath();+ "/" +requestPath StringBuffer requestURL = request.getRequestURL(); log.info("请求地址:" + requestURL); Cookie[] cookies = request.getCookies(); boolean flag = true; if (cookies != null) { for (Cookie cookie : cookies) { log.info("cookieDomain:" + cookie.getDomain()); // && requestURL.toString().contains(cookie.getDomain()) if (cookie.getName().contains("token")) { String value = cookie.getValue(); log.info("token:" + cookie.getDomain()); if (cookie.getDomain() != null && requestURL.toString().contains(cookie.getDomain())) { //缓存凭证使用的 TenantManager.sessionTokenMap.put(session.getId(), value); flag = false; } } // && requestURL.toString().contains(cookie.getDomain()) if (cookie.getName().contains("pcpopclub")) { String value = cookie.getValue(); log.info("pcpopclub:" + cookie.getDomain()); if (cookie.getDomain() != null && requestURL.toString().contains(cookie.getDomain())) { //缓存凭证使用的 TenantManager.sessionTokenMap.put(session.getId(), value); flag = false; } } } } Map<String, String[]> parameterMap = request.getParameterMap(); for (Entry<String, String[]> params : parameterMap.entrySet()) { session.setAttribute(params.getKey(), params.getValue()); requestURL.append("?").append(params.getKey()).append("=").append(params.getValue()[0]); } //cas服务器获取用户信息 User user = SecurityUtils.getUser(); session.setAttribute(REQUESTED_URL, requestURL.toString()); if (user != null) { //查询本地服务是否有此用户 AppUser appUser = tenantManager.getUserByUserCode(user.getUser_code()); // 都是自己加的逻辑,不需要可以删除掉 if (appUser == null) { // 本地服务不存在该用户,调用远程接口查询用户信息,并添加到本地服务 UserQry qry = new UserQry(); qry.setIds(user.getUcid()); UserInfoResponse userBatchInfo = userManager.getUserInfo(qry); List<UserDTO> data = userBatchInfo.getData(); if (ListUtils.isNotEmpty(data)) { tenantManager.addUser(BeanUtils.copy(data.get(0), UserDTO.class)); appUser = tenantManager.getUserByUserCode(user.getUser_code()); } } String token = TenantManager.sessionTokenMap.get(session.getId()); if (token == null) { UserTokenVO userTokenVO = new UserTokenVO(); userTokenVO.setPayload(appUser.getId()); SingleResponse<String> generate = tenantTokenService.generate(userTokenVO); TenantManager.sessionTokenMap.put(session.getId(), generate.getData()); } } else if (flag) { session.setAttribute(REQUESTED_URL, casServiceConfig.getWebHost() + requestURL); } if (user == null) { TenantManager.sessionTokenMap.remove(session.getId()); } else { Cookie ck = new Cookie("homeworld_front_ke_token", user.getBusinessToken()); ck.setDomain( casServiceConfig .getWebHost() .substring(casServiceConfig.getWebHost().indexOf("://") + 3)); response.addCookie(ck); } chain.doFilter(request, response); } @Override public void destroy() {} }
附上获取用户的类 SecurityUtils
public class SecurityUtils { public static Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); } public static Collection<? extends GrantedAuthority> getAllPermission() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return authorities; } public static boolean hasPermission(String permission) { if (StringUtils.isEmpty(permission)) { return false; } Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); boolean hasPermission = false; for (GrantedAuthority grantedAuthority : authorities) { String authority = grantedAuthority.getAuthority(); if (authority.equals(permission)) { hasPermission = true; } } return hasPermission; } public static User getUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); if ("anonymousUser".equals(principal)) { return null; } return (User)principal; } public static void logout() { SecurityContextHolder.clearContext(); } }
附上用户类 User
@Data public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = 530L; private static final Log logger = LogFactory .getLog(org.springframework.security.core.userdetails.User.class); private String password; private String username; private Set<GrantedAuthority> authorities; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; private String birthday; private String accountSystemId; private String gender; private String displayName; private String businessToken; private String avatar; private String passedMfaReason; private String version; private String tgtId; private String realName; private String uid; private String ucname; private String user_code; private String phone; private String mfaAuthMethodLevel; private String id; private String state; private String account; private String email; private String ucid; private String serviceUrl; public User(){ } public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (username != null && !"".equals(username) && password != null) { this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } else { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } } public void setAuthorities(List<GrantedAuthority> grantedAuthorities) { this.authorities = Collections.unmodifiableSet(sortAuthorities(grantedAuthorities)); } public Collection<GrantedAuthority> getAuthorities() { return this.authorities; } public String getPassword() { return this.password; } public String getUsername() { return this.username; } public boolean isEnabled() { return this.enabled; } public boolean isAccountNonExpired() { return this.accountNonExpired; } public boolean isAccountNonLocked() { return this.accountNonLocked; } public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } public void eraseCredentials() { this.password = null; } private static SortedSet<GrantedAuthority> sortAuthorities( Collection<? extends GrantedAuthority> authorities) { Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection"); SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new AuthorityComparator()); Iterator var2 = authorities.iterator(); while (var2.hasNext()) { GrantedAuthority grantedAuthority = (GrantedAuthority) var2.next(); Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements"); sortedAuthorities.add(grantedAuthority); } return sortedAuthorities; } public boolean equals(Object rhs) { return rhs instanceof User ? this.username.equals(((User) rhs).username) : false; } public int hashCode() { return this.username.hashCode(); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append(": "); sb.append("Username: ").append(this.username).append("; "); sb.append("Password: [PROTECTED]; "); sb.append("Enabled: ").append(this.enabled).append("; "); sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; "); sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired).append("; "); sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; "); if (!this.authorities.isEmpty()) { sb.append("Granted Authorities: "); boolean first = true; Iterator var3 = this.authorities.iterator(); while (var3.hasNext()) { GrantedAuthority auth = (GrantedAuthority) var3.next(); if (!first) { sb.append(","); } first = false; sb.append(auth); } } else { sb.append("Not granted any authorities"); } return sb.toString(); } public static UserBuilder withUsername(String username) { return builder().username(username); } public static User.UserBuilder builder() { return new User.UserBuilder(); } /** * @deprecated */ @Deprecated public static User.UserBuilder withDefaultPasswordEncoder() { logger.warn( "User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications."); PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); User.UserBuilder var10000 = builder(); encoder.getClass(); return var10000.passwordEncoder(encoder::encode); } public static User.UserBuilder withUserDetails(UserDetails userDetails) { return withUsername(userDetails.getUsername()).password(userDetails.getPassword()) .accountExpired(!userDetails.isAccountNonExpired()) .accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()) .credentialsExpired(!userDetails.isCredentialsNonExpired()) .disabled(!userDetails.isEnabled()); } public static class UserBuilder { private String username; private String password; private List<GrantedAuthority> authorities; private boolean accountExpired; private boolean accountLocked; private boolean credentialsExpired; private boolean disabled; private Function<String, String> passwordEncoder; private UserBuilder() { this.passwordEncoder = (password) -> { return password; }; } public UserBuilder username(String username) { Assert.notNull(username, "username cannot be null"); this.username = username; return this; } public UserBuilder password(String password) { Assert.notNull(password, "password cannot be null"); this.password = password; return this; } public UserBuilder passwordEncoder(Function<String, String> encoder) { Assert.notNull(encoder, "encoder cannot be null"); this.passwordEncoder = encoder; return this; } public UserBuilder roles(String... roles) { List<GrantedAuthority> authorities = new ArrayList(roles.length); String[] var3 = roles; int var4 = roles.length; for (int var5 = 0; var5 < var4; ++var5) { String role = var3[var5]; Assert.isTrue(!role.startsWith("ROLE_"), () -> { return role + " cannot start with ROLE_ (it is automatically added)"; }); authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } return this.authorities((Collection) authorities); } public UserBuilder authorities(GrantedAuthority... authorities) { return this.authorities((Collection) Arrays.asList(authorities)); } public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = new ArrayList(authorities); return this; } public UserBuilder authorities(String... authorities) { return this.authorities((Collection) AuthorityUtils.createAuthorityList(authorities)); } public UserBuilder accountExpired(boolean accountExpired) { this.accountExpired = accountExpired; return this; } public UserBuilder accountLocked(boolean accountLocked) { this.accountLocked = accountLocked; return this; } public UserBuilder credentialsExpired(boolean credentialsExpired) { this.credentialsExpired = credentialsExpired; return this; } public UserBuilder disabled(boolean disabled) { this.disabled = disabled; return this; } public UserDetails build() { String encodedPassword = (String) this.passwordEncoder.apply(this.password); return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities); } } private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable { private static final long serialVersionUID = 530L; private AuthorityComparator() { } public int compare(GrantedAuthority g1, GrantedAuthority g2) { if (g2.getAuthority() == null) { return -1; } else { return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority()); } } } }
存凭证的容器
public static Map<String, String> sessionTokenMap = new ConcurrentHashMap<>();
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。