基於Spring Boot的單點登陸

 

前言:

    經過對CAS架構的學習,模仿實現了基於Cookie和過濾器的單點登陸。而且利用Spring  Boot中的自配置,來移除客戶端重複配置。java

  • 代碼地址:https://gitee.com/marvelcode/marvelcode-sso
  •  Lombok:經過對應的註解,在編譯源碼的時候生成對應的方法,保證源代碼的整潔。經常使用的有@Getter、@Setter、@Data等。

 

流程圖:

  • 先大體講下流程,用戶請求service-1,過濾器攔截請求後,從cookie中獲取TGT(一個證實用戶已登陸的票據),若是沒有取到,就重定向sso-server的登陸頁,並傳遞本來的請求(方便登陸成功後重定向回原url);
  • 用戶名密碼驗證經過後,就生成ST(一個與服務綁定的票據,說明用戶有權訪問該服務)拼接在原url後重定向,並同時生成cookie用於存放TGT(這裏用到了共享cookie的技巧);
  • 這時會再次被過濾器攔截,校驗ST的合法性,一樣是請求sso-server,在驗證經過後放行。
  • 若是用戶訪問service-2,過濾器檢測到TGT的存在,就會去sso-server驗證TGT,並在驗證經過後生成對應服務的ST,一樣的重定向...

客戶端:

  • 抽象過濾器:
package com.menghao.sso.client.filter;

import com.menghao.sso.client.util.CommonUtils;
import lombok.Setter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
 * <p>客戶端過濾器抽象類.<br>
 *
 * @author menghao.
 * @version 2017/11/15.
 */
public abstract class AbstractCasFilter implements Filter {

    protected final Log log = LogFactory.getLog(this.getClass());

    /*
        請求服務主機名
     */
    @Setter
    private String clientHost;
    /*
        cas服務端地址
     */
    @Setter
    protected String serverHost;

    private static final String HTTP = "http://";

    // ...省略若干構建url方法

    protected String makeOriginalRequest(HttpServletRequest request, HttpServletResponse response) {
        StringBuilder builder = new StringBuilder();
        builder.append(request.isSecure() ? "https://" : "http://");
        builder.append(clientHost);
        builder.append(request.getRequestURI());
        // 若是存在查詢參數,將參數抽取拼接
        if (StringUtils.hasLength(request.getQueryString())) {
            int index = request.getQueryString().indexOf(CommonUtils.ST_ID + "=");
            // 默認規則ticket放在查詢參數最後
            if (index == -1) {
                builder.append("?").append(request.getQueryString());
            } else if (index == 0) {
                // do nothing
            } else {
                index = request.getQueryString().indexOf("&" + CommonUtils.ST_ID + "=");
                if (index == -1) {
                    builder.append("?").append(request.getQueryString());
                } else {
                    builder.append("?").append(request.getQueryString().substring(0, index));
                }
            }
        }
        final String returnValue = response.encodeURL(builder.toString());
        if (log.isDebugEnabled()) {
            log.debug("serviceUrl make: " + returnValue);
        }
        return returnValue;
    }

    protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    /*
        此步對request和response作了統一轉型
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilterInternal((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    @Override
    public void destroy() {

    }

}

    抽象的主要目的,是爲了對ServletRequest 和 ServletResponse的統一轉型。其中代碼省略了不少構造url的方法:好比登陸、驗證、註銷等等。其中makeOriginalRequest是獲取本來的url請求(過濾掉ServiceTicket後的本來url請求),該方法構造的url會傳遞給服務端,方便登陸成功的重定向。git

  • 過濾器一:
package com.menghao.sso.client.filter;


import com.menghao.sso.client.util.CommonUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * <p>對TGT和ST有無校驗.<br>
 *
 * @author menghao.
 * @version 2017/11/15.
 */
public class AuthenticationFilter extends AbstractCasFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        Cookie cookie = WebUtils.getCookie(request, CommonUtils.TGT_ID);
        // 無TGT,說明未登陸
        if (null == cookie || cookie.getValue() == null) {
            String originalRequest = makeOriginalRequest(request, response);
            // 沒有ticket則重定向登陸
            String loginUrl = makeLoginRequest(originalRequest);
            response.sendRedirect(loginUrl);
            return;
        }
        // 有TGT,無ST,說明已登陸但登陸其餘系統
        String serviceTicket = request.getParameter(CommonUtils.ST_ID);
        if (!StringUtils.hasText(serviceTicket)) {
            String originalRequest = makeOriginalRequest(request, response);
            String validateRequest = makeValidateTGTRequest(originalRequest, cookie.getValue());
            response.sendRedirect(validateRequest);
            return;
        }
        // 具有TGT和ST
        filterChain.doFilter(request, response);
    }

}
  • 過濾器二:
