基于Spring Boot的单点登录

 

前言:

    通过对CAS架构的学习,模仿实现了基于Cookie和过滤器的单点登录。并且利用Spring  Boot中的自配置,来移除客户端重复配置。

 

流程图:

  • 先大致讲下流程,用户请求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会传递给服务端,方便登录成功的重定向。

  • 过滤器一:
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);
}

    介绍完客户端的主要验证方案,来看看如何将客户端以插件的形式“配置”到各个需要验证的模块上。

  • 自配置:
<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封装了一些可配置的信息。

  • @ConfigurationProperties:将application.properties文件中设置的以“menghao.sso”+属性名的属性加载至该Bean,方便自配置时使用。

  SsoClientAutoConfiguration则是真正自配置的实现。

  • @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文件,并添加:

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

    这样,将客户端打成Jar包,并在需要的模块引入依赖,就可以在Spring Boot启动时,自动加载该配置类(前提是满足@ConditionOnXxx的条件)

    如果有额外的属性需要指定,可以通过additional-spring-configuration-metadata.json文件中声明,该文件与spring.factories在同一级目录,格式如下:

{
  "properties": [
    {
      "name": "menghao.sso.enabled",
      "type": "java.lang.Boolean",
      "description": "自定义单点登录客户端.",
      "defaultValue": false
    }
  ]
}
  • 配置属性: 

    只需要在需要的模块引入即可,指定服务端,客户端地址,拦截的url请求正则。

# 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架构有更好的理解,而进行的拆分整理,单纯的实现了基本功能,对于并发等情况未做考虑。

转载于:https://my.oschina.net/marvelcode/blog/1576104

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值