Maxkey单点登录系统集成业务系统应用之后,登录界面登录之后不会自动跳转业务系统,需要在首页点击相应应用之后,才能实现跳转业务系统,故以下本人提供解决方法和分析思路。
本例使用的是CAS协议实现单点登录
认证服务器地址端口:9527
前段登录界面地址:http://localhost:8527/maxkey/#/passport/login
server:
port: 8989
cas:
server-url-prefix: http://localhost:9527/sign/authz/cas/ # 认证地址
server-login-url: http://localhost:8527/maxkey/#/passport/login #登录地址
client-host-url: http://localhost:8989 #客户端地址
# 认证方式,默认cas
validation-type: cas3
# CAS拦截的URL地址
authentication-url-patterns:
- /casTest/user
maxkey配置地址如下:
访问地址 http://localhost:8989/casTest/user
AbstractTicketValidationFilter 过滤器拦截请求
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//获取请求中的 ticket
String ticket = this.retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket)) {
//...
}
//无 ticket则放行进入下一个过滤器
filterChain.doFilter(request, response);
}
}
AuthenticationFilter 过滤器拦截请求
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//判断当前请求需不需要拦截
if (this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
//重定向之后的请求 session中拿出以上过滤器存入的 assertion
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
//无 assertion说明之前过滤器无 ticket校验
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if (this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
//重定向到配置的登录见面
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
} else {
filterChain.doFilter(request, response);
}
}
}
}
访问地址 http://localhost:8989/casTest/user
AbstractTicketValidationFilter 过滤器拦截请求
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//获取请求中的ticket
String ticket = this.retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket)) {
this.logger.debug("Attempting to validate ticket: {}", ticket);
try {
//有ticket时校验并返回 assertion
Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
request.setAttribute("_const_cas_assertion_", assertion);
if (this.useSession) {
//session中存入 assertion
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
this.onSuccessfulValidation(request, response, assertion);
if (this.redirectAfterValidation) {
this.logger.debug("Redirecting after successful ticket validation.");
//请求之后加上jsessionid并重定向请求
response.sendRedirect(this.constructServiceUrl(request, response));
return;
}
} catch (TicketValidationException var8) {
this.logger.debug(var8.getMessage(), var8);
this.onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(var8);
}
response.sendError(403, var8.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
}
AuthenticationFilter 过滤器拦截请求
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//判断当前请求需不需要拦截
if (this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
//重定向之后的请求 session中拿出以上过滤器存入的 assertion
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
if (assertion != null) {
//如果存在 assertion说明上个过滤器校验ticket通过,请求放行
filterChain.doFilter(request, response);
} else {
//...
}
}
}
登录之后如果不先点击一次首页应用跳转,而是直接地址栏输入 http://localhost:8989/casTest/user 跳转,还是会跳转到登录界面,无法实现登录之后直接跳转应用业务界面
通过以上 CAS原理分析在以下过滤器位置打个断点,发现 ticket为空
而如果通过点击首页应用进行第一次跳转请求是会携带 ticket的
说明问题就出在 ticket,所以就得搞清楚ticket是在什么时候设置的
通过截图发现点击应用时会调用跳转以下地址
但是该地址通过项目搜索栏搜索却搜不到,所以想别的方法
接着想到点击应用就可以跳转到相应的应用业务系统,而maxkey和应用的唯一关联的地方就是以下设置的回调地址,说明这个回调地址肯定在服务器中哪个地方有使用来进行跳转
通过查看源码发现这回调地址放在一个 callbackUrl的变量里
接着我们查找整个项目发现 callbackUrl会频繁出现在 CasAuthorizeEndpoint.java 类里
点进去发现以下接口,即 /authz/cas/应用id
请求接口,该接口会获取应用 callbackUrl并重定向到 /authz/cas/granting
请求地址,这就与以上的接口对应上了,说明我们分析我找对位置了
@Operation(summary = "CAS页面跳转应用ID认证接口", description = "传递参数应用ID",method="GET")
@GetMapping(CasConstants.ENDPOINT.ENDPOINT_BASE + "/{id}") //ENDPOINT_BASE:authz/cas
public ModelAndView authorize( @PathVariable("id") String id,
HttpServletRequest request,
HttpServletResponse response
){
AppsCasDetails casDetails = casDetailsService.getAppDetails(id , true);
return buildCasModelAndView(request,response,casDetails,casDetails == null ? id : casDetails.getCallbackUrl());
}
private ModelAndView buildCasModelAndView( HttpServletRequest request,
HttpServletResponse response,
AppsCasDetails casDetails,
String casService){
//...省略
//ENDPOINT_SERVICE_TICKET_GRANTING:"authz/cas/granting"
ModelAndView redirect = WebContext.redirect(CasConstants.ENDPOINT.ENDPOINT_SERVICE_TICKET_GRANTING);
return redirect;
}
接着发现该类还有一个接口,这个接口就是 /authz/cas/granting
接口,该接口进行的操作就是获取 ticket并且设置 ticket,那么我们登录完成之后只要也跟这部分功能一样有获取 ticket并设置的操作就能实现自动跳转
@RequestMapping(CasConstants.ENDPOINT.ENDPOINT_SERVICE_TICKET_GRANTING) // /authz/cas/granting
public ModelAndView grantingTicket( Principal principal,
HttpServletRequest request,
HttpServletResponse response){
ModelAndView modelAndView = new ModelAndView("authorize/cas_sso_submint");
AppsCasDetails casDetails = (AppsCasDetails)WebContext.getAttribute(CasConstants.PARAMETER.ENDPOINT_CAS_DETAILS);
ServiceTicketImpl serviceTicket = new ServiceTicketImpl(AuthorizationUtils.getAuthentication(),casDetails);
_logger.trace("CAS start create ticket ... ");
//获取 ticket
String ticket = ticketServices.createTicket(serviceTicket,casDetails.getExpires());
_logger.trace("CAS ticket {} created . " , ticket);
StringBuffer callbackUrl = new StringBuffer(casDetails.getCallbackUrl());
if(casDetails.getCallbackUrl().indexOf("?")==-1) {
callbackUrl.append("?");
}
if(callbackUrl.indexOf("&") != -1 ||callbackUrl.indexOf("=") != -1) {
callbackUrl.append("&");
}
//append ticket 设置ticket
callbackUrl.append(CasConstants.PARAMETER.TICKET).append("=").append(ticket);
callbackUrl.append("&");
//append service
callbackUrl.append(CasConstants.PARAMETER.SERVICE).append("=").append(casDetails.getService());
//...
//重定向到应用业务系统
_logger.debug("redirect to CAS Client URL {}" , callbackUrl);
modelAndView.addObject("callbackUrl", callbackUrl.toString());
return modelAndView;
}
在该类中有以下两个接口,点击应用时通过第二个接口来生成 ticket并设置的,而第一个接口为 /authz/cas/login?service=xxx
来实现的,那是不是我们在登录完成之后请求该接口就可实现自动转发
@Operation(summary = "CAS页面跳转service认证接口", description = "传递参数service",method="GET")
@GetMapping(CasConstants.ENDPOINT.ENDPOINT_LOGIN)
public ModelAndView casLogin(@RequestParam(value=CasConstants.PARAMETER.SERVICE,required=false) String casService,
HttpServletRequest request,
HttpServletResponse response
){
AppsCasDetails casDetails = casDetailsService.getAppDetails(casService , true);
return buildCasModelAndView(request,response,casDetails,casService);
}
@Operation(summary = "CAS页面跳转应用ID认证接口", description = "传递参数应用ID",method="GET")
@GetMapping(CasConstants.ENDPOINT.ENDPOINT_BASE + "/{id}")
public ModelAndView authorize( @PathVariable("id") String id,
HttpServletRequest request,
HttpServletResponse response
){
AppsCasDetails casDetails = casDetailsService.getAppDetails(id , true);
return buildCasModelAndView(request,response,casDetails,casDetails == null ? id : casDetails.getCallbackUrl());
}
根据以上思路我们在登录完成之后添加以上请求地址测试一下
navigate(authJwt: any) {
this.startupService.load().subscribe(() => {
let url = this.tokenService.referrer!.url || '/';
if (url.includes('/passport')) {
url = '/';
}
if (localStorage.getItem(CONSTS.REDIRECT_URI) != null) {
this.redirect_uri = `${localStorage.getItem(CONSTS.REDIRECT_URI)}`;
localStorage.removeItem(CONSTS.REDIRECT_URI);
}
let service = this.getService('service');
//添加请求地址
this.redirect_uri = 'http://localhost:9527/sign/authz/cas/login?service=http://localhost:8989/casTest/user';
if (this.redirect_uri != '') {
console.log(`redirect_uri ${this.redirect_uri}`);
location.href = this.redirect_uri;
}
this.router.navigateByUrl(url);
});
}
发现登录之后实现了跳转故我们就可以在以上方法中添加 this.redirect_uri
来实现登录后自动跳转,至此分析结束
因为访问业务系统时未登录会跳转回登录界面,跳转地址为:
http://localhost:8527/maxkey/#/passport/login?service=http:%2F%2Flocalhost:8989%2FcasTest2%2Fuser2
我们可以通过获取地址栏的serive来拼接到 this.redirect_uri后,这样就可以兼容多应用了,记得把 %2F
转换为 /