标签归档:cas

CAS单点登录原理以及debug跟踪登录流程

CAS 原理和协议

基础模式

基础模式 SSO 访问流程主要有以下步骤:

1. 访问服务: SSO 客户端发送请求访问应用系统提供的服务资源。

2. 定向认证: SSO 客户端会重定向用户请求到 SSO 服务器。

3. 用户认证:用户身份认证。

4. 发放票据: SSO 服务器会产生一个随机的 Service Ticket 。

5. 验证票据: SSO 服务器验证票据 Service Ticket 的合法性,验证通过后,允许客户端访问服务。

6. 传输用户信息: SSO 服务器验证票据通过后,传输用户认证结果信息给客户端。

下面是 CAS 最基本的协议过程:

基础协议图

如 上图: CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同 时, CAS Client 会分析 HTTP 请求中是否包含请求 Service Ticket( ST 上图中的 Ticket) ,如果没有,则说明该用户是没有经过认证的;于是 CAS Client 会重定向用户请求到 CAS Server ( Step 2 ),并传递 Service (要访问的目的资源地址)。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials , CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket ,并缓存以待将来验证,并且重定向用户到 Service 所在地址(附带刚才产生的 Service Ticket ) , 并为客户端浏览器设置一个 Ticket Granted Cookie ( TGC ) ; CAS Client 在拿到 Service 和新产生的 Ticket 过后,在 Step 5 和 Step6 中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。

在该协议中,所有与 CAS Server 的交互均采用 SSL 协议,以确保 ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向 的过程。但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的(使用 HttpsURLConnection )。

CAS 请求认证时序图如下:

CAS 如何实现 SSO

当用户访问另一个应用的服务再次被重定向到 CAS Server 的时候, CAS Server 会主动获到这个 TGC cookie ,然后做下面的事情:

1) 如果 User 持有 TGC 且其还没失效,那么就走基础协议图的 Step4 ,达到了 SSO 的效果;

2) 如果 TGC 失效,那么用户还是要重新认证 ( 走基础协议图的 Step3) 。

以上是在网络上找到的相关描述,详细请参考:

http://www.open-open.com/lib/view/open1432381488005.html

但是光看文字描述还是不够清晰,不如Debug来看一下。

----------------------------------------------------------------------------------

前提:

有两个web应用

app1.testcas.com

app2.testcas.com

Cas认证中心

demo.testcas.com

第一步:访问目标应用app1

如果想要访问app1的网页

例如:app1.testcas.com/user/doWelcome

这时,该请求将会被事先配置好的CAS Filter所拦截

app1的web.xml配置如下:

<filter>
        <filter-name>CAS Filter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>https://demo.testcas.com/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://app1.testcas.com</param-value>
        </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
断点进入该类的doFilter方法

org.jasig.cas.client.authentication.AuthenticationFilter > doFilter
public final void doFilter(final ServletRequest servletRequest,
            final ServletResponse servletResponse, final FilterChain filterChain)
            throws IOException, ServletException
    {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final HttpSession session = request.getSession(false);
        
        // 该变量为判断用户是否已经登录的标记,在用户成功登录后会被设置
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION)
                : null;
        
        // 判断是否登录过,如果已经登录过,进入if并且退出
        if (assertion != null)
        {
            filterChain.doFilter(request, response);
            return;
        }
        // 如果没有登录过,继续后续处理
        
        // 构造访问的URL,如果该Url包含tikicet参数,则去除参数
        final String serviceUrl = constructServiceUrl(request, response);
        // 如果ticket存在,则获取URL后面的参数ticket
        final String ticket = CommonUtils.safeGetParameter(request,
                getArtifactParameterName());
        // 研究中
        final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request,
                serviceUrl);
        
        // 如果ticket存在
        if (CommonUtils.isNotBlank(ticket) || wasGatewayed)
        {
            filterChain.doFilter(request, response);
            return;
        }
        
        final String modifiedServiceUrl;
        
        log.debug("no ticket and no assertion found");
        if (this.gateway)
        {
            log.debug("setting gateway attribute in session");
            modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request,
                    serviceUrl);
        }
        else
        {
            modifiedServiceUrl = serviceUrl;
        }
        
        if (log.isDebugEnabled())
        {
            log.debug("Constructed service url: " + modifiedServiceUrl);
        }
        
        // 如果用户没有登录过,那么构造重定向的URL
        final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
                getServiceParameterName(),
                modifiedServiceUrl,
                this.renew,
                this.gateway);
        
        if (log.isDebugEnabled())
        {
            log.debug("redirecting to \"" + urlToRedirectTo + "\"");
        }
        
        // 重定向跳转到Cas认证中心
        response.sendRedirect(urlToRedirectTo);
    }

第二步:请求被重定向到CAS服务器端后

根据CAS_Server端的login-webflow.xml配置

 View Code

首先会进入initialFlowSetupAction

 protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        if (!this.pathPopulated) {
            final String contextPath = context.getExternalContext().getContextPath();
            final String cookiePath = StringUtils.hasText(contextPath) ? contextPath + "/" : "/";
            logger.info("Setting path for cookies to: "
                + cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
            this.pathPopulated = true;
        }

        // 获取客户端的名为CASTGC的cookie
        context.getFlowScope().put(
            "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
        context.getFlowScope().put(
            "warnCookieValue",
            Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

        // 获取要访问的服务
        final Service service = WebUtils.getService(this.argumentExtractors,
            context);

        if (service != null && logger.isDebugEnabled()) {
            logger.debug("Placing service in FlowScope: " + service.getId());
        }

        context.getFlowScope().put("service", service);

        return result("success");
    }

之后根据webflow流程,主要有两大分歧

如果TGC并且service存在,则发放ST(service ticket)并重定向回到客户端应用

如果首次访问,TGC不存在,则跳转到CAS-server的登录页面,如下(本登录页面是重新绘制,不是CAS原生登录页)

因为我是首次登录,所以会跳转到该登录页进行认证。

第三步:用户认证

输入用户名、密码、验证码,点击登录

这时再来看login-webflow.xml

用户提交登录后,按流程依次是

1.authenticationViaFormAction.doBind

    <view-state id="viewLoginForm" view="casMyLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
            <binding property="imgverifycode" />
        </binder>
        <on-entry>
            <set name="viewScope.commandName" value="'credentials'" />
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="imgverifycodeValidate">
            <evaluate
                expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
        </transition>
    </view-state>
=>

2.imgverifycodeValidate(验证码处理为自定义的处理,不是原生逻辑)

    <action-state id="imgverifycodeValidate">
        <evaluate
            expression="authenticationViaFormAction.validatorCode(flowRequestContext, flowScope.credentials, messageContext)" />
        <transition on="error" to="generateLoginTicket" />
        <transition on="success" to="realSubmit" />
    </action-state>

=>

3.realSubmit

<action-state id="realSubmit">
        <evaluate
            expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
        <transition on="warn" to="warn" />
        <transition on="success" to="sendTicketGrantingTicket" />
        <transition on="error" to="generateLoginTicket" />
    </action-state>

realSubmit中执行的是authenticationViaFormAction.submit

 public final String submit(final RequestContext context,
            final Credentials credentials, final MessageContext messageContext)
            throws Exception
    {
        // Validate login ticket
        final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
        final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
        if (!authoritativeLoginTicket.equals(providedLoginTicket))
        {
            this.logger.warn("Invalid login ticket " + providedLoginTicket);
            final String code = "INVALID_TICKET";
            messageContext.addMessage(new MessageBuilder().error()
                    .code(code)
                    .arg(providedLoginTicket)
                    .defaultText(code)
                    .build());
            return "error";
        }
        
        // 获取TGT,首次登录的话应该是不存在的,所以直接跳过该分歧
        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
        final Service service = WebUtils.getService(context);
        if (StringUtils.hasText(context.getRequestParameters().get("renew"))
                && ticketGrantingTicketId != null && service != null)
        {
            
            try
            {
                final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId,
                        service,
                        credentials);
                WebUtils.putServiceTicketInRequestScope(context,
                        serviceTicketId);
                putWarnCookieIfRequestParameterPresent(context);
                return "warn";
            }
            catch (final TicketException e)
            {
                if (e.getCause() != null
                        && AuthenticationException.class.isAssignableFrom(e.getCause()
                                .getClass()))
                {
                    populateErrorsInstance(e, messageContext);
                    return "error";
                }
                this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
                if (logger.isDebugEnabled())
                {
                    logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials",
                            e);
                }
            }
        }
        
        try
        {
            // 首次登录时,用户输入信息验证成功后,创建一个新的TGT
            WebUtils.putTicketGrantingTicketInRequestScope(context,
                    this.centralAuthenticationService.createTicketGrantingTicket(credentials));
            putWarnCookieIfRequestParameterPresent(context);
            return "success";
        }
        catch (final TicketException e)
        {
            // 如果用户输入信息验证不通过,会抛出异常,并在页面上显示
            populateErrorsInstance(e, messageContext);
            return "error";
        }
    }