package com.menghao.sso.client.filter;

import com.menghao.sso.client.model.ValidateBean;
import com.menghao.sso.client.util.CommonUtils;
import com.menghao.sso.client.validation.TicketValidator;
import com.menghao.sso.client.validation.ValidationException;
import lombok.Setter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * <p>對ST合法性校驗.<br>
 *
 * @author menghao.
 * @version 2017/11/15.
 */
public class CheckTicketFilter extends AbstractCasFilter {

    @Setter
    private TicketValidator ticketValidator;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        // 建立服務驗證請求
        String serviceTicket = request.getParameter(CommonUtils.ST_ID);
        String originalRequest = makeOriginalRequest(request, response);
        String validateRequest = makeValidateSTRequest(originalRequest);
        // 發送驗證請求
        try {
            ValidateBean validateBean = ValidateBean.builder().url(validateRequest).serviceTicket(serviceTicket).build();
            Boolean success = ticketValidator.validate(validateBean);
            if (success) {
                filterChain.doFilter(request, response);
                return;
            }
        } catch (ValidationException e) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            log.warn(e, e);
            throw new ServletException(e);
        }
    }

}

 

package com.menghao.sso.client.validation;

import com.menghao.sso.client.model.ValidateBean;
import lombok.Setter;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;


/**
 * <p>抽象校驗類.<br>
 *
 * @author menghao.
 * @version 2017/11/16.
 */
public abstract class AbstractTicketValidator implements TicketValidator {

    @Setter
    protected RestTemplate restTemplate;
    @Setter
    protected String casServerUrl;

    /**
     * 模版模式
     *
     * @param validateBean 驗證包裝類
     * @return Boolean 是否驗證經過
     * @throws ValidationException
     */
    @Override
    public Boolean validate(ValidateBean validateBean) throws ValidationException {
        try {
            ResponseEntity<Boolean> responseEntity =
                    restTemplate.getForEntity(validateBean.getUrl(), Boolean.class, validateBean.getServiceTicket());
            return parseResponse(responseEntity);
        } catch (RestClientException e) {
            throw new ValidationException(e);
        }
    }

    protected abstract Boolean parseResponse(ResponseEntity<Boolean> responseEntity);
}

    介紹完客戶端的主要驗證方案,來看看如何將客戶端以插件的形式「配置」到各個須要驗證的模塊上。web

  • 自配置:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

 

package com.menghao.sso.client.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * <p>客戶端配置屬性.<br>
 *
 * @author menghao.
 * @version 2017/11/6.
 */
@Data
@ConfigurationProperties(prefix = "menghao.sso")
public class SsoClientProperties {
    /*
        客戶端<host>:<port>
     */
    private String clientHost;
    /*
        服務端<host>:<port>
     */
    private String serverHost;
    /*
        限制登陸url,逗號分割
     */
    private String restrictUrls;

}

 

package com.menghao.sso.client.config;

import com.menghao.sso.client.filter.AuthenticationFilter;
import com.menghao.sso.client.filter.CheckTicketFilter;
import com.menghao.sso.client.filter.WrapInfoFilter;
import com.menghao.sso.client.validation.PersonTicketValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;

/**
 * <p>客戶端自動配置類.<br>
 *
 * @author menghao.
 * @version 2017/11/16.
 */
@Configuration
@EnableConfigurationProperties(SsoClientProperties.class)
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "menghao.sso", value = "enabled", matchIfMissing = false)
public class SsoClientAutoConfiguration {

    private SsoClientProperties ssoClientProperties;

