Cassso

一个关于单点登录框架CAS在前后端分离下的解决方案

2019-07-30  本文已影响0人  Lnstark

这段时间项目要用到cas sso框架,因为之前的老项目一直是用SpringMVC,cas用起来也没什么问题,但是现在改成了前后端分离,用cas就遇到了一些问题。
正常CAS认证流程:


CAS认证流程.png

这里用的是cas4.0,cas客户端是springboot的。遇到的第1个坑第3步的时候有跨域问题,cas里自带的登陆登出页面都是jsp的,用servlet直接跳转。而前后端分离之后就不能直接跳转。前端调接口的时候经过AuthenticationFilter时,发现没有登录会返回给浏览器302的响应以重定向到统一登录页面,这时候浏览器会有跨域问题,即使nginx配了反向代理还是一样。于是就试试让前端直接location.href = redirectUrl进行跳转。当前端访问接口时,在authenticationFilter判断是否登录,如果没有登录的话返回一个值response.getWriter().write("9527");前端获取到这个值后就进行上面的跳转。原来的redirectUrl是CAS登录页面的地址,参数是service=接口地址,跳到这个地址后CAS认证中心会根据cookie里的ticket来判断是否登录,如果登录了,就把service存下来并生成一个ST,但是之后跳转(第7步)的页面也是这个service,总不能让页面显示接口返回的值吧,这是遇到的第2个坑。

所以这里我在redirectUrl里加了一个参数toUrl作为登录成功(判断已经登录)之后跳转的地址,这个由前端来指定,一般都是当前页面。在生成ST的时候获取toUrl并放到flowScope里,然后返给浏览器一个302的响应(第6步),浏览器跳转到service(第7步)的时候将toUrl带过去,然后cas客户端拿到toUrl就直接重定向到该地址。(后来回想觉得这里第6步的时候cas可以直接重定向到toUrl,不用再跳到cas客户端,再重定向到toUrl。)
具体代码修改如下:

GenerateServiceTicketAction部分代码修改:

@Override
   protected Event doExecute(final RequestContext context) {
       final Service service = WebUtils.getService(context);
       final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
       /* 这里创建一个service用于跳转 */
       String toUrl = context.getRequestParameters().get("toUrl");
       Service dService = new MyWebApplicationServiceImpl(service.getId(), service.getId(), null, ResponseType.REDIRECT);
       context.getFlowScope().put("toService", dService);
       context.getFlowScope().put("toUrl", toUrl);
     
       try {
           final String serviceTicketId = this.centralAuthenticationService
               .grantServiceTicket(ticketGrantingTicket,
                   service);
           WebUtils.putServiceTicketInRequestScope(context,
               serviceTicketId);
          
           return success();
       } catch (final TicketException e) {
           if (isGatewayPresent(context)) {
               return result("gateway");
           }
       }

       return error();
   }

MyWebApplicationServiceImpl:

// ......省略其他代码......
// 方法改为公有
public MyWebApplicationServiceImpl(final String id,
       final String originalUrl, final String artifactId,
       final ResponseType responseType) {
       super(id, originalUrl, artifactId);
       this.responseType = responseType;
}
// ......省略其他代码......
// 添加一个参数toUrl
public Response getResponse(final String ticketId, String toUrl) {
       final Map<String, String> parameters = new HashMap<String, String>();

       if (StringUtils.hasText(ticketId)) {
           parameters.put(CONST_PARAM_TICKET, ticketId);
       }
       
       if (toUrl != null && !toUrl.isEmpty() && !toUrl.equals("null")) {
           parameters.put("TO_URL", toUrl);
       }

       if (ResponseType.POST == this.responseType) {
           return Response.getPostResponse(getOriginalUrl(), parameters);
       }
       return Response.getRedirectResponse(getOriginalUrl(), parameters);
}
// ......省略其他代码......

然后修改login-webflow.xml

 <action-state id="redirect">
        <evaluate expression="flowScope.toService.getResponse(requestScope.serviceTicketId, flowScope.toUrl)" result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response" />
        <transition to="postRedirectDecision" />
 </action-state>

