利用pac4j的封装,实现自定义cas校验ST、集成jwt

利用pac4j-cas的封装,实现自定义cas校验ST、集成jwt

  • 关于pac4j-cas
  • pac4j-cas和shiro
  • 正题,代码验证ST
    • 代码

注意↓↓↓↓

其实直接调用 http://cas服务/serviceValidate?service=xxx&ticket=xxx就能校验和获取用户信息了
这里将pac4j-cas的代码拷出来改, 也是方便理解它做了哪些事情.

关于pac4j-cas

这几天一直在折腾pac4j-cas和cas的集成. 也大概了解了一下它的运作机制。

  1. 首先我们配置了一堆关于cas,cas-client的相关信息
  2. 我这边观察到的pac4j-cas两个关键的过滤器,io.buji.pac4j.filter.SecurityFilterio.buji.pac4j.filter.CallbackFilter, SecurityFilter负责对登录认证判断,重定向到cas服务的login页面等,CallbackFilter则主要是负责在cas登录完成后,cas回调客户端,校验cas颁发的ST等.

pac4j-cas和shiro

大概的流程如下

  1. 访问客户端服务的请求, 根据shiro配置ShiroCasConfiguration.factory中设置的过滤器匹配规则,会经过pac4j的SecurityFilter, 进而到org.pac4j.core.engine.DefaultSecurityLogic.DefaultSecurityLogic.perform()中判断如果没有session, 会根据配置好的cas服务地址, 重定向302去跳转到cas登录页面,
    同时携带自身服务/callback的地址信息: http://cas登录地址?service=http://localhost:8080/callback?client_name=client_user
  2. cas认证中心登录成功, 生成cas会话的TGC的cookie信息, 同时根据callback中的地址信息, 重新返回请求本服务, 并且携带cas发放的ticket信息,返回302给浏览器重定向 http://localhost:8080/callback?client_name=client_user&ticket=123
  3. 客户端服务收到/callback请求, 经过callback过滤器io.buji.pac4j.filter.CallbackFilter处理. 到org.pac4j.core.engine.DefaultCallbackLogic.perform(), 根据client_name,
    获取配置好的客户端信息, 其中包括cas的校验地址. 通过org.pac4j.core.client.BaseClient基础客户端类, 调用 this.authenticator.validate(credentials, context);去校验ticket,
    org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.constructValidationUrl()中拼接cas校验ticket所需要的参数, 请求cas服务,String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket);, 这就得到了认证信息, 用户信息List profiles,放入Shiro上下文中, 保存登录信息 同时本服务生产自身的(cookie/生成jwt)记录登录状态.
  4. 再次请求本服务, 这里根据shiro配置的过滤器顺序不同,如果我们之前就保存了session信息, 可能就直接放行了。 或者一层层判断登录状态, 经过io.buji.pac4j.filter.SecurityFilter.doFilter()过滤器, 从我们配置的Pac4jConfig.shiroSessionStore,SessionStore中,获取上下文信息,若本身的cookie失效,就去验证一次ticket, 如果ticket也没有, 就再去重定向到cas的登录页面

利用pac4j的封装,实现自定义cas校验ST、集成jwt_第1张图片

正题,代码验证ST

根据上面说的,我们知道第一次重定向到cas登录后,cas会重定向回来, 并且携带ticket信息,也就是颁发的ST

如果我们想接入jwt, 前后端分离使用token验证的话, 可以简单调用接口来验证,不过我这里直接在pac4j-cas的基础上,将其封装的一部分拿过来直接用了。

也可以对照上面的流程时序图, 打一些断点, 了解一下pac4j-cas内部做了什么操作.

代码

package com.zgd.common.web;

import com.zgd.common.BusinessException;
import io.buji.pac4j.context.ShiroSessionStore;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.pac4j.cas.client.CasClient;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.core.authorization.checker.AuthorizationChecker;
import org.pac4j.core.authorization.checker.DefaultAuthorizationChecker;
import org.pac4j.core.client.Client;
import org.pac4j.core.client.DirectClient;
import org.pac4j.core.client.finder.ClientFinder;
import org.pac4j.core.client.finder.DefaultSecurityClientFinder;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.J2EContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.engine.decision.DefaultProfileStorageDecision;
import org.pac4j.core.engine.decision.ProfileStorageDecision;
import org.pac4j.core.http.ajax.AjaxRequestResolver;
import org.pac4j.core.http.ajax.DefaultAjaxRequestResolver;
import org.pac4j.core.matching.MatchingChecker;
import org.pac4j.core.matching.RequireAllMatchersChecker;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.core.util.CommonHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;



/**
 * LoginController
 *
 * @author zgd
 * @date 2020/8/11 15:46
 */
@Slf4j
@RestController
public class LoginController {
     


  @Autowired
  private CasConfiguration casConfiguration;
  @Autowired
  private Pac4jConfig pac4jConfig;
  @Autowired
  private CasClient casClient;
  @Resource
  private Config authcConfig;
  private AuthorizationChecker authorizationChecker = new DefaultAuthorizationChecker();
  private MatchingChecker matchingChecker = new RequireAllMatchersChecker();
  private ProfileStorageDecision profileStorageDecision = new DefaultProfileStorageDecision();
  private AjaxRequestResolver ajaxRequestResolver = new DefaultAjaxRequestResolver();