    private List<String> urls;

    public SsoClientAutoConfiguration(SsoClientProperties ssoClientProperties) {
        this.ssoClientProperties = ssoClientProperties;
        String strictUrls = ssoClientProperties.getRestrictUrls();
        Assert.hasText(ssoClientProperties.getClientHost(), "服務主機地址必須指定");
        Assert.hasText(strictUrls, "攔截地址必須指定");
        // 初始化時,會將配置須要攔截的url分割成列表
        urls = Arrays.asList(strictUrls.split(","));
    }

    @Bean
    @ConditionalOnMissingBean(AuthenticationFilter.class)
    public FilterRegistrationBean registerAuthenticationFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        authenticationFilter.setServerHost(ssoClientProperties.getServerHost());
        authenticationFilter.setClientHost(ssoClientProperties.getClientHost());
        filterRegistrationBean.setFilter(authenticationFilter);
        // 配置過濾器須要攔截的url列表
        filterRegistrationBean.setUrlPatterns(urls);
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }

    // ...省略其餘過濾器配置

}

  SsoClientProperties封裝了一些可配置的信息。spring

  • @ConfigurationProperties:將application.properties文件中設置的以「menghao.sso」+屬性名的屬性加載至該Bean,方便自配置時使用。

  SsoClientAutoConfiguration則是真正自配置的實現。apache

  • @Configuration:將該類映射爲XML配置中的<beans>標籤;
  • @EnableConfigurationProperties:開啓對@ConfigurationProperties標記的Bean支持;
  • @ConditionalOnXxx:在知足指定條件下才使得該配置生效。其中多個註解同時標識時是條件與的關係。例如@ConditionalOnProperty(prefix = "menghao.sso", value = "enabled", matchIfMissing = false),就是在「menghao.sso.enabled」屬性未設置的狀況下配置不生效,即客戶端默認不開啓;
  •  @Bean:經過標識方法,將方法返回的Bean交由Spring管理,等同於XML配置的<bean>標籤。

    最後一步,在路徑/resources/META-INF路徑下,建立spring.factories文件,並添加:json

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.menghao.sso.client.config.SsoClientAutoConfiguration

    這樣,將客戶端打成Jar包,並在須要的模塊引入依賴,就能夠在Spring Boot啓動時,自動加載該配置類(前提是知足@ConditionOnXxx的條件)緩存

    若是有額外的屬性須要指定,能夠經過additional-spring-configuration-metadata.json文件中聲明,該文件與spring.factories在同一級目錄,格式以下:markdown

{
  "properties": [
    {
      "name": "menghao.sso.enabled",
      "type": "java.lang.Boolean",
      "description": "自定義單點登陸客戶端.",
      "defaultValue": false
    }
  ]
}
  • 配置屬性: 

    只須要在須要的模塊引入便可,指定服務端,客戶端地址,攔截的url請求正則。cookie

# true開啓,false關閉
menghao.sso.enabled = true
menghao.sso.cas-server-host = localhost:1000
menghao.sso.cas-client-host = localhost:1001
# 配置攔截的url,多個用逗號分割
menghao.sso.restrict-urls = /*

 

服務端:

    其中使用到了兩種票據,TicketGrantingTicket (TGT)和 ServiceTicket(ST)。在登陸成功時,往Cookie中放入TGT,在url上拼接ST;在校驗時,若是有ST,直接校驗,若是沒有則獲取TGT,校驗經過後授予ST,一切校驗失敗的行爲都會拋出ValidateFailException異常(自定義),並交由異常統一處理返回登陸界面。架構

  • 驗證與受權:    

    來看下主要的兩個業務實現類:

package com.menghao.sso.server.service;

import com.menghao.sso.server.exception.ValidateFailException;
import com.menghao.sso.server.model.Service;
import com.menghao.sso.server.model.credentials.Credentials;
import com.menghao.sso.server.model.credentials.UsernamePasswordCredentials;
import com.menghao.sso.server.model.ticket.ServiceTicket;
import com.menghao.sso.server.model.ticket.TicketGrantingTicket;
import com.menghao.sso.server.registry.TicketRegistry;
import com.menghao.sso.server.repository.UCredentialsRepository;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * <p>認證Service實現.<br>
 *
 * @author menghao.
 * @version 2017/11/17.
 */
