cas:accesstoken获取流程源码探寻

599 阅读4分钟

CAS是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。作为单点登录的一个解决方案,目前使用的人也十分之多。我们公司也在使用这个系统,所以今天稍微看一下生成token的流程,方便后续对cas系统的使用

  • 首先我们公司的单点是基于oauth2协议,所以第一步是获取code,第二步才是获取token cas默认的地址是https://xxxxxxx:8443/cas/oauth2.0/accessToken ,所以选择accestoken这个路径找到对应的controller
@PostMapping(path = {OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.ACCESS_TOKEN_URL,
    OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.TOKEN_URL})
@SneakyThrows
public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
    response.setContentType(MediaType.TEXT_PLAIN_VALUE);

    try {
        if (!verifyAccessTokenRequest(request, response)) {
            throw new IllegalArgumentException("Access token validation failed");
        }
    } catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
        OAuth20Utils.writeTextError(response, OAuth20Constants.INVALID_REQUEST);
        return;
    }

    final AccessTokenRequestDataHolder requestHolder;
    try {
        requestHolder = examineAndExtractAccessTokenGrantRequest(request, response);
        LOGGER.debug("Creating access token for [{}]", requestHolder);
    } catch (final Exception e) {
        LOGGER.error("Could not identify and extract access token request", e);
        OAuth20Utils.writeTextError(response, OAuth20Constants.INVALID_GRANT);
        return;
    }

    final J2EContext context = Pac4jUtils.getPac4jJ2EContext(request, response);
    final Pair<AccessToken, RefreshToken> accessToken = accessTokenGenerator.generate(requestHolder);
    LOGGER.debug("Access token generated is: [{}]. Refresh token generated is [{}]", accessToken.getKey(), accessToken.getValue());
    generateAccessTokenResponse(request, response, requestHolder, context, accessToken.getKey(), accessToken.getValue());
    response.setStatus(HttpServletResponse.SC_OK);
}

大概浏览下来其实还是结构挺清晰的,先校验,在获取token处理器,最后将生成的token放到session中 所以先看第一步校验:

1.verifyAccessTokenRequest()


private final Collection<OAuth20TokenRequestValidator> accessTokenGrantRequestValidators;

private boolean verifyAccessTokenRequest(final HttpServletRequest request, final HttpServletResponse response) {
    if (accessTokenGrantRequestValidators.isEmpty()) {
        LOGGER.warn("No validators are defined to examine the access token request for eligibility");
        return false;
    }
    final J2EContext context = new J2EContext(request, response);
    return this.accessTokenGrantRequestValidators.stream()
         //校验各类指定的参数
        .filter(ext -> ext.supports(context))
        .findFirst()
        .orElseThrow((Supplier<RuntimeException>) () -> new UnsupportedOperationException("Access token request is not supported"))
        .validate(context);
}

所以我们可以看到accessTokenGrantRequestValidators是一个集合,通过遍历所有的类来校验cas每一步指定的参数,所以我们之后也可以是继承这个类,就可以添加自己自定义的参数

第二步就是获取对应的token处理类

逻辑其实也很类似

private final Collection<BaseAccessTokenGrantRequestExtractor> accessTokenGrantRequestExtractors;
private AccessTokenRequestDataHolder examineAndExtractAccessTokenGrantRequest(final HttpServletRequest request,
                                                                              final HttpServletResponse response) {
    return this.accessTokenGrantRequestExtractors.stream()
        .filter(ext -> ext.supports(request))
        .findFirst()
        .orElseThrow((Supplier<RuntimeException>) () -> new UnsupportedOperationException("Access token request is not supported"))
        .extract(request, response);
}

通过匹配request中的参数,对应到对应的BaseAccessTokenGrantRequestExtractor子类,再调用extract方法返回AccessTokenRequestDataHolder,这时你会发现extract应该就是一个核心方法,点进去发现有两个实现类,会发现少了一个默认的AccessTokenRefreshTokenGrantRequestExtractor,因为这个类是继承的AccessTokenAuthorizationCodeGrantRequestExtractor而不是对应到对应的BaseAccessTokenGrantRequestExtractor子类

image.png

因为我们走的是授权码模式所以直接看AccessTokenAuthorizationCodeGrantRequestExtractor就好了

