首页 > 技术文章 > 问题

ljangle 2019-01-24 16:54 原文

使用restful接口进行登陆:
 
思路和用postman 进行测试是一样的效果
client1的登陆界面:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> 
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0"> 
    <title>登录界面</title>
    <link href="../css/styles.css" rel="stylesheet" type="text/css" />
    <link href="../css/demo.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" src="../js/requestAnimationFrame.js"></script>
    <script type="text/javascript" src="../js/jquery-2.1.1.min.js"></script>
    <script type="text/javascript" src="../js/Star.js"></script>
    <script type="text/javascript" src="../js/Particle.js"></script>
    <script type="text/javascript" src="../js/Smoke.js"></script>
    <script type="text/javascript" src="../js/jquery-ui.min.js"></script>
    <script src="../js/Treatment.js" type="text/javascript"></script>
    <script src="../js/jquery.mockjax.js" type="text/javascript"></script>
</head>
<body>
     <div style="position: absolute;z-index: 1;">
        <canvas id="canvas_app" class="first"></canvas>
      </div>
    <div class='login' style="position: absolute;z-index: 10;">
      <div class='login_title'>
        <span><center><h3>用户登录</h3></center></span>
      </div>
      <div class='login_fields'>
<!-- 使用form表单提交 https://mmoayyed.unicon.net:8433/cas/v1/tickets/login访问cas server提供的这个接口,借此来实现用户登录 -->
         <form action="https://mmoayyed.unicon.net:8433/cas/v1/tickets/login" id="loginForm" method="POST">
          <!--<form action="<%=request.getContextPath()%>/hello/restLogin" id="loginForm" method="POST">-->
          <!--<form action="http://app1.cas.com:9001/hello/restLogin" id="loginForm" method="POST">-->
          <div class='login_fields'>
                  <div class='login_fields__user'>
                      <div class='icon'>
                          <img alt="" src='img/user_icon_copy.png'>
                      </div>
                      <input name="username" id="username" placeholder='用户名' maxlength="16" type='text' autocomplete="off"/>
                      <div class='validation'>
                          <img alt="" src='img/tick.png'>
                      </div>
                  </div>
                  <div class='login_fields__password'>
                      <div class='icon'>
                          <img alt="" src='img/lock_icon_copy.png'>
                      </div>
                      <input name="password" id="password" placeholder='密码' maxlength="16" type='text' autocomplete="off">
                      <div class='validation'>
                          <img alt="" src='img/tick.png'>
                      </div>
                  </div>
                  <div class='login_fields__password'>
                      <div class='icon'>
                          <img alt="" src='img/lock_icon_copy.png'>
                      </div>
                      <input name="email" id="email" placeholder='邮箱' maxlength="16" type='text' autocomplete="off">
                      <div class='validation'>
                          <img alt="" src='img/tick.png'>
                      </div>
                  </div>
                  <div class='login_fields__password'>
                      <div class='icon'>
                          <img alt="" src='img/lock_icon_copy.png'>
                      </div>
                      <input name="telephone" id="telephone" placeholder='手机' maxlength="16" type='text' autocomplete="off">
                      <div class='validation'>
                          <img alt="" src='img/tick.png'>
                      </div>
                  </div>
                  <div class='login_fields__password'>
                      <div class='icon'>
                          <img alt="" src='img/key.png'>
                      </div>
                      <input name="capcha" id="capcha" placeholder='验证码' maxlength="4" type='text'>
                      <div class='validation' style="opacity: 1; right: -5px;top: -3px;">
                          <canvas class="J_codeimg" id="myCanvas" onclick="Code();">对不起,您的浏览器不支持canvas,请下载最新版浏览器!</canvas>
                      </div>
                  </div>
                  <div class='login_fields__password'>
                      <input name="Service" type='hidden' value="http://app1.cas.com:9001">
                  </div>
                  <div class='login_fields__submit'>
                      <input type='submit' onClick="checkCode()" value='登录'>
                  </div>
            </div>
          </form>
        </div>
      </div>
    </div>
    <script type="text/javascript">
        var canGetCookie = 0;//是否支持存储Cookie 0 不支持 1 支持
        //默认账号密码
        var CodeVal = 0;
        Code();
        function Code() {
            if(canGetCookie == 1){
                createCode("AdminCode");
                var AdminCode = getCookieValue("AdminCode");
                showCheck(AdminCode);
            }
            else{
                showCheck(createCode(""));
            }
        }
        function showCheck(a) {
            CodeVal = a;
            var c = document.getElementById("myCanvas");
            var ctx = c.getContext("2d");
            ctx.clearRect(0, 0, 1000, 1000);
            ctx.font = "80px 'Hiragino Sans GB'";
            ctx.fillStyle = "#E8DFE8";
            ctx.fillText(a, 0, 100);
        }
        $(document).keypress(function (e) {
            // 回车键事件  
            if (e.which == 13) {
                $('input[type="button"]').click();
            }
        });
        $('input[name="password"]').focus(function () {
            $(this).attr('type', 'password');
        });
        $('input[type="text"]').focus(function () {
            $(this).prev().animate({ 'opacity': '1' }, 200);
        });
        $('input[type="text"],input[type="password"]').blur(function () {
            $(this).prev().animate({ 'opacity': '.5' }, 200);
        });
        function checkCode() {
            if($("#username").val().length==0||$("#password").val().length==0){
                alert("用户名或密码不能为空");
            }
            else if($("#code").val().length==0){
                alert("验证码不能为空");
            }
            else if($("#code").val().toUpperCase()!=CodeVal.toUpperCase()){
                alert("验证码错误!");
            }
        }
    </script>