=>

4.用户信息认证通过,并且创建了新的TGT后,缓存TGT,并且生成cookie,待后续把cookie写入客户端

    <action-state id="sendTicketGrantingTicket">
        <evaluate expression="sendTicketGrantingTicketAction" />
        <transition to="serviceCheck" />
    </action-state>
sendTicketGrantingTicketAction.doExecute
    protected Event doExecute(final RequestContext context) {
        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); 
        final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
        
        if (ticketGrantingTicketId == null) {
            return success();
        }
        
        // 生成Cookie并且写入response,最终在客户端Cookie中保存了本TGT
        this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
            .getHttpServletResponse(context), ticketGrantingTicketId);

        if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
            this.centralAuthenticationService
                .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
        }

        return success();
    }

=>

5. 然后验证是否存在Service,如果存在,生成ST,重定向用户到 Service 所在地址(附带该ST ) , 并为客户端浏览器设置一个 Ticket Granted Cookie ( TGC ) 

serviceCheck => generateServiceTicket => warn => redirect =>postRedirectDecision

第四步:拿着新产生的ST,到 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。

当cas Service重定向到客户端所在service时,该重定向请求同样会被客户端配置的过滤器所拦截,又进入了第一步处的AuthenticationFilter

但是由于本次请求已经带回了ST(service ticket),所以处理与首次有所不同。

public final void doFilter(final ServletRequest servletRequest,
            final ServletResponse servletResponse, final FilterChain filterChain)
            throws IOException, ServletException
    {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final HttpSession session = request.getSession(false);
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION)
                : null;
        
        if (assertion != null)
        {
            filterChain.doFilter(request, response);
            return;
        }
        final String serviceUrl = constructServiceUrl(request, response);
        final String ticket = CommonUtils.safeGetParameter(request,
                getArtifactParameterName());
        final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request,
                serviceUrl);
        
        // 由于本次已经可以取到cas service返回的新的service ticket
        if (CommonUtils.isNotBlank(ticket) || wasGatewayed)
        {
            // 所以直接进入本代码块,然后退出
            filterChain.doFilter(request, response);
            return;
        }
        // 不会再一次被重定向会cas 认证中心

        final String modifiedServiceUrl;
        
        log.debug("no ticket and no assertion found");
        if (this.gateway)
        {
            log.debug("setting gateway attribute in session");
            modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request,
                    serviceUrl);
        }
        else
        {
            modifiedServiceUrl = serviceUrl;
        }
        
        if (log.isDebugEnabled())
        {
            log.debug("Constructed service url: " + modifiedServiceUrl);
        }
        
        // 如果用户没有登录过,那么构造重定向的URL
        final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
                getServiceParameterName(),
                modifiedServiceUrl,
                this.renew,
                this.gateway);
        
        if (log.isDebugEnabled())
        {
            log.debug("redirecting to \"" + urlToRedirectTo + "\"");
        }
        
        // 重定向跳转到Cas认证中心
        response.sendRedirect(urlToRedirectTo);
    }

之后,又会被web.xml中的CAS Validation Filter(Cas20ProxyReceivingTicketValidationFilter)所拦截

该拦截器用来与CAS Server 进行身份核实,以确保 Service Ticket 的合法性

由于 Cas20ProxyReceivingTicketValidationFilter 没有重写doFilter方法,所以会进入父类AbstractTicketValidationFilter的doFilter方法

AbstractTicketValidationFilter.doFilter

 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 = CommonUtils.safeGetParameter(request,
                getArtifactParameterName());
        
        if (CommonUtils.isNotBlank(ticket))
        {
            if (log.isDebugEnabled())
            {
                log.debug("Attempting to validate ticket: " + ticket);
            }
            
            try
            {
                // 构造验证URL,向cas server发起验证请求
                final Assertion assertion = this.ticketValidator.validate(ticket,
                        constructServiceUrl(request, response));
                
                if (log.isDebugEnabled())
                {
                    log.debug("Successfully authenticated user: "
                            + assertion.getPrincipal().getName());
                }
                
                // 如果验证成功,设置assertion,当再一次发起访问请求时,如果发现assertion已经被设置,所以已经通过验证,不过再次重定向会cas认证中心
                request.setAttribute(CONST_CAS_ASSERTION, assertion);
                
                if (this.useSession)
                {
                    request.getSession().setAttribute(CONST_CAS_ASSERTION,
                            assertion);
                }
                onSuccessfulValidation(request, response, assertion);
                
                if (this.redirectAfterValidation)
                {
                    log.debug("Redirecting after successful ticket validation.");
                    response.sendRedirect(constructServiceUrl(request, response));
                    return;
                }
            }
            catch (final TicketValidationException e)
            {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                log.warn(e, e);
                
                onFailedValidation(request, response);
                
                if (this.exceptionOnValidationFailure)
                {
                    throw new ServletException(e);
                }
                
                return;
            }
        }
        
        filterChain.doFilter(request, response);
        
    }
this.ticketValidator.validate(..) 代码如下
 public Assertion validate(final String ticket, final String service)
            throws TicketValidationException
    {
        
        // 生成验证URL,如果你debug会发现,此处会构造一个类似以下的URL,访问的是cas server的serviceValidate方法
        // https://demo.testcas.com/cas/serviceValidate?ticket=ST-31-cioaDNxSpUWIgeYEn4yK-cas&service=http%3A%2F%2Fapp1.testcas.com%2Fb2c-haohai-server%2Fuser%2FcasLogin
        final String validationUrl = constructValidationUrl(ticket, service);
        if (log.isDebugEnabled())
        {
            log.debug("Constructing validation url: " + validationUrl);
        }
        
        try
        {
            log.debug("Retrieving response from server.");
            // 得到cas service响应,验证成功或者失败
            final String serverResponse = retrieveResponseFromServer(new URL(
                    validationUrl), ticket);
            
            if (serverResponse == null)
            {
                throw new TicketValidationException(
                        "The CAS server returned no response.");
            }
            
            if (log.isDebugEnabled())
            {
                log.debug("Server response: " + serverResponse);
            }
            
            return parseResponseFromServer(serverResponse);
        }
        catch (final MalformedURLException e)
        {
            throw new TicketValidationException(e);
        }
    }
