注意↓↓↓↓

其实直接调用 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等.
  3. 我们需要知道,有个很重要的参数service,去cas登录的时候需要service(如service=http://www.baidu.com),同时cas登录成功后也会重定向到这个service。我们拿到ST去CAS校验的时候也需要这个service

二. pac4j-cas和shiro

大概的流程如下

  1. 访问客户端服务的请求如http://localhost:8080/listOrder, 根据shiro配置ShiroCasConfiguration.factory中设置的过滤器匹配规则,会经过pac4j的SecurityFilter, 进而到org.pac4j.core.engine.DefaultSecurityLogic.DefaultSecurityLogic.perform()中判断如果没有session。
    然后pac4j会通过saveRequestUrl保存原请求url(http://localhost:8080/listOrder)到session中,以便登录后还能知道原来要请求什么
    然后会根据配置好的cas服务地址, 重定向302去跳转到cas登录页面,
    同时携带service信息, 如: http://cas登录地址?service=http://localhost:8080/callback?client_name=client_user。 同时把原来的
  2. cas认证中心登录成功, 生成cas会话的TGC的cookie信息, 同时根据callback中的地址信息, 重新返回请求本服务, 并且携带cas发放的ticket信息,返回302给浏览器重定向到service即 http://localhost:8080/callback?client_name=client_user&ticket=123
  3. 客户端服务收到/callback请求, 经过pac4j的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<CommonProfile> profiles,放入Shiro上下文中, 保存登录信息 同时本服务生产自身的(cookie/生成jwt)记录登录状态.
    同时从session中redirectToOriginallyRequestedUrl再次取出之前保存好的原url信息(http://localhost:8080/listOrder),重定向
  4. 再次请求本服务, 这里根据shiro配置的过滤器顺序不同,如果我们之前就保存了session信息, 可能就直接放行了。 或者一层层判断登录状态, 经过io.buji.pac4j.filter.SecurityFilter.doFilter()过滤器, 从我们配置的Pac4jConfig.shiroSessionStore,SessionStore中,获取上下文信息,若本身的cookie失效,就去验证一次ticket, 如果ticket也没有, 就再去重定向到cas的登录页面

在这里插入图片描述

三. 利用pac4j代码验证ST(一)

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

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

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

3.1 代码

1
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; } }

相关配置类:

1
@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(); } /** * 这里就还是按pac4j的思路, 写死一个serviceUrl.即cas认证后重定向的url, 可以是/aa, 也可以是/bb (我们需要和CallbackFilter一样写一个过滤器来处理,在这个过滤器中,我们需要拿ST去CAS校验) */ 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; } }

四. 不用pac4j,前后端分离jwt验证(二)

先上图:
在这里插入图片描述

直接用md好像不清晰:

这只是个人提供的一种思路, 当然不仅仅限于这种做法.

和上面的做法区别点在于,上面是全靠后台pac4j去完成重定向cas登录页,拦截callback回调信息,请求cas验证st,保存登录状态。上面个人感觉更适合前后端一起的方式,但是不便于jwt的使用

这里是前端去主动跳转cas登录页,拿到st回来后交给后台去验证。也就是传过去的service参数url是前端地址。

五. 附

这里有个需要注意的地方, 验证中用到了一个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参数(比如都是http:/${host}/callback)已经保证了这点, 但是如果我们自己调用cas的api接口, 就需要注意这点