</body>
</html>
<script>
    $(window).resize(resizeCanvas);
    $(window).load(onloadFun);
    var model = { //数据配置
        hue: 237,
        stars: [],
        starImg: null,
        count: 0,
        smoke: null,
        maxStars: 4000,
    }

    var canvas = $("#canvas_app");
    var ctx = canvas.get(0).getContext("2d");
    var starImg;

    function init() {
        createStar(); //星星
        createSmoke(); //烟雾
        animation();
    }

    function animation() {
        ctx.globalCompositeOperation = 'source-over';
        ctx.globalAlpha = 0.3;
        ctx.fillStyle = 'hsla(' + model.hue + ', 64%, 6%, 1)';
        ctx.fillRect(0, 0, $(window).get(0).innerWidth, $(window).get(0).innerHeight)
        ctx.globalCompositeOperation = 'lighter';

        for (var i = 1, l = model.stars.length; i < l; i++) {
            model.stars[i].draw(ctx, model.starImg);
        };

        model.smoke.update(ctx);

        window.requestAnimationFrame(animation);
    }

    function createStar() {

        //圆点
        var canvas2 = document.createElement('canvas');
        model.starImg = canvas2;
        var ctx2 = canvas2.getContext("2d");
        canvas2.width = 88;
        canvas2.height = 88;
        var half = canvas2.width / 2;
        var gradient2 = ctx2.createRadialGradient(half, half, 0, half, half, half);
        gradient2.addColorStop(0.01, '#fff');
        gradient2.addColorStop(0.1, 'hsl(' + model.hue + ', 61%, 50%)');
        gradient2.addColorStop(0.15, 'hsl(' + model.hue + ', 64%, 80%)');
        gradient2.addColorStop(0.26, 'transparent');
        ctx2.fillStyle = gradient2;
        ctx2.beginPath();
        ctx2.arc(half, half, half, 0, Math.PI * 2);
        ctx2.fill();

        for (var i = 0; i < model.maxStars; i++) {
            var star = new Star($(window).get(0).innerWidth, $(window).get(0).innerHeight, model.maxStars);
            model.stars[i] = star;
        }
    }

    function createSmoke() {
        model.smoke = new Smoke();
    }

    function resizeCanvas() {
        canvas.attr("width", $(window).get(0).innerWidth);
        canvas.attr("height", $(window).get(0).innerHeight);
        ctx.fillRect(0, 0, canvas.width(), canvas.height());
        //图片
        $(".second").attr("width", $(window).get(0).innerWidth);
        $(".second").attr("height", $(window).get(0).innerHeight);
    }

    function onloadFun() {
        init();
        resizeCanvas();
    }
</script>
登陆界面如下图所示:
服务端进行的修改操作:

编写https://mmoayyed.unicon.net:8433/cas/v1/tickets/login 接口,也就是重写TicketGrantingTicketResource这个实现类,重写的实现类名为:CutomTicketGrantingTicketResource

 @PostMapping(value = "/v1/tickets/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity<String> createTicketGrantingTicket(@RequestBody(required=false) final MultiValueMap<String, String> requestBody,
                                                             final HttpServletRequest request, final HttpServletResponse response) {
        try {
            TicketGrantingTicket tgtId = createTicketGrantingTicketForRequest(requestBody, request);
            var cookies = request.getCookies();
            if(cookies!=null){
                System.out.println("您上次的访问时间是");
                for(int i=0;i<cookies.length;i++){
                    Cookie cookie = cookies[i];
                    System.out.println("cookie的名字"+cookie.getName());
                    System.out.println("cookie的值"+cookie.getValue());
                }
            }else {
                System.out.println("这是您第一次访问本站");
            }
            return createResponseEntityForTicket(request, tgtId);
        } catch (final AuthenticationException e) {
            return RestResourceUtils.createResponseEntityForAuthnFailure(e, request, applicationContext);
        } catch (final BadRestRequestException e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
        } catch (final Exception e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

https://mmoayyed.unicon.net:8433/cas/v1/tickets/login/{tgtId:.+}  {tgtId:.+}这个是tgt的值
 @PostMapping(value = "/v1/tickets/login/{tgtId:.+}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity<String> createServiceTicket(final HttpServletRequest httpServletRequest,
                                                      @RequestBody(required = false) final MultiValueMap<String, String> requestBody,
                                                      @PathVariable("tgtId") final String tgtId) {
        try {
            Authentication authn = this.ticketRegistrySupport.getAuthenticationFrom(tgtId);
            AuthenticationCredentialsThreadLocalBinder.bindCurrent(authn);
            if (authn == null) {
                throw new InvalidTicketException(tgtId);
            }
            Service service = this.argumentExtractor.extractService(httpServletRequest);//AbstractWebApplicationService(id=http://localhost:8081/cas-sample-java-webapp, originalUrl=http://localhost:8081/cas-sample-java-webapp, artifactId=null, principal=null, source=service, loggedOutAlready=false, format=XML, attributes={})
            if (service == null) {
                throw new IllegalArgumentException("Target service/application is unspecified or unrecognized in the request");
            }
            //getParameter 返回的是String, 用于读取提交的表单中的值;
            if (BooleanUtils.toBoolean(httpServletRequest.getParameter(CasProtocolConstants.PARAMETER_RENEW))) {
                List<Credential> credential = this.credentialFactory.fromRequest(httpServletRequest, requestBody);
                if (credential == null || credential.isEmpty()) {
                    throw new BadRestRequestException("No credentials are provided or extracted to authenticate the REST request");
                }
                AuthenticationResult authenticationResult =
                        authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, credential);

                return this.serviceTicketResourceEntityResponseFactory.build(tgtId, service, authenticationResult);
            } else {
                DefaultAuthenticationResultBuilder builder = new DefaultAuthenticationResultBuilder();
                AuthenticationResult authenticationResult = builder
                        .collect(authn)
                        .build(this.authenticationSystemSupport.getPrincipalElectionStrategy(), service);
                System.out.println("service"+service+"*****tgtId*******"+tgtId+"*****authenticationResult*******"+authenticationResult);
                return this.serviceTicketResourceEntityResponseFactory.build(tgtId, service, authenticationResult);
            }
        } catch (final InvalidTicketException e) {
            return new ResponseEntity<>(tgtId + " could not be found or is considered invalid", HttpStatus.NOT_FOUND);
        } catch (final AuthenticationException e) {
            return RestResourceUtils.createResponseEntityForAuthnFailure(e, httpServletRequest, applicationContext);
        } catch (final BadRestRequestException e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
        } catch (final Exception e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        } finally {
            AuthenticationCredentialsThreadLocalBinder.clear();
        }
    }

编写完成后,要对该实现类进行配置并注入,配置类参考:CasRestConfiguration 将 TicketGrantingTicketResource替换为我们自己的实现类CutomTicketGrantingTicketResource

那么对st验证的操作,也是同样的操作

最后关于注入配置spring.factories文件,使我们自己的实现类生效,(这里要将我们的实现类和配置类一起诸如该文件才可以生效,参考大神说法是:与之前自定义实现使用自己的数据库来存储用户名密码的诸如方式不同,自定义实现使用自己的数据库验证只需要实现配置类的注入,因为是给内部使用的;而现在我们写的接口都是给外部使用的,所以注入的时候要把全部都写进去,注入形式不再描述):

此时进行验证操作:这里注意的是cas 默认的认证类型是:UsernamePasswordCredential,如果cas server的认证类型为自定义的,此时会报错,验证通不过

点击登陆之后的界面为:

点击Submit,可以得到ST的值:ST-1-wSsZGyn3yEZL2gojGnzZEV1b4cADESKTOP-NEPM8G8
接下来使用st进行校验登陆:http://app1.cas.com:9001/?ticket=ST-1-wSsZGyn3yEZL2gojGnzZEV1b4cADESKTOP-NEPM8G8  说明校验成功
完整日志信息:
*********CustomerHandlerAuthentication执行*********
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
2019-01-28 16:26:07,214 INFO [org.apereo.cas.authentication.PolicyBasedAuthenticationManager] - <Authenticated principal [111] with attributes [{}] via credentials [[UsernamePasswordCredential(username=111, source=null)]].>
2019-01-28 16:26:07,223 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: Supplied credentials: [UsernamePasswordCredential(username=111, source=null)]
ACTION: AUTHENTICATION_SUCCESS
APPLICATION: CAS
WHEN: Mon Jan 28 16:26:07 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:26:07,347 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: TGT-2-*****lGyY-3GlcUADESKTOP-NEPM8G8
ACTION: TICKET_GRANTING_TICKET_CREATED
APPLICATION: CAS
WHEN: Mon Jan 28 16:26:07 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
您上次的访问时间是Mon Jan 28 16:26:07 CST 2019
cookie的名字JSESSIONID
cookie的值F432529EE1811DC43BEAF8F551D7A902
2019-01-28 16:26:07,390 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: [status=201-CREATED,location=https://mmoayyed.unicon.net:8433/cas/v1/tickets/login/TGT-2-*****lGyY-3GlcUADESKTOP-NEPM8G8]
ACTION: REST_API_TICKET_GRANTING_TICKET_CREATED
APPLICATION: CAS
WHEN: Mon Jan 28 16:26:07 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
serviceAbstractWebApplicationService(id=http://app1.cas.com:9001/, originalUrl=http://app1.cas.com:9001/, artifactId=null, principal=null, source=service, loggedOutAlready=false, format=XML, attributes={})*****tgtId*******TGT-2-v8yUrMSxaHAr-YWVpuh48sDRniz-DPKpAtzFeXYHdRJS7cZsuSje6gmHlGyY-3GlcUADESKTOP-NEPM8G8*****authenticationResult*******DefaultAuthenticationResult(authentication=org.apereo.cas.authentication.DefaultAuthentication@cc89e1b4, service=AbstractWebApplicationService(id=http://app1.cas.com:9001/, originalUrl=http://app1.cas.com:9001/, artifactId=null, principal=null, source=service, loggedOutAlready=false, format=XML, attributes={}), credentialProvided=false)
2019-01-28 16:26:38,588 INFO [org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner] - <[0] expired tickets removed.>
2019-01-28 16:28:02,495 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: [result=Service Access Granted,service=http://app1.cas.com:9001/,requiredAttributes={}]
ACTION: SERVICE_ACCESS_ENFORCEMENT_TRIGGERED
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:02 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:28:04,866 INFO [org.apereo.cas.DefaultCentralAuthenticationService] - <Granted ticket [ST-1-R4bNjOBlFeHz74g8XrmFlzFLPo0DESKTOP-NEPM8G8] for service [http://app1.cas.com:9001/] and principal [111]>
2019-01-28 16:28:04,867 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: ST-1-R4bNjOBlFeHz74g8XrmFlzFLPo0DESKTOP-NEPM8G8 for http://app1.cas.com:9001/
ACTION: SERVICE_TICKET_CREATED
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:04 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:28:04,867 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: [status=200-OK,body=ST-1-R4bNjOBlFeHz74g8XrmFlzFLPo0DESKTOP-NEPM8G8]
ACTION: REST_API_SERVICE_TICKET_CREATED
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:04 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:28:18,331 INFO [org.apereo.cas.web.flow.login.InitialFlowSetupAction] - <Setting path for cookies for warn cookie generator to: [/cas/] >
2019-01-28 16:28:18,345 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: audit:unknown
WHAT: [event=success,timestamp=Mon Jan 28 16:28:18 CST 2019,source=RankedMultifactorAuthenticationProviderWebflowEventResolver]
ACTION: AUTHENTICATION_EVENT_TRIGGERED
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:18 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:28:20,126 WARN [org.apache.catalina.util.SessionIdGeneratorBase] - <Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [120] milliseconds.>
2019-01-28 16:28:29,333 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: audit:unknown
WHAT: [result=Service Access Granted,service=http://app1.cas.com:9001/,principal=SimplePrincipal(id=111, attributes={}),requiredAttributes={}]
ACTION: SERVICE_ACCESS_ENFORCEMENT_TRIGGERED
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:29 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1
=============================================================

>
2019-01-28 16:28:29,334 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: 111
WHAT: ST-1-R4bNjOBlFeHz74g8XrmFlzFLPo0DESKTOP-NEPM8G8
ACTION: SERVICE_TICKET_VALIDATE_SUCCESS
APPLICATION: CAS
WHEN: Mon Jan 28 16:28:29 CST 2019
CLIENT IP ADDRESS: 127.0.0.1
SERVER IP ADDRESS: 127.0.0.1

=============================================================基于此restful认证已经完成。
 
问题:
一、cas 单点登陆是依靠cookie实现的,也就是TGC来进行校验,看是否需要重新登陆。而使用restful进行登录的时候,客户端浏览器里面根本没有TGC这个cookie;所以其他配置了单点登陆的客户端依然需要重新登陆,失去单点登陆的意义。
二、这个resturl接口并没有完全的完成登录认证,因为中间涉及其他的一些页面跳转,理想状态要求是在客户端的登陆界面中,就可以直接完成整个登陆流程。
 
 
 
参考链接:https://www.cnblogs.com/bryanx/p/8588270.html
       https://blog.csdn.net/cn_yh/article/details/77962467
关于TGC了解
cas server version:6.0.0
cas client:3.4.1
client1登陆的流程,输入client1的请求url:http://localhost:8081/cas-sample-java-webapp
302:临时重定向(指出被请求的文档已被临时移动到别处,此文档的新的URL在Location响应头中给出)此处client1由于未登录,因此被重定向到https://mmoayyed.unicon.net:8433/cas/login?service=http%3A%2F%2Flocalhost%3A8081%2Fcas-sample-java-webapp%2F
200:访问成功(表示一切正常,返回的是正常请求结果),指出重定向加载出的正确结果:https://mmoayyed.unicon.net:8433/cas/login?service=http%3A%2F%2Flocalhost%3A8081%2Fcas-sample-java-webapp%2F
 接下来输入用户名,密码进行登陆:
响应的location为:http://localhost:8081/cas-sample-java-webapp/?ticket=ST-7-1xEm8PtwNknJ4HX0WdBoAuR3nU0DESKTOP-NEPM8G8

请求中的cookie为:8074ACBDF7367A3EBDFF049730D741BF

这个cookie创建的位置为:

在对于验证码页面进行加载的时候,重置了sessionid

关于/login提交后,从数据库验证用户名,密码;通过后创建TGT,并创建jsessionid和TGC的创建;之后重定向到请求的url并传入st参数进行验证

cookie的详细内容

/login登陆页面的提交参数有:

 

重定向到url的请求:

Set-Cookie的值:A792112B087A9DB1E333E76868ED7B4C

 

至此,完成整个登陆界面

 在cas client1已经登陆的情况下,client2在cas server中的认证流程是这样的:

输入访问client2资源的url:http://localhost:8082/cas-sample-java-webapp/
对改请求的处理如下:会将该请求资源响应到https://mmoayyed.unicon.net:84…82%2Fcas-sample-java-webapp%2F
这是响应的url:https://mmoayyed.unicon.net:8433/cas/login?service=http://localhost:8082/cas-sample-java-webapp/(会发现这是我们首次登陆被拦截之后重定向的界面)
在这个界面中, CAS Server 会主动获到这个 TGC cookie,流程如下:
InitialFlowSetupAction类的doExecute方法调用this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)方法获取TGT并存放入当前作用域中,
private void configureWebflowContext(final RequestContext context) {
        val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
        WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
        WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

        WebUtils.putGoogleAnalyticsTrackingIdIntoFlowScope(context, casProperties.getGoogleAnalytics().getGoogleAnalyticsTrackingId());
        WebUtils.putGeoLocationTrackingIntoFlowScope(context, casProperties.getEvents().isTrackGeolocation());
        WebUtils.putPasswordManagementEnabled(context, casProperties.getAuthn().getPm().isEnabled());
        WebUtils.putRememberMeAuthenticationEnabled(context, casProperties.getTicket().getTgt().getRememberMe().isEnabled());
        WebUtils.putStaticAuthenticationIntoFlowScope(context,
            StringUtils.isNotBlank(casProperties.getAuthn().getAccept().getUsers())
                || StringUtils.isNotBlank(casProperties.getAuthn().getReject().getUsers()));

        if (casProperties.getAuthn().getPolicy().isSourceSelectionEnabled()) {
            val availableHandlers = authenticationEventExecutionPlan.getAuthenticationHandlers()
                .stream()
                .filter(h -> h.supports(UsernamePasswordCredential.class))
                .map(h -> StringUtils.capitalize(h.getName().trim()))
                .distinct()
                .sorted()
                .collect(Collectors.toList());
            WebUtils.putAvailableAuthenticationHandleNames(context, availableHandlers);
        }
    }
具体实现类CookieRetrievingCookieGenerator类的retrieveCookieValue方法直接从cookie中获取TGC,如果TGC不为空,调用this.casCookieValueManager.obtainCookieValue(cookie, request)方法解析TGC得到TGT,
 public String retrieveCookieValue(final HttpServletRequest request) {
        try {
            var cookie = org.springframework.web.util.WebUtils.getCookie(request, getCookieName());
            if (cookie == null) {
                val cookieValue = request.getHeader(getCookieName());
                if (StringUtils.isNotBlank(cookieValue)) {
                    LOGGER.trace("Found cookie [{}] under header name [{}]", cookieValue, getCookieName());
                    cookie = createCookie(cookieValue);
                }
            }
            return cookie == null ? null : this.casCookieValueManager.obtainCookieValue(cookie, request);
        } catch (final Exception e) {
            LOGGER.debug(e.getMessage(), e);
        }
        return null;
    }
问题逐渐明朗了,TGT是根据TGC获取到的。DefaultCasCookieValueManager.obtainValueFromCompoundCookie()使用该方法进行验证,在找到这个DefaultCasCookieValueManager实现类时,很麻烦,不知道怎样快速找到,有经验的欢迎赐教。
protected String obtainValueFromCompoundCookie(final String cookieValue, final HttpServletRequest request) {
        val cookieParts = Splitter.on(String.valueOf(COOKIE_FIELD_SEPARATOR)).splitToList(cookieValue);
        if (cookieParts.isEmpty()) {
            throw new IllegalStateException("Invalid empty cookie");
        }
        val value = cookieParts.get(0);
        if (!cookieProperties.isPinToSession()) {
            LOGGER.debug("Cookie session-pinning is disabled. Returning cookie value as it was provided");
            return value;
        }

        if (cookieParts.size() != COOKIE_FIELDS_LENGTH) {
            throw new IllegalStateException("Invalid cookie. Required fields are missing");
        }
        val remoteAddr = cookieParts.get(1);
        val userAgent = cookieParts.get(2);

        if (Stream.of(value, remoteAddr, userAgent).anyMatch(StringUtils::isBlank)) {
            throw new IllegalStateException("Invalid cookie. Required fields are empty");
        }

        val clientInfo = ClientInfoHolder.getClientInfo();
        if (!remoteAddr.equals(clientInfo.getClientIpAddress())) {
            throw new IllegalStateException("Invalid cookie. Required remote address "
                + remoteAddr + " does not match " + clientInfo.getClientIpAddress());
        }

        val agent = HttpRequestUtils.getHttpServletRequestUserAgent(request);
        if (!userAgent.equals(agent)) {
            throw new IllegalStateException("Invalid cookie. Required user-agent " + userAgent + " does not match " + agent);
        }
        return value;
    }

代码说明:首先解密TGC后得到一个由@符号分隔的字符串,分隔后获取到TGT、客户端IP、客户端代理信息。并将从TGC中解密的客户端IP信息和客户端代理信息与当前请求的客户端IP信息和客户端代理信息进行比较,若不相等就抛出异常(Cas的安全策略)。

验证通过以后相应的url为:http://localhost:8082/cas-sample-java-webapp/?ticket=ST-3-SwyI4c-Ky-BHSWfGdD8PUk98zZ4DESKTOP-NEPM8G8
此时信息显示为:
此时,st认证成功,回到访问资源的登陆界面:http://localhost:8082/cas-sample-java-webapp/
至此,第二个客户端登陆已经成功。
 
 

推荐阅读