@org.springframework.stereotype.Service
public class AuthenticationServiceImpl implements AuthenticationService {

    private final Log log = LogFactory.getLog(this.getClass());

    @Autowired
    private UCredentialsRepository uCredentialsRepository;
    @Autowired
    private TicketRegistry ticketRegistry;

    @Override
    public void validateCredentials(Credentials credentials) throws ValidateFailException {
        if (credentials instanceof UsernamePasswordCredentials) {
            UsernamePasswordCredentials usernamePasswordCredentials =
                    uCredentialsRepository.queryByProperty((UsernamePasswordCredentials) credentials);
            // 驗證經過
            if (usernamePasswordCredentials == null) {
                throw new ValidateFailException("用戶名與密碼不匹配");
            }
        } else {
            throw new ValidateFailException("暫不支持的認證方式");
        }
    }

    @Override
    public void validateGrantingTicket(String ticketGrantingTicketId) throws ValidateFailException {
        if (ticketGrantingTicketId == null) {
            throw new ValidateFailException("未檢測到票據信息,請登陸");
        }
        final TicketGrantingTicket ticketGrantingTicket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId);
        if (ticketGrantingTicket == null) {
            log.debug("TicketGrantingTicket [" + ticketGrantingTicket + "] does not exist.");
            throw new ValidateFailException("票據信息校驗未經過,請登陸");
        }

        if (ticketGrantingTicket.isExpired()) {
            log.debug("ServiceTicket [" + ticketGrantingTicket + "] has expired.");
            this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
            throw new ValidateFailException("身份驗證已過時,請從新登陸");
        }
        ticketGrantingTicket.updateLastTimeUsed();
    }

    @Override
    public void validateServiceTicket(String serviceTicketId, Service service) throws ValidateFailException {
        if (serviceTicketId == null || service == null) {
            throw new ValidateFailException("未檢測到票據信息,請登陸");
        }
        final ServiceTicket serviceTicket = (ServiceTicket) this.ticketRegistry.getTicket(serviceTicketId);
        if (serviceTicket == null) {
            log.debug("ServiceTicket [" + serviceTicketId + "] does not exist.");
            throw new ValidateFailException("票據信息校驗未經過,請登陸");
        }

        if (serviceTicket.isExpired()) {
            log.debug("ServiceTicket [" + serviceTicketId + "] has expired.");
            this.ticketRegistry.deleteTicket(serviceTicketId);
            throw new ValidateFailException("身份驗證已過時,請從新登陸");
        }
        serviceTicket.incrementCountOfUses();
        serviceTicket.updateLastTimeUsed();

        if (serviceTicket.isExpired()) {
            log.debug("ServiceTicket [" + serviceTicketId + "] has expired.");
            this.ticketRegistry.deleteTicket(serviceTicketId);
            throw new ValidateFailException("身份驗證已過時,請從新登陸");
        }

        if (!service.equals(serviceTicket.getService())) {
            log.debug("ServiceTicket [" + serviceTicketId + "] does not match supplied service.");
            throw new ValidateFailException("票據信息與服務不匹配,請登陸");
        }

    }

}

 

package com.menghao.sso.server.service;

import com.menghao.sso.server.exception.InvalidTicketException;
import com.menghao.sso.server.exception.ValidateFailException;
import com.menghao.sso.server.model.Principal;
import com.menghao.sso.server.model.Service;
import com.menghao.sso.server.model.SimplePrincipal;
import com.menghao.sso.server.model.credentials.Credentials;
import com.menghao.sso.server.model.credentials.UsernamePasswordCredentials;
import com.menghao.sso.server.model.ticket.ServiceTicket;
import com.menghao.sso.server.model.ticket.TicketGrantingTicket;
import com.menghao.sso.server.model.ticket.TicketGrantingTicketImpl;
import com.menghao.sso.server.model.validation.Authentication;
import com.menghao.sso.server.registry.ExpirationPolicy;
import com.menghao.sso.server.registry.TicketRegistry;
import com.menghao.sso.server.util.TicketIdGenerator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import java.util.HashMap;

