spring 多租户_使用Spring Security的多租户应用程序的无状态会话

spring 多租户

无状态会话 从前, 我发表了一篇文章,解释了构建无状态会话的原理 。 巧合的是,我们再次为多租户应用程序执行同一任务。 这次,我们将解决方案集成到Spring Security框架中,而不是自己构建身份验证机制。

本文将解释我们的方法和实现。

业务需求

我们需要为Saas应用程序建立身份验证机制。 每个客户都通过专用子域访问该应用程序。 由于该应用程序将部署在云上,因此很明显,无状态会话是首选,因为它使我们能够轻松部署其他实例。

在项目词汇表中,每个客户都是一个站点。 每个应用程序都是一个应用程序。 例如,站点可以是Microsoft或Google。 应用可以是Gmail,GooglePlus或Google云端硬盘。 用户用于访问应用程序的子域将包括应用程序和网站。 例如,它可能看起来像microsoft.mail.somedomain.com或google.map.somedomain.com

用户一旦登录到一个应用程序,就可以访问同一站点的任何其他应用程序。 在一定的非活动时间后,会话将超时。

背景

无状态会话

具有超时的无状态应用程序并不是什么新鲜事物。 Play框架从2007年的第一个版本开始就一直是无状态的。很多年前,我们也切换到了无状态会话。 好处很明显。 您的负载均衡器不需要粘性; 因此,它更易于配置。 在浏览器中进行会话时,我们可以简单地引入新服务器以立即增加容量。 但是,缺点是您的会话不太大,也不是那么机密。

与会话存储在服务器中的有状态应用程序相比,无状态应用程序将会话存储在HTTP cookie中,该cookie不能超过4KB。 此外,由于它是cookie,因此建议开发人员仅将文本或数字存储在会话中,而不要存储复杂的数据结构。 会话存储在浏览器中,并在每个单个请求中传输到服务器。 因此,我们应该使会话尽可能小,并避免在其上放置任何机密数据。 简而言之,无状态会话迫使开发人员改变应用程序使用会话的方式。 应该是用户身份,而不是方便存储。

安全框架

Security Framework背后的想法非常简单,它有助于确定执行代码的原理,检查他是否有权执行某些服务,如果用户没有权限则抛出异常。 在实现方面,安全框架以AOP样式体系结构与您的服务集成。 每次检查都将在调用方法之前由框架进行。 实现权限检查的机制可以是过滤器或代理。

通常,安全框架会将主体信息存储在线程存储中(Java中的ThreadLocal)。 这就是为什么它可以随时为开发人员提供静态方法访问主体的原因。 我认为这是开发人员应该知道的一些事情; 否则,他们可能会在单独线程中运行的某些后台作业中实施权限检查或获取委托人。 在这种情况下,很明显,安全框架将无法找到主体。

单点登录

单一登录主要使用身份验证服务器来实现。 它独立于实现会话(无状态或有状态)的机制。 每个应用程序仍保持自己的会话。 首次访问应用程序时,它将与身份验证服务器联系以对用户进行身份验证,然后创建自己的会话。

思想的食物

从头开始构架或构建

由于无状态会话是标准,因此我们最大的顾虑是使用或不使用安全框架。 如果使用的话,那么Spring Security是最便宜,最快的解决方案,因为我们已经在应用程序中使用了Spring Framework。 为了利益,任何安全框架都为我们提供了快速和声明性的方式来声明评估规则。 但是,它不是业务逻辑感知的访问规则。 例如,我们可以定义仅代理可以访问产品,而不能定义一个代理只能访问属于他的某些产品。

在这种情况下,我们有两种选择,从头开始构建我们自己的业务逻辑许可权检查,或者构建两层许可权检查,一种仅基于角色,一种是业务逻辑感知。 比较两种方法之后,我们选择了后一种方法,因为它更便宜且构建速度更快。 我们的应用程序的功能将类似于任何其他Spring Security应用程序。 这意味着如果在没有会话的情况下访问受保护的内容,则用户将被重定向到登录页面。 如果会话存在,则用户将获得状态码403。如果用户访问具有有效角色但受未经授权的记录的受保护内容,则将获得401。

认证方式

接下来的问题是如何将我们的身份验证和授权机制与Spring Security集成在一起。 一个标准的Spring Security应用程序可以处理如下请求:

standard_spring_security

该图已简化,但仍给我们一个原始的想法。 如果请求是登录或注销,则前两个过滤器将更新服务器端会话。 此后,另一个过滤器帮助检查请求的访问权限。 如果权限检查成功,则另一个过滤器将帮助将用户会话存储到线程存储中。 之后,控制器将在正确的设置环境下执行代码。

对于我们来说,我们更喜欢创建身份验证机制,因为凭据需要包含网站域。 例如,我们可能有Xerox的Joe和WDS的Joe访问Saas应用程序。 由于Spring Security控制着准备身份验证令牌和身份验证提供程序的控制,因此我们发现在控制器级别实现自己的登录和注销要便宜得多,而不是花很多精力来定制Spring Security。

当我们实现无状态会话时,我们需要在这里实现两项工作。 首先,我们需要在进行任何授权检查之前从cookie构造会话。 我们还需要更新会话时间戳,以便每次浏览器向服务器发送请求时刷新会话。

由于先前决定在控制器中进行身份验证,因此我们在这里面临挑战。 我们不应该在控制器执行之前刷新会话,因为我们在此处进行身份验证。 但是,View Resolver附带了一些控制器方法,这些方法可立即写入输出流。 因此,执行控制器后,我们没有机会刷新Cookie。 最后,我们使用HandlerInterceptorAdapter选择一个稍有妥协的解决方案。 该处理程序拦截器使我们可以在每种控制器方法之前和之后进行额外的处理。 如果方法用于身份验证,则在控制器方法之后,而出于其他任何目的,则在控制器方法之前,我们实现刷新cookie。 新图应如下所示

stateless_spring_security

曲奇饼

为了有意义,用户应该只有一个会话cookie。 由于会话总是在每次请求后更改时间戳,因此我们需要在每个响应上更新会话。 通过HTTP协议,只有在Cookie与名称,路径和域匹配时才能执行此操作。

在满足此业务需求时,我们更喜欢尝试通过共享会话cookie来实现SSO的新方法。 如果每个应用程序都在相同的父域下并且理解相同的会话cookie,则实际上我们拥有一个全局会话。 因此,不再需要认证服务器。 为了实现这一愿景,我们必须将域设置为所有应用程序的父域。

性能

从理论上讲,无状态会话应该更慢。 假设服务器实现将会话表存储在内存中,则传入JSESSIONID cookie只会触发一次从会话表读取对象,以及一次可选的写入操作以更新上一次访问(用于计算会话超时)。 相反,对于无状态会话,我们需要计算哈希值以验证会话cookie,从数据库加载主体,分配新的时间戳并再次哈希。

但是,以今天的服务器性能而言,散列不应增加服务器响应时间的太多延迟。 更大的问题是从数据库查询数据,为此,我们可以使用缓存来加快速度。

在最佳情况下,如果没有进行数据库调用,则无状态会话可以与有状态会话足够接近地执行。 代替从由容器维护的会话表中加载,而是从由应用程序维护的内部缓存中加载会话。 在最坏的情况下,请求被路由到许多不同的服务器,并且主体对象存储在许多实例中。 这增加了额外的工作量,即每个服务器一次将主体加载到缓存。 尽管成本可能很高,但它仅偶尔出现一次。

如果我们将粘性路由应用于负载均衡器,则我们应该能够实现最佳情况。 这样,我们可以将无状态会话cookie视为与JSESSIONID相似的机制,但具有重建会话对象的后备功能。

实作

我已将此实现的示例发布到https://github.com/tuanngda/sgdev-blog存储库。 请检查无状态会话项目。 该项目需要一个mysql数据库才能工作。 因此,请在build.properties之后设置一个模式,或者修改属性文件以适合您的模式。

该项目包括用于在端口8686上启动tomcat服务器的maven配置。因此,您只需键入mvn cargo:run即可启动服务器。

这是项目层次结构:

项目

我打包了Tomcat 7服务器和数据库,以便它能在没有MySQL以外的任何其他安装的情况下工作。 Tomcat配置文件TOMCAT_HOME / conf / context.xml包含数据源声明和项目属性文件。

现在,让我们仔细看看实现。

届会

我们需要两个会话对象,一个代表会话cookie,一个代表我们在Spring安全框架内部构建的会话对象:

public class SessionCookieData {private int userId;private String appId;private int siteId;private Date timeStamp;
}

public class UserSession {private User user;private Site site;public SessionCookieData generateSessionCookieData(){return new SessionCookieData(user.getId(), user.getAppId(), site.getId());}
}

通过此组合,我们有了将会话对象存储在cookie和内存中的对象。 下一步是实现一种方法,该方法允许我们从cookie数据构建会话对象。

public interface UserSessionService {public UserSession getUserSession(SessionCookieData sessionData);
}

现在,又有一项服务可以从Cookie数据中检索并生成Cookie。

public class SessionCookieService {public Cookie generateSessionCookie(SessionCookieData cookieData, String domain);public SessionCookieData getSessionCookieData(Cookie sessionCookie);public Cookie generateSignCookie(Cookie sessionCookie);
}

到目前为止,我们提供的服务可帮助我们进行转换

Cookie –> SessionCookieData –> UserSession

会话–> SessionCookieData –> Cookie

现在,我们应该有足够的资料将无状态会话与Spring Security框架集成在一起。

与Spring安全性集成

首先,我们需要添加一个过滤器以根据Cookie构造会话。 因为这应该在权限检查之前发生,所以最好使用AbstractPreAuthenticatedProcessingFilter

@Component(value="cookieSessionFilter")
public class CookieSessionFilter extends AbstractPreAuthenticatedProcessingFilter {...@Overrideprotected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {SecurityContext securityContext = extractSecurityContext(request);if (securityContext.getAuthentication()!=null  && securityContext.getAuthentication().isAuthenticated()){UserAuthentication userAuthentication = (UserAuthentication) securityContext.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();SecurityContextHolder.setContext(securityContext);return session;}return new UserSession();}...}

上面的过滤器根据会话cookie构造主体对象。 筛选器还会创建一个PreAuthenticatedAuthenticationToken,稍后将用于身份验证。 显然,Spring不会理解该负责人。 因此,我们需要提供自己的AuthenticationProvider,它可以基于此主体来对用户进行身份验证。

public class UserAuthenticationProvider implements AuthenticationProvider {
@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) authentication;UserSession session = (UserSession)token.getPrincipal();if (session != null && session.getUser() != null){SecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(new UserAuthentication(session));return new UserAuthentication(session);}throw new BadCredentialsException("Unknown user name or password");}
}

这是春天的方式。 如果我们设法提供有效的身份验证对象,则对用户进行身份验证。 实际上,我们让用户针对每个单个请求通过会话cookie登录。

但是,有时我们需要更改用户会话,并且可以像往常一样在控制器方法中进行操作。 我们只需覆盖SecurityContext,它已在过滤器中更早设置。

还将UserSession存储到SecurityContextHolder,这有助于设置环境。 因为它是预身份验证过滤器,所以它对大多数请求(身份验证除外)都可以很好地工作。

我们应该手动更新身份验证方法中的SecurityContext:

public ModelAndView login(String login, String password, String siteCode) throws IOException{if(StringUtils.isEmpty(login) || StringUtils.isEmpty(password)){throw new HttpServerErrorException(HttpStatus.BAD_REQUEST, "Missing login and password");}User user = authService.login(siteCode, login, password);if(user!=null){SecurityContext securityContext = SecurityContextHolder.getContext();UserSession userSession = new UserSession();userSession.setSite(user.getSite());userSession.setUser(user);securityContext.setAuthentication(new UserAuthentication(userSession));}else{throw new HttpServerErrorException(HttpStatus.UNAUTHORIZED, "Invalid login or password");}return new ModelAndView(new MappingJackson2JsonView());}

刷新会议

到目前为止,您可能会注意到我们从未提到过编写cookie。 假设我们有一个有效的Authentication对象,并且我们的SecurityContext包含UserSession,则需要将此信息发送到浏览器很重要。 在生成HttpServletResponse之前,我们必须将会话cookie附加到它。 具有相同域和路径的cookie将替换浏览器保留的较旧的会话。

