SSO 单点登录 sessionFilter 登录认证

382 阅读2分钟

引言

SSO 单点登录生成用户登入会话的通证,访问期间无需重复身份认证,即可根据 Session 和 Cookie 中保留的浏览器和服务器直接的交互数据,实现服务的持续安全访问。通过添加 sessionFilter 实现,可以处理自定义的登录认证方式。

SSO Configuration 配置 Bean

首先写入配置,注册 Filter,主要是通过 FilterRegistrationBean 注册 commonSessionFilter 和 localeResolverFilter 并指定过滤顺序。

@Configuration
@ConditionalOnProperty(prefix = "sso", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SsoConfiguration {

    @Bean
    SystemConfig systemConfig(){
        SystemConfig systemConfig = new SystemConfig() ;
        systemConfig.init();
        return systemConfig ;
    }

    // 注册filter
    @Bean
    public FilterRegistrationBean commonSessionFilter(){
        FilterRegistrationBean registration = new FilterRegistrationBean();
        CommonSessionFilter commonSessionFilter = new CommonSessionFilter() ;
        registration.setName("commonSessionFilter");
        registration.setFilter(commonSessionFilter);
        registration.addUrlPatterns("/*");
        registration.setOrder(2);
        return registration;
    }

    @Bean
    public FilterRegistrationBean localeResolverFilter(SystemConfig systemConfig){
        FilterRegistrationBean registration = new FilterRegistrationBean();
        LocaleResolverFilter localeResolverFilter = new LocaleResolverFilter() ;
        registration.setName("localeResolverFilter");
        registration.setFilter(localeResolverFilter);
        registration.addUrlPatterns("/*");
        registration.setOrder(3);
        return registration;
    }

    @Bean
    public BaseServiceContext baseServiceContext(@Value("${tenantApiKey}") String baseKey,
                                                 @Value("${tenantApiSecret}") String baseToken,
                                                 @Value("${sessionConfigHost}") String sessionConfigHost,
                                                 @Value("${tenantServiceRoute}") String tenantServiceRoute) {
        BaseEnvironment baseEnvironment = new BaseEnvironment();
        baseEnvironment.setBaseKey(baseKey);
        baseEnvironment.setBaseToken(baseToken);
        TenantEnvironment tenantEnvironment = new TenantEnvironment();

        if(xxx){
            tenantEnvironment.setHost(sessionConfigHost);
        } else {
            tenantEnvironment.setHost(tenantServiceRoute);
        }

        BaseServiceContext context = new BaseServiceContext();
        context.setBaseEnvironment(baseEnvironment);
        context.setTenantEnvironment(tenantEnvironment);
        return context;
    }

    @Bean
    public UserService interfaceInvokerProxyFactoryBean(BaseServiceContext baseServiceContext) {
        return baseServiceContext.getService(UserService.class);
    }

    @Bean
    public FilterRegistrationBean getContextFilterRegistrationBean(
            @Autowired(required = false) UserService baseUserService
    ) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.addUrlPatterns("/*");
        BaseUserFilter filter = new BaseUserFilter(baseUserService);
        registrationBean.setFilter(filter);
        registrationBean.setName("baseUserFilter");
        registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE);

        StandardLoggingUtil.info(LOGGER,"BaseUserFilter init success");
        return registrationBean;
    }

    // 触发构建PropertyUtils
    @Bean
    @ConditionalOnBean(Environment.class)
    PropertyUtils propertyUtils(){
        return new PropertyUtils() ;
    }

}

CommonSession 获取用户上下文