这样就将toUrl发送到cas客户端了。客户端那边通过Cas20ProxyReceivingTicketValidationFilter进行ticket验证,在父类里修改doFilter方法:

private String toUrl = null;
   
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
           final FilterChain filterChain) throws IOException, ServletException {

       if (!preFilter(servletRequest, servletResponse, filterChain)) {
           return;
       }

       final HttpServletRequest request = (HttpServletRequest) servletRequest;
       final HttpServletResponse response = (HttpServletResponse) servletResponse;
       final String ticket = retrieveTicketFromRequest(request);
       
       toUrl = request.getQueryString() == null ? null 
                        : request.getParameter("TO_URL");
       
       if (CommonUtils.isNotBlank(ticket)) {
           logger.debug("Attempting to validate ticket: {}", ticket);

           try {
               final Assertion assertion = this.ticketValidator.validate(ticket,
                       constructServiceUrl(request, response));

               logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());

               request.setAttribute(CONST_CAS_ASSERTION, assertion);

               if (this.useSession) {
                   request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
               }
               onSuccessfulValidation(request, response, assertion);

               if (this.redirectAfterValidation) {
                   logger.debug("Redirecting after successful ticket validation.");
                   response.sendRedirect(constructServiceUrl(request, response));
                   return;
               }
           } catch (final TicketValidationException e) {
               logger.debug(e.getMessage(), e);

               onFailedValidation(request, response);

               if (this.exceptionOnValidationFailure) {
                   throw new ServletException(e);
               }

               response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());

               return;
           }
       } else if(CommonUtils.isNotBlank(toUrl)) {
           response.sendRedirect(toUrl);
           return;
       }
       
       filterChain.doFilter(request, response);

}

这里加了一个toUrl,判断ticket不存在并且toUrl存在,即ST验证通过后就重定向到toUrl(前端页面)。但是这里还需要修改一下验证的url,就是把service里的TO_URL参数去掉,修改AbstractUrlBasedTicketValidator的validate方法:

public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
       final String validationUrl = removeToUrl(constructValidationUrl(ticket, service));
       
       
       logger.debug("Constructing validation url: {}", validationUrl);

       try {
           logger.debug("Retrieving response from server.");
           final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

           if (serverResponse == null) {
               throw new TicketValidationException("The CAS server returned no response.");
           }

           logger.debug("Server response: {}", serverResponse);

           return parseResponseFromServer(serverResponse);
       } catch (final MalformedURLException e) {
           throw new TicketValidationException(e);
       }
   }

   /**
    * 删掉TO_URL参数让后面验证通过
    * @param constructValidationUrl
    * @return
    */
   private String removeToUrl(String constructValidationUrl) {
       Matcher m = Pattern.compile("%3FTO_URL.*").matcher(constructValidationUrl);
       if(m != null)
           return m.replaceAll("");
       return constructValidationUrl;
   }

修改之后就可以验证成功并跳回前端了,并生成了ST。
所有类的修改都是自己写一个类然后修改代码,再把自己的类注入到Spring容器,父类改了子类也跟着改。
前端调接口的时候设置xhrFields和crossDomain,就可以带上Cookie了:

xhrFields: {
    withCredentials: true
},
crossDomain: true

相应的后端响应头也需要设置一下,这里加了个过滤器,注意Access-Control-Allow-Origin不能设为'*':

    @Bean
    public FilterRegistrationBean<Filter> filterCorsRegistration() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>();
        registration.setFilter(new Filter() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
                    throws IOException, ServletException {
                
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                String origin = request.getHeader("origin");// 获取源站
                response.setHeader("Access-Control-Allow-Origin", origin);
                response.setHeader("Access-Control-Allow-Methods", "POST, GET");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Credentials", "true");
                response.setHeader("Access-Control-Allow-Headers",
                        "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
                filterChain.doFilter(servletRequest, servletResponse);
            
            }
        });
        registration.addUrlPatterns("/*");
        registration.setOrder(0);
        return registration;
    }

弄完之后测试一下,第一次访问前端,前端调接口,判断没有登录就location.href到登录页面,登录之后能返回到前端,之后接口也能调成功了。

参考资料

CAS单点登录原理解析

上一篇下一篇

猜你喜欢

热点阅读