如上所述,刷新会话最好在控制器方法之后完成,因为我们在此处实现了身份验证。 但是,挑战是由Spring MVC的ViewResolver引起的。 有时,它这么快就写入OutputStream,以至于将cookie添加到响应中的任何尝试都是没有用的。 最后,我们提出了一种折衷解决方案,该解决方案在用于常规请求的控制器方法之前和在用于身份验证请求的控制器方法之后刷新会话。 要知道请求是否用于身份验证,我们在身份验证方法上放置一个注释。

@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;SessionUpdate sessionUpdateAnnotation = handlerMethod.getMethod().getAnnotation(SessionUpdate.class);if (sessionUpdateAnnotation == null){SecurityContext context = SecurityContextHolder.getContext();if (context.getAuthentication() instanceof UserAuthentication){UserAuthentication userAuthentication = (UserAuthentication)context.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();persistSessionCookie(response, session);}}}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;SessionUpdate sessionUpdateAnnotation = handlerMethod.getMethod().getAnnotation(SessionUpdate.class);if (sessionUpdateAnnotation != null){SecurityContext context = SecurityContextHolder.getContext();if (context.getAuthentication() instanceof UserAuthentication){UserAuthentication userAuthentication = (UserAuthentication)context.getAuthentication();UserSession session = (UserSession) userAuthentication.getDetails();persistSessionCookie(response, session);}}}}

结论

该解决方案对我们来说效果很好,但是我们没有把握这可能是最佳实践。 但是,它很简单,并且不需要花费很多精力来实施(大约需要3天的测试时间)。

如果您有更好的想法来与Spring建立无状态会话,请提供反馈。

翻译自: https://www.javacodegeeks.com/2014/09/stateless-session-for-multi-tenant-application-using-spring-security.html

spring 多租户

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/339770.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java api 1.6 下载_Java JDK API

JDKJavaDevelopmentKit是SunMicrosystems针对Java开发员的产品。自从Java推出以来,JDK已经成为使用最广泛的JavaSDK。JDK是整个Java的核心,包括了Java运行环境。相关软件软件大小版本说明下载地址jdk(Java Development Kit)是Sun Microsystems针对java开…

Java / Cloud:如何快速创建支持Kubernetes的REST微服务

可以肯定地说,如今微服务与云的结合风靡一时。 微服务的开发比以往任何时候都多,从而导致应用程序部署数量增加。 在过去的十年中,开发了诸如Docker和Kubernetes之类的容器化和编排工具,从而使微服务模式真正易于采用。 本文将教…

使用Spring Boot和Project Reactor处理SQS消息-第2部分

这是我关于使用Spring Boot和Project Reactor有效处理SQS消息的博客文章的后续文章 我在第一部分中列出了一些方法上的差距。 1.处理SQS客户端调用中的失败 2.该方法一次只能处理来自SQS的一条消息,如何并行化 3.它不处理错误,管道中的任何错误都会中…

java爬虫jsoup_Java爬虫之利用Jsoup自制简单的搜索引擎

内容导读在上述代码中,url为输入词条(暂时仅限于英文),进入while循环可一直搜索,当输入为’exit’时退出。contentText为该词条的百度百科简介的网页形式,通过正则表达式将其中的文字提取出来。代码虽然简洁,但是功能还…

matplotlib的默认字体_浅谈matplotlib默认字体设置探索

控制默认字体的设置根据官方文档https://matplotlib.org/tutorials/text/text_props.html#default-font可知:The base default font is controlled by a set of rcParams默认字体是由一组rcParams控制的。rcParamusage‘font.family"List of either names of f…

java空心菱形_java 空心菱形

分为两部分,先打印前四行,再打印后三行,int n 4;    //设初始值为4for(int i0;ifor(int j0;jSystem.out.print(" ");}for(int k0;kif(k0||k2*i) {    //打印前四行的*,中间部分输出空格System.out.print(&quo…

java接口版本控制_为什么要在Java中控制类和接口的可见性

java接口版本控制维护是软件开发的重要方面之一,并且经验证明,保持较低组件可视性的软件比暴露更多组件的软件更易于维护。 您不会在一开始就意识到它,但是在重新设计应用程序时会严重错过它。 由于保持向后兼容性是许多应用程序的“必须具备…

遮掩java_css之图片下方定位遮掩层