  private ClientFinder clientFinder = new DefaultSecurityClientFinder();

  /**获取token
  */
  @GetMapping("/token")
  public Response<String> getToken( HttpServletRequest request, HttpServletResponse response) throws TicketValidationException {
     
    CommonProfile profile = validCASServiceTicket(request, response);
    if (profile ==null){
     
      throw new BusinessException(CAS_ST_ERROR);
    }

    String token = JwtTokenUtil.generateToken(profile.getUsername() ==null ? profile.getId() : profile.getUsername());
    log.info("生成的token: {}", token);
// 将签发的 JWT token 设置到 HttpServletResponse 的 Header中,并重写向vue前端页面
    response.setHeader(AUTH_HEADER, token);
    return Response.ok(token);
  }
 /**校验st
  
   * @param request
   * @param response
   * @return
   */
  private CommonProfile validCASServiceTicket(HttpServletRequest request, HttpServletResponse response) {
     
  //这两行代码其实就是下面的核心校验代码. 也可以直接用这两行代替..
    //    TicketValidator ticketValidator = casConfiguration.retrieveTicketValidator(webContext);
    //    Assertion assertion = ticketValidator.validate(ticket, pac4jConfig.getServerUrl());

    J2EContext context = new J2EContext(request, response, ShiroSessionStore.INSTANCE);
    List<Client> currentClients = clientFinder.find(authcConfig.getClients(), context, pac4jConfig.getClientName());
    log.debug("currentClients: {}", currentClients);

    CommonProfile profile = null;
    for (Client currentClient : currentClients) {
     
          log.debug("Performing authentication for direct client: {}", currentClient);
          Credentials credentials = currentClient.getCredentials(context);
          log.debug("credentials: {}", credentials);
          profile = currentClient.getUserProfile(credentials, context);
          log.debug("profile: {}", profile);
          if (profile != null) {
     
              break;
          }
      }
    return profile;
  }

}

相关配置类:


@Configuration
@Data
public class Pac4jConfig {
     
  //地址为:cas地址
  @Value("${shiro.cas.url}")
  private String casServerUrlPrefix;

  //地址为:验证返回后的项目地址:http://localhost:8080
  @Value("${shiro.client.url}")
  private String shiroServerUrlPrefix;

  //相当于一个标志,可以随意,shiroConfig中也会用到
  @Value("${shiro.client.name}")
  private String clientName;


  public String getLoginUrl() {
     
    return casServerUrlPrefix + "/login?service=" + getServiceUrl();
  }

  public String getLogoutUrl() {
     
    return casServerUrlPrefix + "/logout?service=" + getServiceUrl();
  }

  public String getServiceUrl(){
     
    return shiroServerUrlPrefix + "/aa?client_name=" + clientName;
  }

  /**
   * pac4j配置
   *
   * @param casClient
   * @return
   */
  @Bean("authcConfig")
  public Config config(CasClient casClient) {
     
    Config config = new Config(casClient);
     return config;
  }



  /**
   * cas 客户端配置
   *
   * @param casConfig
   * @return
   */
  @Bean
  public CasClient casClient(CasConfiguration casConfig) {
     
    CasClient casClient = new CasClient(casConfig);
    //客户端回调地址, 同样也是ST校验的service参数. 必须要和获取ST的service参数一致, 才能验证成功
    casClient.setCallbackUrl(getServiceUrl());
    casClient.setName(clientName);
    return casClient;
  }

  /**
   * 请求cas服务端配置
   *
   * @param
   */
  @Bean
  public CasConfiguration casConfig() {
     
    final CasConfiguration configuration = new CasConfiguration();
    //CAS server登录地址
    configuration.setLoginUrl(casServerUrlPrefix + "/login");
    //CAS 版本,默认为 CAS30,我们使用的是 CAS20
    configuration.setProtocol(CasProtocol.CAS20);
    configuration.setAcceptAnyProxy(true);
    configuration.setPrefixUrl(casServerUrlPrefix + "/");
    //监控CAS服务端登出,登出后销毁本地session实现双向登出
    DefaultLogoutHandler logoutHandler = new DefaultLogoutHandler();
    logoutHandler.setDestroySession(true);
    configuration.setLogoutHandler(logoutHandler);
    return configuration;
  }
}

这里有个需要注意的地方, 验证中用到了一个url, Assertion assertion = ticketValidator.validate(ticket, pac4jConfig.getServerUrl());
必须要和 重定向到cas登录中, 那个service参数http://cas.com/login?service=http://localhost:8080/callback&client_name=client_user一样, ST才能校验通过.

也就是 cas 服务的/login接口和校验的/serviceValidate 都需要传一个service参数, 这个参数必须一致才能保证校验通过

这个如果是用pac4j的话, 它内部拼接url参数已经保证了这点, 但是如果我们自己调用cas的api接口, 就需要注意这点

你可能感兴趣的:(security,源码,shiro,cas,单点登录,pac4j,sso)