可以看一下,cas server侧的serverValidate的具体实现
在cas server的cas-servlet.xml中,可以看到如下配置:
    <bean
        id="handlerMappingC"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property
            name="mappings">
            <props>
                <prop
                    key="/logout">
                    logoutController
                </prop>
                <prop
                    key="/serviceValidate">
                    serviceValidateController
                </prop>
     ...
指向serviceValidateController

ServiceValidateController.handleRequestInternal(...)
 protected final ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        final WebApplicationService service = this.argumentExtractor.extractService(request);
        final String serviceTicketId = service != null ? service.getArtifactId() : null;

        if (service == null || serviceTicketId == null) {
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Could not process request; Service: %s, Service Ticket Id: %s", service, serviceTicketId));
            }
            return generateErrorView("INVALID_REQUEST", "INVALID_REQUEST", null);
        }

        try {
            final Credentials serviceCredentials = getServiceCredentialsFromRequest(request);
            String proxyGrantingTicketId = null;

            // XXX should be able to validate AND THEN use
            if (serviceCredentials != null) {
                try {
                    proxyGrantingTicketId = this.centralAuthenticationService
                        .delegateTicketGrantingTicket(serviceTicketId,
                            serviceCredentials);
                } catch (final TicketException e) {
                    logger.error("TicketException generating ticket for: "
                        + serviceCredentials, e);
                }
            }

            final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);

            final ValidationSpecification validationSpecification = this.getCommandClass();
            final ServletRequestDataBinder binder = new ServletRequestDataBinder(validationSpecification, "validationSpecification");
            initBinder(request, binder);
            binder.bind(request);

            if (!validationSpecification.isSatisfiedBy(assertion)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("ServiceTicket [" + serviceTicketId + "] does not satisfy validation specification.");
                }
                return generateErrorView("INVALID_TICKET", "INVALID_TICKET_SPEC", null);
            }

            onSuccessfulValidation(serviceTicketId, assertion);

            final ModelAndView success = new ModelAndView(this.successView);
            success.addObject(MODEL_ASSERTION, assertion);

            if (serviceCredentials != null && proxyGrantingTicketId != null) {
                final String proxyIou = this.proxyHandler.handle(serviceCredentials, proxyGrantingTicketId);
                success.addObject(MODEL_PROXY_GRANTING_TICKET_IOU, proxyIou);
            }

            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Successfully validated service ticket: %s", serviceTicketId));
            }

            return success;
        } catch (final TicketValidationException e) {
            return generateErrorView(e.getCode(), e.getCode(), new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()});
        } catch (final TicketException te) {
            return generateErrorView(te.getCode(), te.getCode(),
                new Object[] {serviceTicketId});
        } catch (final UnauthorizedServiceException e) {
            return generateErrorView(e.getMessage(), e.getMessage(), null);
        }
    }
验证成功后,就可以正常访问了。

来源:https://www.cnblogs.com/notDog/p/5252973.html

CAS3.5.2 Server 集成 OAuth2.0 Server 详细介绍

OAuth2.0是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储用户信息,而无需将用户名和密码提供给第三方应用,CAS3.5.x(x>1)提供了OAuth2.0的支持,包括客户端和服务端,依赖cas-server-support-oauth.jar包。

基本工作过程如下:

OAuth 2.0 Authorization

Jakob Jenkov
Last update: 2014-06-15

When a client applications wants access to the resources of a resource owner, hosted on a resource server, the client application must first obtain an authorization grant. This text explains how such an application grant is obtained.

Client ID, Client Secret and Redirect URI

Before a client application can request access to resources on a resource server, the client application must first register with the authorization server associated with the resource server.

The registration is typically a one-time task. Once registered, the registration remains valid, unless the client app registration is revoked.

At registration the client application is assigned a client ID and a client secret (password) by the authorization server. The client ID and secret is unique to the client application on that authorization server. If a client application registers with multiple authorization servers (e.g. both Facebook, Twitter and Google), each authorization server will issue its own unique client ID to the client application.

Whenever the client application requests access to resources stored on that same resource server, the client application needs to authenticate itself by sending along the client ID and the client secret to the autorhization server.

During the registration the client also registers a redirect URI. This redirect URI is used when a resource owner grants authorization to the client application. When a resource owner has successfully authorized the client application via the authorization server, the resource owner is redirected back to the client application, to the redirect URI.

Authorization Grant

The authorization grant is given to a client application by the resource owner, in cooperation with the authorization server associated with the resource server.

The OAuth 2.0 specification lists four different types of authorization grants. Each type has different security characteristics. The authorization grant types are:

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

Each of these authorization grant types is covered in the following sections.

Authorization Code

An authorization grant using an authorization code works like this (the numbers correspond to the steps shown in the diagram below the description):

1) The resource owner (user) accesses the client application.

2) The client application tells the user to login to the client application via an authorization server (e.g. Facebook, Twitter, Google etc.).

3) To login via the authorizaion server, the user is redirected to the authorization server by the client application. The client application sends its client ID along to the authorization server, so the authorization server knows which application is trying to access the protected resources.

4) The user logs in via the authorization server. After successful login the user is asked if she wants to grant access to her resources to the client application. If the user accepts, the user is redirected back to the client application.

5) When redirected back to the client application, the authorization server sends the user to a specific redirect URI, which the client application has registered with the authorization server ahead of time. Along with the redirection, the authorization server sends an authorization code, representing the authorization.

6) When the redirect URI in the client application is accessed, the client application connects directly to the authorization server. The client application sends the authorization code along with its own client ID and and client secret.

7) If the authorization server can accept these values, the authorization server sends back an access token.

10) The client application can now use the access token to request resources from the resource server. The access token serves as both authentication of the client, resource owner (user) and authorization to access the resources.

Here is a diagram illustrating the authorization process when using authorization code to authorize a client application:

1
Authorization grant via authorization code.

Implicit

An implicit authorization grant is similar to an authorization code grant, except the access token is returned to the client application already after the user has finished the authorization. The access token is thus returned when the user agent is redirected to the redirect URI.

This of course means that the access token is accessible in the user agent, or native application participating in the implicit authorization grant. The access token is not stored securely on a web server.

Furthermore, the client application can only send its client ID to the authorization server. If the client were to send its client secret too, the client secret would have to be stored in the user agent or native application too. That would make it vulnerable to hacking.

Implicit authorization grant is mostly used in a user agent or native client application. The user agent or native application would receive the access token from the authorization server.

Here is an illustration of implicit authorization grant:

2
Implicit authorization grant.

Resource Owner Password Credentials

The resource owner password credentials authorization grant method works by giving the client application access to the resource owners credentials. For instance, a user could type his Twitter user name and password (credentials) into the client application. The client application could then use the user name and password to access resources in Twitter.

Using the resource owner password credentials requires a lot of trust in the client application. You do not want to type your credentials into an application you suspect might abuse it.

The resource owner password credentials would normally be used by user agent client applications, or native client applications.

Client Credentials