/**
 * <p>受權Service實現.<br>
 *
 * @author menghao.
 * @version 2017/11/17.
 */
@org.springframework.stereotype.Service
public class AuthorizationServiceImpl implements AuthorizationService {

    private final Log log = LogFactory.getLog(this.getClass());

    @Autowired
    private TicketRegistry ticketRegistry;
    @Autowired
    private ExpirationPolicy expirationPolicy;

    @Override
    public String createTicketGrantingTicket(Credentials credentials) throws ValidateFailException {
        if (credentials instanceof UsernamePasswordCredentials) {
            String username = ((UsernamePasswordCredentials) credentials).getUsername();
            if (!StringUtils.hasText(username)) {
                throw new ValidateFailException("沒法獲取用戶名");
            }
            String tgtId = TicketIdGenerator.newTGTId();
            Principal principal = new SimplePrincipal(username);
            Authentication authentication = new Authentication(principal, new HashMap());
            ticketRegistry.addTicket(new TicketGrantingTicketImpl(tgtId, authentication, this.expirationPolicy));
            return tgtId;
        }
        return null;
    }

    @Override
    public String createServiceTicket(String ticketGrantingTicketId, Service service) throws ValidateFailException {
        if (!StringUtils.hasText(ticketGrantingTicketId)) {
            throw new ValidateFailException("未檢測到票據信息,請登陸");
        }
        TicketGrantingTicket ticketGrantingTicket = (TicketGrantingTicket) ticketRegistry.getTicket(ticketGrantingTicketId);

        if (ticketGrantingTicket == null) {
            throw new ValidateFailException("票據信息校驗未經過,請登陸");
        }

        if (ticketGrantingTicket.isExpired()) {
            throw new ValidateFailException("身份驗證已過時,請從新登陸");
        }

        final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
                TicketIdGenerator.newSTId(), service, this.expirationPolicy);

        this.ticketRegistry.addTicket(serviceTicket);

        log.info("Granted service ticket ["
                + serviceTicket.getId()
                + "] for service ["
                + service.getUrl()
                + "] for user ["
                + serviceTicket.getGrantingTicket().getAuthentication()
                .getPrincipal().getUrl() + "]");

        return serviceTicket.getId();
    }

    @Override
    public void destroyTicketGrantingTicket(String ticketGrantingTicketId) throws InvalidTicketException {
        if (!StringUtils.hasText(ticketGrantingTicketId)) {
            throw new InvalidTicketException();
        }
        ticketRegistry.deleteTicket(ticketGrantingTicketId);
    }
}

    這兩個類中幾乎包含了全部的服務端處理邏輯,Controller層就是藉助這兩個類的方法,拼裝後實現的。其中注入的 TicketRegistry(票據註冊)和 ExpirationPolicy(票據過時)代碼講解在下面。

  • 異常統一處理:
package com.menghao.sso.server.exception;

import lombok.Getter;

/**
 * <p>校驗失敗異常.<br>
 *
 * @author menghao.
 * @version 2017/11/20.
 */
public class ValidateFailException extends Exception {

    public ValidateFailException(String msg) {
        super();
        this.msg = msg;
    }

    public ValidateFailException(String service, String msg) {
        this(msg);
        this.service = service;
    }

    @Getter
    private String msg;

    @Getter
    private String service;
}

 

package com.menghao.sso.server.controller.advice;

import com.menghao.sso.server.exception.ValidateFailException;
import com.menghao.sso.server.util.CommonUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
 * <p>異常統一處理類.<br>
 *
 * @author menghao.
 * @version 2017/11/20.
 */
@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler(ValidateFailException.class)
    public ModelAndView validateException(ValidateFailException e) throws UnsupportedEncodingException {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("msg", URLEncoder.encode(e.getMsg(), "UTF-8"));
        modelAndView.addObject(CommonUtils.SERVICE, e.getService());
        modelAndView.setViewName("redirect:" + CommonUtils.LOGIN_URL);
        return modelAndView;
    }
}

    除了驗證經過的其餘任何狀況,如過時,不存在等等,都會拋出 ValidateFailException 異常,經過異常統一處理,重定向至登陸頁,並將提示信息封裝到 request 域中。

  • 票據註冊:

    爲了方便橫向擴展,將註冊策略抽象爲接口,目前只實現了一種:

