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

这段时间项目要用到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单点登录原理解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容