@Component
@Order(5)
public class IamUserContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
            throws ServletException, IOException {
            
        String userName = null;
        String tenantId = null;

        if (httpServletRequest.getSession() instanceof CommonSession) {
            CommonSession commonSession = (CommonSession) httpServletRequest.getSession();
            if (SessionUtils.isLogin(commonSession)) {
                userName = String.valueOf(commonSession.getAttribute(USERNAME));
                tenantId = commonSession.getCustomSessionAttribute(TENANT_ID);
                UserContextHolder.setUserContext(new UserContext(userName, tenantId));
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    }

filterChain.doFilter 按照指定顺序依次执行,如果都不能匹配上,会有一个 BaseUserFilter 兜底,返回 UserContext 推动系统下一步跳转登录或跳转服务。

header 携带验签信息

如果需要自定义访问认证和验签信息,可以用 HttpServletRequest 的 header 携带。包含验签部分的认证,需要额外做一步消息 body 的 cache 处理,因为消息体中数据只能取出一次,完成登录认证再到接口执行时取出会是空白。

@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@WebFilter(filterName = "RequestBodyCachingFilter", urlPatterns = "/*")
public class RequestBodyCachingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
                                    FilterChain filterChain) throws ServletException, IOException {
        String header = req.getHeader("x-header");
        if (Objects.isNull(header) || !header.equals("true")) {
            filterChain.doFilter(req, resp);
        } else {
            CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(req);
            filterChain.doFilter(cachedBodyHttpServletRequest, resp);
        }
    }

    public static class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

        private final byte[] cachedBody;

        public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
            super(request);
            InputStream requestInputStream = request.getInputStream();
            this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
        }

        @Override
        public ServletInputStream getInputStream() {
            return new CachedBodyServletInputStream(this.cachedBody);
        }

        @Override
        public BufferedReader getReader() {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
            return new BufferedReader(new InputStreamReader(byteArrayInputStream));
        }
    }

    public static class CachedBodyServletInputStream extends ServletInputStream {

        private final InputStream cachedBodyInputStream;

        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
        }

        @Override
        public boolean isFinished() {
            try {
                return cachedBodyInputStream.available() == 0;
            } catch (IOException e) {
                StandardLoggingUtil.error("cached body input stream  " + e);
            }
            return false;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int read() throws IOException {
            return cachedBodyInputStream.read();
        }
    }
}

改造 IamUserContextFilter,判断 header 并处理验签。验签过程:首先构造和上游相同的签名方法,然后用同一个密钥对消息生成签名,最后检验签名是否匹配。

// 构造签名内容
TreeMap<String, String> headerMap = new TreeMap<>();
headerMap.put("x-header", header);
headerMap.put("x-name", userName);
headerMap.put("x-access", accessId);
ByteString signatureContent;
try {
    signatureContent = composeSignatureContent(headerMap, httpServletRequest);
} catch (IOException e) {
    StandardLoggingUtil.error("compose signature content failed");
    throw new ErrorCodeException(ErrorCodes.fromErrorTemplate(FAILED_TO_COMPOSE_SIGNATURE_CONTENT));
}

// 验签
String signature = httpServletRequest.getHeader("x-signature");
ByteString skByteString = ByteString.encodeUtf8(openapiSk);
if (!signatureContent.hmacSha256(skByteString).base64().equals(signature)) {
    throw new ErrorCodeException(ErrorCodes.fromErrorTemplate(FAILED_TO_PASS_SIGNATURE_CHECK));

提供一种构造签名方法:

protected static ByteString composeSignatureContent(TreeMap<String, String> headerMap,
                                                    HttpServletRequest req) throws IOException {
    TreeMap<String, String> queryParamMap = new TreeMap<>();
    for (Map.Entry<String, String[]> entry : req.getParameterMap().entrySet()) {
        queryParamMap.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
    }
    ByteString byteString = new Buffer().writeUtf8(req.getRequestURI())
            .writeUtf8(connectParamMap(headerMap))
            .writeUtf8(connectParamMap(queryParamMap))
            .write(StreamUtils.copyToByteArray(req.getInputStream()))
            .readByteString();
    StandardLoggingUtil.info(LOGGER, "compose signature content " + byteString.utf8());
    return byteString;
}

private static String connectParamMap(TreeMap<String, String> paramMap) {
    List<String> items = new ArrayList<>();
    for (Map.Entry<String, String> entry : paramMap.entrySet()) {
        items.add(entry.getKey() + "=" + entry.getValue());
    }
    return org.apache.commons.lang3.StringUtils.join(items, "&");
}

总结

综上,SSO 提供了一种登录验证方法,并允许开发者通过改造 sessionFilter 自定义登录认证和验签方法。