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子类
因为我们走的是授权码模式所以直接看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系统注册的应用信息
然后就可以控制各种自己需要的类型了,比如是否生成刷新令牌,是否是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对象带大概是这样的
然后再将调用addTicketToRegistry将token注册,这时候你会发现一个它是很直接的存在map里的,也没有别的地方让我们装配,将数据存在再比如redis中
最后就是就判断是用code换取还是用刷新令牌换取
你就会看到isGenerateRefreshToken这个参数,这个参数是由isAllowedToGenerateRefreshToken直接控制的,所以这个isAllowedToGenerateRefreshToken应该算是一个钩子函数了把😂
this.generateRefreshToken = isAllowedToGenerateRefreshToken ? (registeredService != null && registeredService.isGenerateRefreshToken()) : false;
返回token之后就是通过generateAccessTokenResponse生成响应信息,到这流程就基本走完了
整体看其实流程还是十分的清晰明了,最让人借鉴的我感觉可能还是整体的设计,可以说非常灵活,各种核心逻辑都是通过组件的装配来进行处理,很有设计感,希望自己以后也能写出这样的代码把😂😂