Client credential authorization is for the situations where the client application needs to access resources or call functions in the resource server, which are not related to a specific resource owner (e.g. user). For instance, obtaining a list of venues from Foursquare. This does not necessary have anything to do with a specific Foursquare user.

cas 3.5.2下载地址:https://github.com/apereo/cas/releases/tag/v3.5.2

CAS默认提供了三个服务:

/oauth2.0/authorize

需要输入GET参数:client_id和redirect_uri

/oauth2.0/accessToken

需要输入GET参数:client_idredirect_uriclient_secret和代码

/oauth2.0/profile

需要输入GET参数:access_token

关于CAS3.5.x Server版本接入oauth2.0 Server配置步骤,下面为本站素文宅www.yoodb.com大家分享一下个人总结,仅供大家参考学习使用。

1、将cas-server-support-oauth工程引入cas-server-webapp项目中,通过pom.xml文件,增加内容如下:

<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-support-oauth</artifactId>
<version>${project.version}</version>
</dependency>

2、配置应用获得client_id和client_secret

在QQ、新浪等使用第三方登录中,通常提供页面供用户申请应用,然后提供用户client_id和client_secret,并允许用户配置回调地址,那么oauth2.0 server考虑的就是需要持久化这些配置,CAS默认在文件deployerConfigContext.xml的serviceRegistryDao中配置应用服务,注意实际使用可以将申请的应用信息存储在数据库中,具体配置如下:

<bean
id="serviceRegistryDao"
        class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
            <property name="registeredServices">
                <list>
                    <bean class="org.jasig.cas.services.RegexRegisteredService">
                        <property name="id" value="0" />
                        <property name="name" value="HTTP and IMAP" />
                        <property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />
                        <property name="serviceId" value="^(https?|imaps?)://.*" />
                        <property name="evaluationOrder" value="10000001" />
<!-- 参数回传 -->
                        <property name="ignoreAttributes" value="true"/>
                    </bean>
                    <bean class="org.jasig.cas.services.RegisteredServiceImpl">
                    <property name="id" value="1" />
                    <property name="name" value="HTTP" />
                    <property name="description" value="oauth wrapper callback url" /><!-- oauth wrapper callback url -->
                    <property name="serviceId" value="${server.prefix}/oauth2.0/callbackAuthorize" />
                </bean>
               <bean class="org.jasig.cas.services.RegisteredServiceImpl">
                <property name="id" value="2" />
                <property name="name" value="key" />
                <property name="description" value="secret" />
                <property name="serviceId" value="http://www.yoodb.com/" />
                <property name="theme" value="Yoodb" />
              </bean>
                </list>
            </property>
        </bean>

如上述代码所示,新注册了两个bean,第一个bean不需要考虑那是默认存在的bean,如果想了解参考地址:bean http://blog.yoodb.com/yoodb/article/detail/1223,我们新增的第二个bean中,name为client_id,description为client_secret,serviceId为回调地址,theme为应用名称,下面为大家说说第一个bean的用法及其用途。

3、 Oauth client构造url获得authorization_code(ST票据)

1)cas server对/oauth2.0/authorize的url进行拦截处理,需要配置映射,在web.xml中增加配置信息如下:

<servlet-mapping>
    <servlet-name>cas</servlet-name>
    <url-pattern>/oauth2.0/*</url-pattern>
</servlet-mapping>

2)在cas-servlet.xml配置文件,增加内容如下:

<bean
      id="handlerMappingC"
      class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <props>
...
...
<prop key="/oauth2.0/*">oauth20WrapperController</prop>
...
...
<props>
</bean>
<bean id="oauth20WrapperController"
    class="org.jasig.cas.support.oauth.web.OAuth20WrapperController"
    p:loginUrl="${server.prefix}/login" p:servicesManager-ref="servicesManager"
    p:ticketRegistry-ref="ticketRegistry" p:timeout="7200" />

配置完成后,我们获取授权码的链接会转向login页面,此时的service地址就是第二步时新增的bean配置的第一个bean的serviceId,通过这个默认提供的地址间接的获取到ST票据,具体请求如下:

CAS Server在浏览器中打开地址:

http://localhost:8080/cas/oauth2.0/authorize?client_id=key&redirect_uri=http://www.yoodb.com/&response_type=codea

将会跳转到地址:

http://localhost:8080/cas/login?service=http%3A%2F%2F127.0.0.1%3A8080%2Fcas%2Foauth2.0%2FcallbackAuthorize

,从而间接的获取到ST票据。认证成功之后,就会携带值为ST票据的参数跳转到callbackAuthorize页面,此时生成的ST即为授权码,其中回调地址、服务名称是通过session传递过来的,注意到浏览器地址变为如下:

http://localhost:8080/cas/oauth2.0/callbackAuthorize?ticket=ST-5-u53bEqNQz2phBhnocFsb-www.yoodb.com

默认授权码只能使用一次且有效时间为10s,可以参考http://blog.yoodb.com/yoodb/article/detail/1225文章,通过修改票据过期策略进行配置时间。

4、通过ST票据授权码获得access_token

在浏览器中输入如下地址:

http://localhost:8080/cas/oauth2.0/accessToken?client_id=key&client_secret=secret&
grant_type=authorization_code&redirect_uri=http://www.yoodb.com/&code=ST-3-KIxamZyWgOYFcoXggdfs-www.yoodb.com

返回access_token信息如下:

access_token=TGT-6-csg7B16Pc09aZ6bx9j3Y6INMzScyZFhZcOGEqpnBUvoiLJkv93-www.yoodb.com&expires=7193

5、根据access_token获取用户信息

在浏览器中输入如下地址:

http://localhost:8080/cas/oauth2.0/cas/oauth2.0/profile?access_token=TGT-4-bxgrfir4tdCJ2O1EbR6eeguc3tcdpToZeyFBv416vdvvH1g3Nk-www.yoodb.com

根据access_token等到的返回用户信息如下:

{
    "id": "000000000000000001",
    "attributes": [
        {
            "uid": "000000000000000001"
        },
        {
            "username": "mrwang"
        },
        {
            "password": "123456"
        }
    ]
}

通过上述描述CAS3.5.x Server认证成功之后生成ST,此值即为授权码,传递给应用的回调地址即可,OAuth2的实现并不是很标准,对于CAS3.5.x Server版本需要扩展org.jasig.cas.support.oauth.web.OAuth20WrapperController类来进一步完善oauth2.0协议。

本站文章除注明转载外,均由 素文宅 整理发布,欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 素文宅博客

本文标题:CAS3.5.2 Server 集成 OAuth2.0 Server 详细介绍

本文地址:https://blog.yoodb.com/yoodb/article/detail/1226

CAS 跨域原理

CAS(Central Authentication Service) 是 Yale 大学发起的一个开源项目,据统计,大概每 10 个采用开源构建 Web SSO 的 Java 项目,就有 8 个使用 CAS 。对这些统计,我虽然不以为然,但有一点可以肯定的是, CAS 是我认为最简单实效,而且足够安全的 SSO 选择。

本节主要分析 CAS 的安全性,以及为什么 CAS 被这样设计,带着少许密码学的基础知识,我希望有助于读者对 CAS 的协议有更深层次的理解。
从结构体系看, CAS 包含两部分:
l         CAS Server
CAS Server 负责完成对用户的认证工作, CAS Server 需要独立部署,有不止一种 CAS Server 的实现, Yale CAS Server 和 ESUP CAS Server 都是很不错的选择。
CAS Server 会处理用户名 / 密码等凭证 (Credentials) ,它可能会到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户密码,对这种方式, CAS 均提供一种灵活但同一的接口 / 实现分离的方式, CAS 究竟是用何种认证方式,跟 CAS 协议是分离的,也就是,这个认证的实现细节可以自己定制和扩展。
l         CAS Client
CAS Client 负责部署在客户端(注意,我是指 Web 应用),原则上, CAS Client 的部署意味着,当有对本地 Web 应用的受保护资源的访问请求,并且需要对请求方进行身份认证, Web 应用不再接受任何的用户名密码等类似的 Credentials ,而是重定向到 CAS Server 进行认证。
目前, CAS Client 支持(某些在完善中)非常多的客户端,包括 Java 、 .Net 、 ISAPI 、 Php 、 Perl 、 uPortal 、 Acegi 、 Ruby 、 VBScript 等客户端,几乎可以这样说, CAS 协议能够适合任何语言编写的客户端应用。

 剖析协议就像剖析设计模式,有些时候,协议让人摸不着头脑。 CAS 的代理模式要相对复杂一些,它引入了一些新的概念,我希望能够在这里描述一下其原理,有助于读者在配置和调试 CAS SSO 有更清晰的思路。

如果没记错, CAS 协议应该是由 Drew Mazurek 负责可开发的,从 CAS v1 到现在的 CAS v3 ,整个协议的基础思想都是基于 Kerberos 的票据方式。
       CAS v1 非常原始,传送一个用户名居然是 ”yes david.turing” 的方式, CAS v2 开始使用了 XML 规范,大大增强了可扩展性, CAS v3 开始使用 AOP 技术,让 Spring 爱好者可以轻松配置 CAS Server 到现有的应用环境中。
CAS 是通过 TGT(Ticket Granting Ticket) 来获取 ST(Service Ticket) ,通过 ST 来访问服务,而 CAS 也有对应 TGT , ST 的实体,而且他们在保护 TGT 的方法上虽然有所区别,但是,最终都可以实现这样一个目的——免去多次登录的麻烦。
       下面,我们看看 CAS 的基本协议框架:
       上图是一个最基础的 CAS 协议, CAS Client 以 Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同时, CAS Client 会分析 HTTP 请求中是否包请求 Service Ticket( 上图中的 Ticket) ,如果没有,则说明该用户是没有经过认证的,于是, CAS Client 会重定向用户请求到 CAS Server ( Step 2 )。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials , CAS Server 会产生一个随机的 Service Ticket ,然后,缓存该 Ticket ,并且重定向用户到 CAS Client (附带刚才产生的 Service Ticket ), Service Ticket 是不可以伪造的,最后, Step 5 和 Step6 是 CAS Client 和 CAS Server 之间完成了一个对用户的身份核实,用 Ticket 查到 Username ,因为 Ticket 是 CAS Server 产生的,因此,所以 CAS Server 的判断是毋庸置疑的。
       该协议完成了一个很简单的任务,就是 User(david.turing) 打开 IE ,直接访问 helloservice 应用,它被立即重定向到 CAS Server 进行认证, User 可能感觉到浏览器在 helloservcie 和 casserver 之间重定向,但 User 是看不到, CAS Client 和 CAS Server 相互间的 Service Ticket 核实 (Validation) 过程。当 CAS Server 告知 CAS Client 用户 Service Ticket 对应确凿身份, CAS Client 才会对当前 Request 的用户进行服务。

CAS 如何实现 SSO

       当我们的 Web 时代还处于初级阶段的时候, SSO 是通过共享 cookies 来实现,比如,下面三个域名要做 SSO :
http://www.blogjava.net
http://www.matrix.org.cn
http://www.csdn.net
如果通过 CAS 来集成这三个应用,那么,这三个域名都要做一些域名映射,
http://blogjava.cas.org
http://matrix.cas.org
http://csdn.cas.org
因为是同一个域,所以每个站点都能够共享基于 cas.org 的 cookies 。这种方法原始,不灵活而且有不少安全隐患,已经被抛弃了。
CAS 可以很简单的实现跨域的 SSO ,因为,单点被控制在 CAS Server ,用户最有价值的 TGC-Cookie 只是跟 CAS Server 相关, CAS Server 就只有一个,因此,解决了 cookies 不能跨域的问题。
回到 CAS 的基础协议图,当 Step3 完成之后, CAS Server 会向 User 发送一个 Ticket granting cookie (TGC) 给 User 的浏览器,这个 Cookie 就类似 Kerberos 的 TGT ,下次当用户被 Helloservice2 重定向到 CAS Server 的时候, CAS Server 会主动 Get 到这个 TGC cookie ,然后做下面的事情:
1,              如果 User 的持有 TGC 且其还没失效,那么就走基础协议图的 Step4 ,达到了 SSO 的效果。
2,              如果 TGC 失效,那么用户还是要重新认证 ( 走基础协议图的 Step3) 。
 CAS 的代理模式

 模式 1 已经能够满足大部分简单的 SSO 应用,现在,我们探讨一种更复杂的情况,即用户访问 helloservice , helloservice 又依赖于 helloservice2 来获取一些信息,如同:

User à helloservice à helloservice2
这种情况下,假设 helloservice2 也是需要对 User 进行身份验证才能访问,那么,为了不影响用户体验(过多的重定向导致 User 的 IE 窗口不停地 闪动 ) , CAS 引入了一种 Proxy 认证机制,即 CAS Client 可以代理用户去访问其它 Web 应用。
代理的前提是需要 CAS Client 拥有用户的身份信息 ( 类似凭据 ) 。 与其说之前我们提到的 TGC 是用户持有对自己身份信息的一种凭据,则这里的 PGT 就是 CAS Client 端持有的对用户身份信息的一种凭据。凭借 TGC , User 可以免去输入密码以获取访问其它服务的 Service Ticket ,所以,这里,凭借 PGT , Web 应用可以代理用户去实现后端的认证,而无需前端用户的参与。
如下面的 CAS Proxy 图所示, CAS Client 在基础协议之上,提供了一个额外的 PGT URL 给 CAS Server, 于是, CAS Server 可以通过 PGT URL 提供一个 PGT 给 CAS Client 。
初学者可能会对上图的 PGT URL 感到迷惑,或者会问,为什么要这么麻烦,要通过一个额外的 URL( 而且是 SSL 的入口 ) 去传递 PGT ?如果直接在 Step 6 返回,则连用来做对应关系的 PGTIOU 都可以省掉。 PGTIOU 设计是从安全性考虑的,非常必要, CAS 协议安全性问题我会在后面一节介绍。
于是, CAS Client 拿到了 PGT( PGTIOU-85…..ti2td ) ,这个 PGT 跟 TGC 同样地关键, CAS Client 可以通过 PGT 向后端 Web 应用进行认证。如下图所示, Proxy 认证与普通的认证其实差别不大, Step1, 2 与基础模式的 Step 1,2 几乎一样,唯一不同的是, Proxy 模式用的是 PGT 而不是 TGC ,是 Proxy Ticket ( PT )而不是 Service Ticket 。
最终的结果是, helloservice2 明白 helloservice 所代理的客户是 David. Turing 同学,同时,根据本地策略, helloservice2 有义务为 PGTURL=http://helloservice/proxy 服务 (PGTURL 用于表示一个 Proxy 服务 ) ,于是它传递数据给 helloservice 。这样, helloservice 便完成一个代理者的角色,协助 User 返回他想要的数据。
代理认证模式非常有用,它也是 CAS 协议 v2 的一个最大的变化,这种模式非常适合在复杂的业务领域中应用 SSO 。因为,以前我们实施 SSO 的时候,都是假定以 IE User 为 SSO 的访问者,忽视了业务系统作为 SSO 的访问者角色。
作者:木木
出处:http://haore147.cnblogs.com/
博客文章大部分为原创,版权归作者和博客园共有,欢迎转载。

CAS实现单点登录(SSO)经典完整教程

来源:互联网

一、简介

1、cas是有耶鲁大学研发的单点登录服务器

2、本教材所用环境

· Tomcat7.2

· JDK6

· CAS Service 版本    cas-server-3.4.8-release

· CAS Client版本      cas-client-3.2.1-release

二、生成证书

证书对于实现此单点登录非常之重要,证书是服务器端和客户端安全通信的凭证,本教程只是演示,所有用了

JDK自带的证书生成工具keytool。当然在实际项目中你可以到专门的证书认证中心购买证书。

中文官方网站:http://www.verisign.com/cn/

1、用JDK自带的keytool生成证书

1. 命令:keytool -genkey -alias  smalllove -keyalg RSA -keystore D:/keys/smallkey

此命令是生成一个证书,其中 smalllove 是证书别名

此命令的执行如图所示:

wps_clip_image-14543[3][1]

其中名字与姓氏这一最好写你的 域名,如果在单击测试你可以在C:\Windows\System32\drivers\etc\hosts文件中映射一个虚拟域名,

注意不要写IP。

2、导出证书

1. 命令:C:\>keytool -export -file d:/keys/small.crt -alias smalllove -keystore d:/keys/smallkey

如图:

wps_clip_image-30622[3][1]

密码为上步设置的密码。

3、把证书导入到客户端JDK中。

1. 命令:keytool -import -keystore C:\Java\jdk1.6.0_21\lib\security\cacerts -file D:/keys/small.crt -alias smalllove

此命令是把证书导入到JDK中。

如图:

wps_clip_image-8415[3][1]

到此证书导入成功。

注意:在此步有可能出现如下错误

1. C:\>keytool -import -keystore C:\Java\jdk1.6.0_21\lib\security\cacerts -file D:/keys/small.crt -alias smalllove

2.  输入keystore密码:

3.  keytool错误: java.io.IOException: Keystore was tampered with, or password was incorrect

次错误的解决方法是,把%JAVA_HOME%\lib\security下的cacerts文件删除掉,在执行。

三、配置服务端

1、 下载CAS的服务端,解压,把解压后的文件中modules文件夹中的cas-server-webapp-3.4.8.war文件拷贝的%TOMCAT_HOME%\webapps

下,并修改文件名为:cas.war。

2、修改%TOMCAT_HOME%\conf\server.xml文件

去掉此文件83到93行之间的注释,修改为:

view plain

1. <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"

2.  maxThreads="150" scheme="https" secure="true"

3.  clientAuth="false" sslProtocol="TLS"

4.  keystoreFile="D:/keys/smallkey" <!--在2.1中生成的证书的位置-->

5.  keystorePass="smalllove"/> <!--在2.1中设置的密码-->

3、以上配置完成访问http://yourhost:8443/cas出现一下页面

wps_clip_image-25343[3][1]

点击继续浏览会出现

wps_clip_image-22055[3][1]

输入用户名admin和密码admin登录则会出现

wps_clip_image-13962[3][1]

登录成功。

至此,说明服务端配置成功。

四、配置客户端

1、添加客户端到你的项目中

·手动下载下载cas-client,地址:http://www.ja-sig.org/downloads/cas-clients/,然后解压cas-client-3.1.12.zip,在modules文件夹中有需要的jar包,  请根据自己的项目情况选择使用,把相应的jar包放到你项目WEB-INF/lib下。

·使用maven

1. <!-- cas -->

2. <dependency>

3.  <groupId>org.jasig.cas.client</groupId>

4.  <artifactId>cas-client-core</artifactId>

5.  <version>3.1.12</version>

6. </dependency>

2、在客户端项目的web.xml配置过滤器

view plain

1. <!-- ======================== 单点登录开始 ======================== -->

2.  <!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->

3.  <listener>

4.  <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>

5.  </listener>

6.

7.  <!-- 该过滤器用于实现单点登出功能,可选配置。 -->

8.  <filter>

9.  <filter-name>CAS Single Sign Out Filter</filter-name>

10.  <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>

11.  </filter>

12.  <filter-mapping>

13.  <filter-name>CAS Single Sign Out Filter</filter-name>

14.  <url-pattern>/*</url-pattern>

15.  </filter-mapping>

16.

17.  <!-- 该过滤器负责用户的认证工作,必须启用它 -->

18.  <filter>

19.  <filter-name>CASFilter</filter-name>

20.  <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>

21.  <init-param>

22.  <param-name>casServerLoginUrl</param-name>

23.  <param-value>https://www.travel.com:8443/cas/login</param-value>

24.  <!--这里的server是服务端的IP -->

25.  </init-param>

26.  <init-param>

27.  <param-name>serverName</param-name>

28.  <param-value>http://www.travel.com:8080</param-value><span style="color:#FF0000;"> ①</span>

29.  </init-param>

30.  </filter>

31.  <filter-mapping>

32.  <filter-name>CASFilter</filter-name>

33.  <url-pattern>/*</url-pattern>

34.  </filter-mapping>

35.

36.  <!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->

37.  <filter>

38.  <filter-name>CAS Validation Filter</filter-name>

39.  <filter-class>

40.             org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>

41.  <init-param>

42.  <param-name>casServerUrlPrefix</param-name>

43.  <param-value>https://www.travel.com:8443/cas</param-value>

44.  </init-param>

45.  <init-param>

46.  <param-name>serverName</param-name>

47.  <param-value>http://www.travel.com:8080</param-value> <span style="color:#FF0000;">②</span>

48.  </init-param>

49.  </filter>

50.  <filter-mapping>

51.  <filter-name>CAS Validation Filter</filter-name>

52.  <url-pattern>/*</url-pattern>

53.  </filter-mapping>

54.

55.  <!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->

56.  <filter>

57.  <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>

58.  <filter-class>

59.             org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>

60.  </filter>

61.  <filter-mapping>

62.  <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>

63.  <url-pattern>/*</url-pattern>

64.  </filter-mapping>

65.

66.  <!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->

67.  <filter>

68.  <filter-name>CAS Assertion Thread Local Filter</filter-name>

69.  <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>

70.  </filter>

71.  <filter-mapping>

72.  <filter-name>CAS Assertion Thread Local Filter</filter-name>

73.  <url-pattern>/*</url-pattern>

74.  </filter-mapping>

75.

76.  <!-- ======================== 单点登录结束 ======================== -->

五、常见问题说明

错误一、

wps_clip_image-29822[3][1]

若出现以上错原因是:你在客户端的web.xml中①,②的配置有误。

错误二、

1. javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException:

2. PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:

3. unable to find valid certification path to requested target

若出现次错误是有与你客户端的证书有问题。重新导入你证书。

使用CAS实现SSO简洁教程

来源:互联网

SSO 即Single sign on/off,单点登录/退出

CAS 全称JA-SIG Central Authentication Service,实现SSO的开源项目

1. 概述

1.1背景

单点登录是必须的,实现方式颇多,这里就说使用CAS的实现方式。使用CAS实现SSO,网络上说明很多,大部分都是从制作证书开始,而实际上是可以不使用HTTPS验证,这样更方便。

单点登录的原理是通过拦截你设定的URL,并跳转到你指定的CAS SERVER登录页,当你登录成功后,带着TICKET,返回到你打开的URL。然后你就可以一票在手,畅通无阻。

网上有个家伙用旅游的套票来解释单点登录,非常形象。当你到达一个旅游区门口,你可以买一个套票,套票规定你可以游览N个景点,进入这些景点的时候,你不需要再买票,也就实现了单点登录。

同时,也可以借用这个比喻说明一下单点注销。当你打开一个应用A时,单击了注销按钮,跳转到http://hostname:port/cas/logout 或者https://hostname:port/cas/logout ,系统显示注销成功。此时,IE窗口没有关闭,你继续打开应用A,仍然没有注销成功,不需要登录。这就相当于你已经在旅游景点内,即使你把套票撕毁了,你仍然可以继续参观这个景点,不会把你驱逐出去。但是,你再也进不了其它的景点了。

那么怎么实现立即生效的注销呢?或者这种方式是否就满足我们的需求呢?

1.2环境

Windows XP、JDK1.6.03、Tomcat6.0.20

注意:配置好环境变量。

1.3下载资源

服务器端:http://www.ja-sig.org/downloads/cas

当前最新版本是3.3.4,测试安装的版本为3.3.3

cas-server-3.3.3-release.zip

客户端:https://www.ja-sig.org/svn/cas-clients/

cas-client-2.0.11.zip JAVA支持单点登录

cas-client-3.1.8-release.zip JAVA支持单点注销

dotnet-client DOTNET支持类

phpcas PHP支持

注意:同时要下载源代码,部分功能需要修改源代码,重新做包。

2. 配置CAS SERVER

CAS SERVER目录介绍。

2.1简单配置

把你下载cas-server解压,进入cas-server-3.3\modules,复制cas-server-webapp-3.3.war到tomcat\webapps下,修改名称为cas.war,方便使用,原来的名字太长了。然后启动IE,输入http://localhost:8080/cas检验是否可以访问,如果可以,则输入相同的用户名和密码,比如cas/cas,测试是否能登录。

如果你对安全性要求不高且急不可待,这个时候就可以直接进行CAS CLIENT的配置。

wps_clip_image-16711[3][1]

wps_clip_image-24140[3][1]

2.2数据库验证配置

简单配置后,可以使用相同的用户名和密码,进行登录。这个不实际,可以通过配置实现连接自己的数据库进行配置。这里有个前提,就是所有系统需要使用相同的用户表和密码加密方法,可以使用CAS自带的加密方法(\org\jasig\cas\authentication\handler\ DefaultPasswordEncoder.class)或自己用JAVA写的加密方法。

进入目录tomcat\webapps\cas\WEB-INF,打开文件deployerConfigContext.xml,找到类似下面的代码:

注意:cas-server根据版本不同,文件的路径或BEAN位置可能不同,逐个文件夹找下。

<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />

这是默认的方法。同时还有两种可选,如下:

<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">

<property name="dataSource" ref="casDataSource" />

<property name="sql" value="select password from tbUser where lower(name) = lower(?)" />

</bean>

/************************************************************************/