package com.menghao.sso.server.registry;

import com.menghao.sso.server.model.ticket.Ticket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>默認的票據註冊實現.<br>
 *
 * @author menghao.
 * @version 2017/11/21.
 */
public final class DefaultTicketRegistry implements TicketRegistry {

    private final Log log = LogFactory.getLog(getClass());

    private final Map<String, Ticket> cache = new HashMap<String, Ticket>();

    public synchronized void addTicket(final Ticket ticket) {
        if (ticket == null) {
            throw new IllegalArgumentException("ticket cannot be null");
        }
        log.debug("Added ticket [" + ticket.getId() + "] to registry.");
        this.cache.put(ticket.getId(), ticket);
    }

    public synchronized Ticket getTicket(final String ticketId) {
        log.debug("Attempting to retrieve ticket [" + ticketId + "]");
        final Ticket ticket = this.cache.get(ticketId);
        if (ticket != null) {
            log.debug("Ticket [" + ticketId + "] found in registry.");
        }
        return ticket;
    }

    public synchronized boolean deleteTicket(final String ticketId) {
        log.debug("Removing ticket [" + ticketId + "] from registry");
        return (this.cache.remove(ticketId) != null);
    }

    public synchronized Collection getTickets() {
        return Collections.unmodifiableCollection(this.cache.values());
    }
}
  • 票據過時:

    爲了方便橫向擴展,將過時策略抽象爲接口,目前只實現了兩種:

package com.menghao.sso.server.registry;

import com.menghao.sso.server.model.ticket.Ticket;

/**
 * 基於過時時間:最近一次使用的使用
 */
public final class TimeoutExpirationPolicy implements ExpirationPolicy {

    private final long timeToKillInMilliSeconds;

    public TimeoutExpirationPolicy(final long timeToKillInMilliSeconds) {
        this.timeToKillInMilliSeconds = timeToKillInMilliSeconds;
    }

    public boolean isExpired(final Ticket ticket) {
        return (ticket == null) || (System.currentTimeMillis() - ticket.getLastTimeUsed() >= this.timeToKillInMilliSeconds);
    }
}

 

package com.menghao.sso.server.registry;

import com.menghao.sso.server.model.ticket.Ticket;

/**
 * 永不過時
 */
public final class NeverExpirationPolicy implements ExpirationPolicy {

    public boolean isExpired(final Ticket ticket) {
        return false;
    }
}
  • 自配置:
package com.menghao.sso.server.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * <p>服務端配置屬性Bean.<br>
 *
 * @author menghao.
 * @version 2017/11/17.
 */
@Data
@ConfigurationProperties(prefix = "menghao.sso.server")
public class SsoServerProperties {
    /*
        緩存策略
     */
    private String ticketCache = "default";
}

 

package com.menghao.sso.server.config;

import com.menghao.sso.server.registry.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * <p>服務端自配置類<br>
 *
 * @author menghao.
 * @version 2017/11/17.
 */
@Configuration
public class SsoServerAutoConfiguration {
    @Bean
    @ConditionalOnProperty(prefix = "menghao.sso.server", name = "ticketCache", havingValue = "default")
    public TicketRegistry defaultRegistry() {
        return new DefaultTicketRegistry();
    }

    @Bean
    public ExpirationPolicy expirationPolicy() {
        return new TimeoutExpirationPolicy(1000 * 60 * 60);
    }
}

    默認註冊策略,採用內存放置Map的形式,存儲 ticketId-Ticket鍵值對。默認的過時策略,1小時的間隔使用時間。

 

總結:

    目前只完成了單個請求的校驗邏輯,若是是服務間調用,按照Cas本來架構中,是以代理票據實現的,目前還不能支持。該架構只是爲了可以對Cas架構有更好的理解,而進行的拆分整理,單純的實現了基本功能,對於併發等狀況未作考慮。

相關文章
相關標籤/搜索