@Override
public AccessTokenRequestDataHolder extract(final HttpServletRequest request, final HttpServletResponse response) {
    final String grantType = request.getParameter(OAuth20Constants.GRANT_TYPE);
    final Set<String> scopes = OAuth20Utils.parseRequestScopes(request);

    LOGGER.debug("OAuth grant type is [{}]", grantType);

    final String redirectUri = getRegisteredServiceIdentifierFromRequest(request);
    final OAuthRegisteredService registeredService = getOAuthRegisteredServiceBy(request);
    if (registeredService == null) {
        throw new UnauthorizedServiceException("Unable to locate service in registry for redirect URI " + redirectUri);
    }
    //校验临时码是否过期等
    final OAuthToken token = getOAuthTokenFromRequest(request);
    if (token == null) {
        throw new InvalidTicketException(getOAuthParameter(request));
    }
    
    final Service service = this.webApplicationServiceServiceFactory.createService(redirectUri);
    scopes.addAll(token.getScopes());

    return new AccessTokenRequestDataHolder(service, token.getAuthentication(), token,
        registeredService, getGrantType(),
        isAllowedToGenerateRefreshToken(), scopes);
}

你就会发现OAuthRegisteredService这个参数看着很熟,其实就是获取到了我们再cas系统注册的应用信息

image.png

然后就可以控制各种自己需要的类型了,比如是否生成刷新令牌,是否是json形式返回,还有白名单的匹配等等,后面的几个方法就不赘述了,但是isAllowedToGenerateRefreshToken()方法还是需要注意下,是控制是否可以生成刷新令牌的方法,如果它是false哪怕你注册信息设置了刷新令牌为true也是没用的,这就是为什么AccessTokenRefreshTokenGrantRequestExtractor返回刷新令牌的时候不会返回新的刷新令牌。

第三步就是生成token,将token放在session中,

主要就看accessTokenGenerator.generate(requestHolder)就好了;

@Override
public Pair<AccessToken, RefreshToken> generate(final AccessTokenRequestDataHolder holder) {
    LOGGER.debug("Creating refresh token for [{}]", holder.getService());
    //根据传进来的账户信息等,生成认证对象,用于生成令牌
    final Authentication authn = DefaultAuthenticationBuilder
        .newInstance(holder.getAuthentication())
        .addAttribute(OAuth20Constants.GRANT_TYPE, holder.getGrantType().toString())
        .build();

    LOGGER.debug("Creating access token for [{}]", holder);
    final AccessToken accessToken = this.accessTokenFactory.create(holder.getService(),
        authn, holder.getTicketGrantingTicket(), holder.getScopes());

    LOGGER.debug("Created access token [{}]", accessToken);
    addTicketToRegistry(accessToken, holder.getTicketGrantingTicket());
    LOGGER.debug("Added access token [{}] to registry", accessToken);

    if (holder.getToken() instanceof OAuthCode) {
        final TicketState codeState = TicketState.class.cast(holder.getToken());
        codeState.update();

        if (holder.getToken().isExpired()) {
            this.ticketRegistry.deleteTicket(holder.getToken().getId());
        } else {
            this.ticketRegistry.updateTicket(holder.getToken());
        }
        this.ticketRegistry.updateTicket(holder.getTicketGrantingTicket());
    }

    RefreshToken refreshToken = null;
    if (holder.isGenerateRefreshToken()) {
        refreshToken = generateRefreshToken(holder);
        LOGGER.debug("Refresh Token: [{}]", refreshToken);
    } else {
        LOGGER.debug("Service [{}] is not able/allowed to receive refresh tokens", holder.getService());
    }

    return Pair.of(accessToken, refreshToken);
}

大概的逻辑就是根据传进来的账户信息,生成认证对象,根据认证对象,注册的参数等等生成令牌,个人认为无需多关注生成token的逻辑,毕竟不太具备通用性,返回的token对象带大概是这样的

image.png

然后再将调用addTicketToRegistry将token注册,这时候你会发现一个它是很直接的存在map里的,也没有别的地方让我们装配,将数据存在再比如redis中

image.png

最后就是就判断是用code换取还是用刷新令牌换取

image.png

你就会看到isGenerateRefreshToken这个参数,这个参数是由isAllowedToGenerateRefreshToken直接控制的,所以这个isAllowedToGenerateRefreshToken应该算是一个钩子函数了把😂

this.generateRefreshToken = isAllowedToGenerateRefreshToken ? (registeredService != null && registeredService.isGenerateRefreshToken()) : false;

返回token之后就是通过generateAccessTokenResponse生成响应信息,到这流程就基本走完了

整体看其实流程还是十分的清晰明了,最让人借鉴的我感觉可能还是整体的设计,可以说非常灵活,各种核心逻辑都是通过组件的装配来进行处理,很有设计感,希望自己以后也能写出这样的代码把😂😂