<bean class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler"

abstract="false" lazy-init="default" autowire="default" dependency-check="default">

<property name="dataSource" ref="casDataSource" />

<property name="tableUsers" value="tbUser" />

<property name="fieldUser" value="u_userid"/>

<property name="fieldPassword" value="u_password"/>

<property name="passwordEncoder" ref="passwordEncoder"/>

</bean>

以上两种方式都经过测试。另外,也可以写出自己的方法验证,修改BEAN的CLASS属性即可。如果没有密码没有加密,则可以把参数<property name="passwordEncoder" ref="passwordEncoder"/>去掉。

下面针对最下面的方法进行说明。

看它的属性,还需要定义两个BEAN,数据源和加密,如下:

<bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource">

<!—SQL SERVER

<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"></property>

<property name="url" value="jdbc:sqlserver://10.7.3.90:1433;DatabaseName=itacc"></property>

<property name="username" value="zero" />

<property name="password" value="123456" />

-->

<!--ORACLE à

<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property>

<property name="url" value="jdbc:oracle:thin:@10.7.3.90:1521:orcl"></property>

<property name="username" value="zero"></property>

<property name="password" value="123456"></property>

<property name="maxActive" value="100"></property>

<property name="maxIdle" value="30"></property>

<property name="maxWait" value="500"></property>

