spring security集成cas实现单点登录过程

cas流程

如下

用户发送请求,后台服务器验证ticket(票据信息),未登录时,用户没有携带,所以验证失败,将用户重定向到cas服务器去登录换取票据凭证

用户获取凭证后携带凭证进行请求,后台服务器拿着ticket票据信息去cas服务器验证,验证成功后并获取用户信息,后台服务器再将获取到的用户信息返回给浏览器

下图,路线:1->5->6; 1->2->3->4; 1->2->7->8->9->1;

spring security集成cas实现单点登录过程_第1张图片

下面代码解决的问题

1.完成第三方客户的cas登录,

2.并将用户信息同步到自己数据库,

3.同时将登录用户的凭证转换成自己系统识别的凭证返回给自己系统的前端使用

具体代码使用

pom.xml

 
    
      org.springframework.boot
      spring-boot-starter-security
    
    
    
      org.springframework.security
      spring-security-cas
    

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
      singleSignOutHttpSessionListener() {
    ServletListenerRegistrationBean 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 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 parameterMap = request.getParameterMap();
    for (Entry 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 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 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 getAllPermission() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    Collection authorities = authentication.getAuthorities();
    return authorities;
  }

  public static boolean hasPermission(String permission) {
    if (StringUtils.isEmpty(permission)) {
      return false;
    }
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    Collection 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 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 authorities) {
    this(username, password, true, true, true, true, authorities);
  }

  public User(String username, String password, boolean enabled, boolean accountNonExpired,
      boolean credentialsNonExpired, boolean accountNonLocked,
      Collection 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 grantedAuthorities) {
    this.authorities = Collections.unmodifiableSet(sortAuthorities(grantedAuthorities));
  }

  public Collection 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 sortAuthorities(
      Collection authorities) {
    Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
    SortedSet 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 authorities;
    private boolean accountExpired;
    private boolean accountLocked;
    private boolean credentialsExpired;
    private boolean disabled;
    private Function 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 encoder) {
      Assert.notNull(encoder, "encoder cannot be null");
      this.passwordEncoder = encoder;
      return this;
    }

    public UserBuilder roles(String... roles) {
      List 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 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, 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 sessionTokenMap = new ConcurrentHashMap<>();

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

你可能感兴趣的:(spring security集成cas实现单点登录过程)