需要的效果如图,图片下方加个遮掩层:html:css:.listContent>div{width:300px;height: 300px;float: left;margin-top: 20px;margin-left: 20px;position:relative;}.mask{width:300px;height: 40px;background-color:#FFCCCC;p…

使用JDK的密码流的加密怪癖(以及如何做)

在我们的日常工作中,我们经常遇到经常性的主题,即将数据(例如文件)从一个位置传输到另一个位置。 这听起来像是一个非常简单的任务,但让我们通过声明这些文件可能包含机密信息并可以通过非安全的通信渠道进行传输这一事…

学java专科_专科学历可以学习java开发吗

学习Java的热潮越来越高涨,除了转行而来的人,很多刚毕业的学生也加入到其中。很多人都觉得学习Java需要有一个高学历作为基础,一些专科生在学习之前会犹豫,他们是否能学习Java,首先学程序开发,入行Java开发…

具有InlfuxDB的Spring Boot和Micrometer第3部分:Servlet和JDBC

在上一个博客中,我们使用由InfluxDB支持的千分尺设置了反应式应用程序。 在本教程中,我们将使用传统的带JDBC阻塞式Servlet的Spring Stack。 我选择的数据库是postgresql。 我将使用与先前博客文章相同的脚本。 因此,我们将拥有初始化数据库…

jsf xhtml调用方法_JSF的工作方式以及调试方法–可以使用polyglot吗?

jsf xhtml调用方法JSF不是我们通常认为的那样。 这也是一个调试起来可能有些棘手的框架,尤其是在初次遇到时。 在这篇文章中,让我们继续探讨为什么会出现这种情况,并提供一些JSF调试技术。 我们将讨论以下主题: JSF不是我们经常想…

将Auth0 OIDC(OAUTH 2)与授权(组和角色)集成

如果您正在使用Auth0对多个现有应用程序中的用户进行身份验证和授权,则可能需要将下一个Web应用程序与Auth0集成。 有多种方法可以执行此操作,例如,如果要将Jenkins与Auth0集成,则可以使用SAML v2;否则,可…

tomee_一罐将其全部统治:Apache TomEE + Shrinkwrap == JavaEE引导

tomee警告:我不是Spring Boot的专家。 我发现很多事情对此非常有趣,并且当然可以真正改善您的日常工作。 而且,我对Spring Boot没有任何反对,也没有开发或使用它的人。 但是我认为社区高估了该产品。 一年前,我开始收…

使用Spring Boot和Project Reactor处理SQS消息

我最近参与了一个项目,在该项目中,我不得不有效地处理通过AWS SQS Queue流入的大量消息。 在这篇文章(可能还有一篇)中,我将介绍使用出色的Project Reactor处理消息的方法。 以下是我要进行的设置: 设置本…

初级测试开发面试题_初级开发人员在编写单元测试时常犯的错误

初级测试开发面试题自从我编写第一个单元测试以来已经有10年了。 从那时起,我不记得我已经编写了成千上万的单元测试。 老实说,我在源代码和测试代码之间没有任何区别。 对我来说是同一回事。 测试代码是源代码的一部分。 在过去的3-4年中,我…

使用SoapUI调用安全WCF SOAP服务–第1部分,该服务

在这个由三部分组成的传奇中,我将演示如何使用SoapUI API工具来调用安全的SOAP服务。 首先,我将专注于创建服务,在接下来的文章中它将充当被测系统。 使用基本身份验证传输安全性机制维护对该服务中资源的访问。 Windows Communication Foun…

java简单系统_Java简单学生管理系统

Java简单学生管理系统这个不需要手动输入,笔记记录//studentpublic class student(){private String id;//学号private String name;//姓名private int age;//年龄public String getId() {return id;}public void setId(String id) {this.id id;}public String get…

kafka java编程demo_Kafka简单客户端编程实例

今天,我们给大家带来一篇如何利用Kafka的API进行客户端编程的文章,这篇文章很简单,就是利用Kafka的API创建一个生产者和消费者,生产者不断向Kafka写入消息,消费者则不断消费Kafka的消息。下面是具体的实例代码。一、创…

java我的世界极限生存_我的世界 1.7.10 极限生存整合包

整合包介绍:最近总有人觉得Minecraft很无聊,没有什么可玩的,或者觉得生存太简单 那么就来试试这个吧,全部是增强怪物的MOD,保证不无聊,保证不简单 基本上没有增加一些新的东西,只增加了几种怪物…