<property name="defaultAutoCommit" value="true"></property>

</bean>

<!—加密à

<bean id="passwordEncoder"

class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" autowire="byName">

<constructor-arg value="MD5"/>

</bean>

wps_clip_image-8209[3][1]

由于CAS-SERVER.WAR默认没有采用数据库加密,所以部分JAR包,没有引入,下面这些需要复制到webapps\cas\WEB-INF\lib里面:

Cas-server-support-jdbc-3.3.3.jar

Cas-server-support-ldap-3.3.3.jar

Commons-dbcp.jar

Commons-pool.jar

Org.springframework.jdbc-3.0.0.M4.jar        //spring要根据自己的版本选择

Org.springframework.transaction-3.0.0.M4.jar

Sqljdbc.jar //数据库连接JAR,根据自己的复制

Oraclejdbc.jar

wps_clip_image-12599[3][1]

完成以后,用户表数据准备好后,可以运行http://localhost:8080/cas进行登录测试。这个时候很容易出现错误,多数是因为JAR包问题,注意查看tomcat\logs日志文件,进行检查,问题很容易解决。

2.3参数配置

这里只说明一下我涉及到的参数,可能有更多参数,目前还没有用到。

1、tomcat\webapps\cas\WEB-INF\deployerConfigContext.xml

<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

p:httpClient-ref="httpClient"/>

增加参数p:requireSecure="false",是否需要安全验证,即HTTPS,false为不采用,加上去之后如下:

<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

p:httpClient-ref="httpClient" p:requireSecure="false"/>

2、Tomcat 6.0\webapps\cas\WEB-INF\spring-configuration\

ticketGrantingTicketCookieGenerator.xml

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"

p:cookieSecure="true"

p:cookieMaxAge="-1"

p:cookieName="CASTGC"

p:cookiePath="/cas" />

参数p:cookieSecure="true",同理为HTTPS验证相关,TRUE为采用HTTPS验证,与deployerConfigContext.xml的参数保持一致。

参数p:cookieMaxAge="-1",简单说是COOKIE的最大生命周期,-1为无生命周期,即只在当前打开的IE窗口有效,IE关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于0的数字,比如3600等,意思是在3600秒内,打开任意IE窗口,都不需要验证。

warnCookieGenerator.xml

<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"

p:cookieSecure="true"

p:cookieMaxAge="-1"

p:cookieName="CASPRIVACY"

p:cookiePath="/cas" />

两个参数与上面同理。

3、TICKET的生命周期也可以在web.xml加这个参数实现:

<!-- Timeout for granting tickets -->

<context-param>

<param-name>edu.yale.its.tp.cas.grantingTimeout</param-name>

<param-value>7200</param-value>

</context-param>

wps_clip_image-22420[3][1]

2.4HTTPS验证配置

证书的制作使用keytool工具,进入CMD,输入keytool,回车测试,如果出现帮助,则说明环境变量配置正确,如果没有,则配置PATH,加入%JAVA_HOME%\BIN。

