注意↓↓↓↓
其实直接调用 http://cas服务/serviceValidate?service=xxx&ticket=xxx就能校验和获取用户信息了
这里将pac4j-cas的代码拷出来改, 也是方便理解它做了哪些事情.
这几天一直在折腾pac4j-cas和cas的集成. 也大概了解了一下它的运作机制。
io.buji.pac4j.filter.SecurityFilter
和io.buji.pac4j.filter.CallbackFilter
, SecurityFilter负责对登录认证判断,重定向到cas服务的login页面等,CallbackFilter则主要是负责在cas登录完成后,cas回调客户端,校验cas颁发的ST等.大概的流程如下
ShiroCasConfiguration.factory
中设置的过滤器匹配规则,会经过pac4j的SecurityFilter
, 进而到org.pac4j.core.engine.DefaultSecurityLogic.DefaultSecurityLogic.perform()
中判断如果没有session, 会根据配置好的cas服务地址, 重定向302去跳转到cas登录页面,http://cas登录地址?service=http://localhost:8080/callback?client_name=client_user
ticket
信息,返回302给浏览器重定向 http://localhost:8080/callback?client_name=client_user&ticket=123
/callback
请求, 经过callback过滤器io.buji.pac4j.filter.CallbackFilter
处理. 到org.pac4j.core.engine.DefaultCallbackLogic.perform()
, 根据client_name,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)记录登录状态.io.buji.pac4j.filter.SecurityFilter.doFilter()
过滤器, 从我们配置的Pac4jConfig.shiroSessionStore
,SessionStore中,获取上下文信息,若本身的cookie失效,就去验证一次ticket, 如果ticket也没有, 就再去重定向到cas的登录页面根据上面说的,我们知道第一次重定向到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接口, 就需要注意这点