wps_clip_image-32639[3][1]

下面开始制作,先找好存放证书的位置:

(1) keytool -genkey -alias tomcatcas -keystore tomcatcas -keyalg RSA -validity 3666

这一步是制作密钥库。

wps_clip_image-32105[3][1]

注意:名字与姓氏要输入主机名或域名或localhost,不能随意输入。密码为自己设定。下面的密码直接回车。

(2)keytool -export -alias tomcatcas -keystore tomcatcas -file tomcatcas.crt

这一步是把密钥库导出为证书文件。

wps_clip_image-3739[3][1]

注意:密码为上一步设定的密码。

(3)keytool -import -alias tomcatcas -file tomcatcas.crt

-keystore %JAVA_HOME%/jre/lib/security/cacerts

这一步是把证书导入TOMCAT。

后面的路径为TOMCAT使用的JRE路径,JDK1.6安装有两个目录JDK1.6和JRE1.6,TOMCAT6.0只要JRE1.6即可,通常在安装时,也都是默认到JRE1.6,这时要输入%JRE_HOME%\lib\security\cacerts,这个地方必须要确认准确。

注意:这里需要输入密码,此密码不是前面设定的密码,是系统默认的密码changeit

wps_clip_image-13922[3][1]

(4)配置tomcat6.0\conf\server.xml

<!--

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"

maxThreads="150" scheme="https" secure="true"

clientAuth="false" sslProtocol="TLS"

/>

à

加入密钥库文件和密码(前面设定的密码),修改为

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"

maxThreads="150" scheme="https" secure="true"

clientAuth="false" sslProtocol="TLS"

keystoreFile="d:\data\tomcatcas.keystore"

keystorePass="123456"

/>

注意:密钥库文件的要放到安全的位置,不能被随意移动。

wps_clip_image-30824[3][1]

完成后,可重启TOMCAT,进行测试。

wps_clip_image-25588[3][1]

2.5自定义页面

CAS页面文件放在Tomcat 6.0\webapps\cas\WEB-INF\view\jsp中,页面配置文件在Tomcat 6.0\webapps\cas\WEB-INF\classes中,比如default_views.properties,在这里指定登录用哪个页面,注销用哪个页面等等。

在Tomcat 6.0\webapps\cas\WEB-INF\cas.properties文件指定使用哪个皮肤页面。

下面开始操作:

1、进入Tomcat 6.0\webapps\cas\WEB-INF\view\jsp,复制default文件夹,并改名,再复制回来,如myth。

2、进入Tomcat 6.0\webapps\cas\WEB-INF\classes,复制default_views.properties,并改名再复制回来,如myth_views.properties。打开文件,把里面的目录..\jsp\default\ui修改为自己的路径,如..\jsp\myth\ui。

3、进入Tomcat 6.0\webapps\cas\WEB-INF,打开cas.properties文件,修改

cas.viewResolver.basename=myth_views

完成以上3步后,打开IE,进入http://localhost:8080/cas进行测试,如果成功则进行下一步。

wps_clip_image-13159[3][1]

注意:文件中logout使用的类。

4、修改页面样式

wps_clip_image-23028[3][1]

wps_clip_image-1552[3][1]

最后一点说明,Tomcat 6.0\webapps\cas\WEB-INF\cas-server.xml中,有个BEAN:

<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver"

p:order="0">

<property name="basenames">

<list>

<value>${cas.viewResolver.basename}</value>

<value>protocol_views</value>

</list>

</property>

</bean>

这个不要修改,否则出现http code 500 for url 。

3. 配置JAVA CLIENT

3.1HTTPS验证

1、把casclient.jar、cas-client-3.1.8.jar复制到你的jsp工程的WEB-INF\lib里;

2、修改web.xml,增加下面的代码:

<!-- 用于单点退出 -->

<listener>  <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>

</listener>

<filter>

<filter-name>CAS Single Sign Out Filter</filter-name>

<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>

</filter>

<!-- 用于单点登录 -->

<filter>

<filter-name>CAS Filter</filter-name>

<filter-class>edu.yale.its.tp.cas.client.filter.CASFilter</filter-class>

<!—下面两个为验证地址,即cas server的地址,如果使用https验证,地址为https://hostname:8443字样- ->

<init-param>

<param-name>edu.yale.its.tp.cas.client.filter.loginUrl</param-name>

<param-value>http://c7.byd.com:8080/cas/login</param-value>

</init-param>

<init-param>

<param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>

<param-value>http://c7.byd.com:8080/cas/serviceValidate</param-value>

</init-param>

<!—本工程的URL,被拦截的地址- ->

<init-param>

<param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>

<param-value>localhost:8080</param-value>

</init-param>

</filter>

<filter-mapping>

<filter-name>CAS Single Sign Out Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

<filter-mapping>

<filter-name>CAS Filter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

wps_clip_image-18214[3][1]

3、在页面获得验证返回的用户

用户可以通过以下方式,从JSP或servlet中获取通过认证的用户名:

String username = (String)session.getAttribute(edu.yale.its.tp.cas.client.filter.CASFilter.CAS_FILTER_USER);

获得更完整的受认证用户信息对象CASReceipt Java Bean,可以使用以下语句:

CASReceipt  receipt = (CASReceipt )session.getAttribute(edu.yale.its.tp.cas.client.filter.CASFilter.CAS_FILTER_RECEIPT);

wps_clip_image-1425[3][1]

完成以上后,进行测试。出现错误时,请查看tomcat\logs文件解决。

3.2HTTP验证

1、修改casclient.jar中文件,如图:

wps_clip_image-3963[3][1]

2、重新做jar包,复制到工程目录

wps_clip_image-945[3][1]

完成后,做好测试。登录成功后的样子,如下图:

wps_clip_image-3301[3][1]

4. 配置DOTNET CLIENT

1、把DotNetCasClient.cs复制到DotNet项目

wps_clip_image-11445[3][1]

wps_clip_image-5166[3][1]

2、新建loginPage.aspx,Page_Load加入代码

DotNetCASClient.DotNetCASClientServiceValidate client = new DotNetCASClient.DotNetCASClientServiceValidate();

String userId = client.Authenticate(Request, Response, false);

if (userId.Equals("failed"))

{

//Handle the auth failure...

}

else

{

Session[DotNetCASClient.SessionHandle.getUserSession()] = userId;

FormsAuthentication.RedirectFromLoginPage(userId, false);

}

注意:FormsAuthentication.RedirectFromLoginPage这个方法。

wps_clip_image-23925[3][1]

3、在Default.aspx的Page_Load内加入获得用户的代码:

wps_clip_image-17724[3][1]

4、修改web.config文件

<?xml version="1.0" encoding="utf-8"?>

<configuration>

<appSettings>

<add key="casLoginURL" value="http://c7.byd.com:8080/cas/login" />

<add key="casValidateURL" value="http://c7.byd.com:8080/cas/serviceValidate" />

<add key="serviceURL" value="http://localhost/dq/Default.aspx" />

</appSettings>

<connectionStrings/>

<system.web>

<authentication mode="Forms" >

<forms name="casauth" loginUrl="loginPage.aspx" />

</authentication>

<authorization>

<deny users="?" />

</authorization>

</system.web>

</configuration>

wps_clip_image-15104[3][1]

完成后,运行测试,效果如下图:

wps_clip_image-10908[3][1]

Cas为用户名。