SpringSecurity身份验证详细介绍
SpringSecurity提供了对身份验证的全面支持。架构组件
下面描述SpringSecurity在Servlet身份验证中使用的主要架构组件。SecurityContextHolder:SecurityContextHolder是SpringSecurity存储身份验证的详细信息的地方。SecurityContext:从SecurityContextHolder中获取,包含当前认证用户的Authentication信息。Authentication:可以是AuthenticationManager的输入,以提供用户已提供的用于身份验证的凭据,也可以是来自SecurityContext的当前用户。GrantedAuthority:在Authentication上授予主体的授权(即角色、范围等)。AuthenticationManager:定义SpringSecurity的过滤器如何执行身份验证的API。ProviderManager:AuthenticationManager最常见的实现。AuthenticationProvider:由ProviderManager用于执行特定类型的身份验证。使用AuthenticationEntryPoint请求凭证:用于客户端请求凭证(即重定向到一个登录页面,发送一个WWWAuthenticate响应,等等)。AbstractAuthenticationProcessingFilter:用于身份验证的基本过滤器。这还可以很好地了解高层次的身份验证流程以及各个部分如何协同工作。身份验证机制UsernameandPassword:如何使用用户名密码进行身份验证。OAuth2。0Login:OAuth2。0登录使用OpenID连接和非标准的OAuth2。0登录(例如GitHub)。SAML2。0Login:使用SAML2。0登录。CentralAuthenticationServer(CAS):支持CAS。RememberMe:如何记住用户会话过期。JAASAuthentication:使用JAAS进行身份验证。OpenID:OpenID身份验证(不要与OpenIDConnect混淆)。PreAuthenticationScenarios:使用外部机制(如SiteMinder或JavaEE安全性)进行身份验证,但仍然使用Springsecurity进行授权和保护,以防止常见攻击。X509Authentication:X509身份验证。SecurityContextHolder
SpringSecurity的身份验证模型的核心是SecurityContextHolder。它包含SecurityContext。
SecurityContextHolder介绍
SecurityContextHolder是SpringSecurity存储身份验证的详细信息的地方。SpringSecurity并不关心SecurityContextHolder是如何填充的。如果它包含一个值,那么它就被用作当前经过身份验证的用户。
指定用户身份验证的最简单方法是直接设置SecurityContextHolder。
示例:设置SecurityContextHolderSecurityContextcontextSecurityContextHolder。createEmptyContext();AuthenticationauthenticationnewTestingAuthenticationToken(username,password,ROLEUSER);context。setAuthentication(authentication);SecurityContextHolder。setContext(context);
我们首先创建一个空的SecurityContext。
重要的是要创建一个新的SecurityContext实例,而不是使用SecurityContextHolder。getContext()。setAuthentication(authentication),以避免多线程之间的竞争条件。
接下来,我们创建一个新的Authentication对象。
SpringSecurity并不关心在SecurityContext上设置了什么类型的Authentication实现。
这里我们使用TestingAuthenticationToken,因为它非常简单。更常见的生产场景是UsernamePasswordAuthenticationToken(userDetails,password,authorities)。
最后,我们在SecurityContextHolder上设置SecurityContext。
SpringSecurity将使用此信息进行授权。
如果希望获得关于经过身份验证的主体的信息,可以通过访问SecurityContextHolder来实现。
示例:访问当前认证用户SecurityContextcontextSecurityContextHolder。getContext();Authenticationauthenticationcontext。getAuthentication();Stringusernameauthentication。getName();Objectprincipalauthentication。getPrincipal();C?extendsGrantedAuthorityauthoritiesauthentication。getAuthorities();
默认情况下,SecurityContext使用ThreadLocal来存储这些详细信息,这意味着SecurityContext对于同一个线程中的方法总是可用的,即使SecurityContext没有显式地作为参数传递给这些方法。以这种方式使用ThreadLocal是相当安全的,如果在处理当前主体的请求后小心地清除线程。SpringSecurity的FilterChainProxy确保SecurityContext总是被清除。
有些应用程序并不完全适合使用ThreadLocal,因为它们处理线程的特定方式。例如,Swing客户机可能希望Java虚拟机中的所有线程都使用相同的安全上下文。
SecurityContextHolder可以在启动时配置一个策略,以指定您希望如何存储上下文。对于独立的应用程序,您将使用SecurityContextHolder。MODEGLOBAL策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder。MODEINHERITABLETHREADLOCAL实现的。您可以通过两种方式更改默认SecurityContextHolder。MODETHREADLOCAL的模式。第一个是设置系统属性,第二个是调用SecurityContextHolder上的静态方法。大多数应用程序不需要改变默认设置,但如果需要,请查看SecurityContextHolder的JavaDoc以了解更多信息。SecurityContext
SecurityContext从SecurityContextHolder中获取。SecurityContext包含一个Authentication对象。Authentication
在SpringSecurity中,Authentication有两个主要目的:AuthenticationManager的输入,用于提供用户为进行身份验证而提供的凭证。在此场景中使用时,isAuthenticated()返回false。表示当前通过身份验证的用户。当前的Authentication可以从SecurityContext中获取。
Authentication包含如下内容:principal:用户标识。当使用用户名密码进行身份验证时,这通常是UserDetails的一个实例credentials:通常指一个密码。在许多情况下,这将在用户身份验证后清除,以确保不会泄漏。authorities:GrantedAuthoritys是授予用户的高级权限。一些例子是角色或作用域。GrantedAuthority
GrantedAuthority为用户被授予的高级权限。一些例子是角色或作用域。
GrantedAuthority可以从Authentication。getAuthorities()方法获得。此方法提供了一个GrantedAuthority对象集合。GrantedAuthority是授予主体的权限,这并不奇怪。这样的权限通常是角色,例如ROLEADMINISTRATOR或ROLEHRSUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。SpringSecurity的其他部分能够解释这些权限,并期望它们存在。当使用基于用户名密码的身份验证时,GrantedAuthority通常由UserDetailsService加载。
通常,GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,您不太可能拥有一个GrantedAuthority来表示对Employee对象编号54的权限,因为如果有数千个这样的权限,您将很快耗尽内存(或者至少导致应用程序花很长时间来验证用户)。当然,SpringSecurity是专门设计来处理这一常见需求的,但是您可以使用项目的域对象安全功能来实现这一目的。AuthenticationManager
AuthenticationManager是定义SpringSecurity的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即SpringSecurity的Filterss)在SecurityContextHolder上设置返回的Authentication。如果你没有集成SpringSecurity的过滤器,你可以直接设置SecurityContextHolder,而不需要使用AuthenticationManager。
虽然AuthenticationManager的实现可以是任何内容,但最常见的实现是ProviderManager。ProviderManager
ProviderManager是AuthenticationManager最常用的实现。ProviderManager委托给AuthenticationProviders列表。每个AuthenticationProvider都有机会表明身份验证应该是成功的或失败的,或者表明它不能做出决定,并允许下游的AuthenticationProvider来做出决定。如果配置的AuthenticationProviders中没有一个可以进行身份验证,那么身份验证将失败,因为ProviderNotFoundException是特殊的AuthenticationException,表示ProviderManager没有配置成支持传递给它的身份验证类型。
ProvviderManager介绍
实际上,每个AuthenticationProvider都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider可能能够验证用户名密码,而另一个AuthenticationProvider可能能够验证SAML断言。这允许每个AuthenticationProvider执行特定类型的身份验证,同时支持多种类型的身份验证,并且只公开一个AuthenticationManagerbean。
ProviderManager还允许配置一个可选的父AuthenticationManager,当AuthenticationProvider不能执行身份验证时,会咨询该父AuthenticationManager。父类可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。
ProviderManagerParent介绍
事实上,多个ProviderManager实例可能共享相同的父AuthenticationManager。这在多个SecurityFilterChain实例具有某些共同身份验证(共享的父类AuthenticationManager)和不同身份验证机制(不同的ProviderManager实例)的场景中有些常见。
多个ProviderManager同一个Parent介绍
默认情况下,ProviderManager将尝试从成功的身份验证请求返回的Authentication对象中清除任何敏感凭证信息。这可以防止密码等信息在HttpSession中保留的时间超过必要时间
当您使用用户对象的缓存(例如,在无状态应用程序中提高性能)时,这可能会导致问题。如果Authentication包含对缓存中的对象(如UserDetails实例)的引用,并且该引用已删除其凭证,那么将不再能够根据缓存的值进行身份验证。如果您正在使用缓存,则需要考虑到这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中复制一个对象。或者,您可以禁用ProviderManager上的eraseCredentialsAfterAuthentication属性AuthenticationProvider
多个AuthenticationProviders可以被注入到ProviderManager中。每个AuthenticationProvider执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名密码的身份验证,而JwtAuthenticationProvider支持验证JWT令牌。使用AuthenticationEntryPoint请求凭证
AuthenticationEntryPoint用于从客户端发送一个HTTP响应请求凭证。有时客户端将主动包括凭证,例如请求资源的用户名密码。在这些情况下,SpringSecurity不需要提供从客户端请求凭证的HTTP响应,因为它们已经包含在其中了。
在其他情况下,客户端将向未被授权访问的资源发出未经身份验证的请求。在本例中,AuthenticationEntryPoint的实现用于客户端的请求凭证。AuthenticationEntryPoint实现可能执行重定向到一个登录页面,响应一个WWWAuthenticate头,等等。AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter被用作验证用户凭据的基本过滤器。在验证凭证之前,SpringSecurity通常使用AuthenticationEntryPoint请求凭证。接下来,AbstractAuthenticationProcessingFilter可以验证提交给它的任何身份验证请求。
AbstractAuthenticationProcessingFilter介绍
当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter从HttpServletRequest创建一个Authentication来进行身份验证。创建的身份验证类型依赖于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的用户名和密码创建UsernamePasswordAuthenticationToken。
接下来,将Authentication传递给AuthenticationManager进行身份验证。
如果身份验证失败,则失败:清除SecurityContextHolder。RememberMeServices。loginFail被调用。请记住我没有配置,这是不允许操作的。AuthenticationFailureHandler被调用。
如果身份验证成功,则成功。SessionAuthenticationStrategy会在新登录时得到通知。Authentication在SecurityContextHolder上设置。稍后,securitycontextpersistencfilter将SecurityContext保存到HttpSession。RememberMeServices。loginSuccess被调用。请记住我没有配置,这是不允许操作的。ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent。AuthenticationSuccessHandler被调用。UsernamePasswordAuthentication
验证用户身份的最常见方法之一是验证用户名和密码。因此,SpringSecurity提供了对使用用户名和密码进行身份验证的全面支持。
读取用户名和密码
SpringSecurity提供了以下内置机制来从HttpServletRequest读取用户名和密码:表单登录基本身份验证摘要式身份验证
存储机制
每种受支持的读取用户名和密码的机制都可以利用任何受支持的存储机制:使用内存身份验证的简单存储使用JDBC身份验证的关系数据库使用UserDetailsService自定义数据存储使用LDAP身份验证的LDAP存储表单登录
SpringSecurity支持通过html表单提供用户名和密码。下面详细介绍基于表单的身份验证如何在SpringSecurity中工作。
让我们看看基于表单的登录是如何在SpringSecurity中工作的。首先,我们将看到如何将用户重定向到登录表单页面。
重定向到登录页面
该图构建了我们的SecurityFilterChain图解。
首先,用户向未授权的资源private发出未经身份验证的请求。
SpringSecurity的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
由于用户没有经过身份验证,ExceptionTranslationFilter将启动StartAuthentication并发送一个重定向到配置了AuthenticationEntryPoint的登录页面。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的一个实例。
然后,浏览器将请求重定向到登录页面。
应用程序内的某些东西必须呈现登录页面。
提交用户名和密码后,UsernamePasswordAuthenticationFilter将对用户名和密码进行验证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,所以下面的流程图看起来应该很相似。
验证用户名和密码
该图构建了我们的SecurityFilterChain图解。
当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码创建UsernamePasswordAuthenticationToken,这是一种Authentication类型。
接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
如果身份验证失败,则失败清除SecurityContextHolder。RememberMeServices。loginFail被调用。请记住我没有配置,这是不允许操作的。AuthenticationFailureHandler被调用。
如果身份验证成功,则成功。SessionAuthenticationStrategy会在新登录时得到通知。Authentication在SecurityContextHolder上设置。稍后,securitycontextpersistencfilter将SecurityContext保存到HttpSession。RememberMeServices。loginSuccess被调用。请记住我没有配置,这是不允许操作的。ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent。AuthenticationSuccessHandler被调用。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter保存的请求。
SpringSecurity表单登录在默认情况下是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供基于表单的登录。
一个最小的、显式的Java配置如下所示:protectedvoidconfigure(HttpSecurityhttp){http。。。。formLogin(withDefaults());}
在这个配置中,SpringSecurity将呈现一个默认的登录页面。大多数生产应用程序都需要一个自定义的登录表单。
下面的配置演示了如何在表单中提供自定义登录页面。protectedvoidconfigure(HttpSecurityhttp)throwsException{http。。。。formLogin(formform。loginPage(login)。permitAll());}
当在SpringSecurity配置中指定登录页面时,您将负责呈现该页面。下面是一个Thymeleaf模板,它生成一个符合login登录页面的HTML登录表单:
示例:登录表单
路径:srcmainresourcestemplateslogin。html!DOCTYPEhtmlhtmlxmlnshttp:www。w3。org1999xhtmlxmlns:thhttps:www。thymeleaf。orgheadtitlePleaseLogIntitleheadbodyh1PleaseLogInh1Invalidusernameandpassword。Youhavebeenloggedout。bodyhtml
关于默认HTML表单有几个关键点:表单应该执行到login的post提交。该表单将需要包括一个CSRF令牌,由Thymeleaf自动包含。表单应该在名为username的参数中指定用户名表单应该在名为password的参数中指定密码如果发现HTTP参数error,则表示用户未能提供有效的用户名密码如果发现HTTP参数logout,则表示用户注销成功
许多用户只需要定制登录页面。但是,如果需要的话,上面的一切都可以通过额外的配置进行定制。
如果您正在使用SpringMVC,您将需要一个控制器,将GETlogin映射到我们创建的登录模板。LoginController的最小示例如下:ControllerclassLoginController{GetMapping(login)Stringlogin(){}}基本身份验证
下面详细介绍SpringSecurity如何为基于servlet的应用程序提供对基本HTTP身份验证的支持。
让我们看看HTTP基本身份验证是如何在SpringSecurity中工作的。首先,我们看到WWWAuthenticate头被发回给一个未经过身份验证的客户端。
发送WWWAuthenticate头
该图构建了我们的SecurityFilterChain图解。
首先,用户向未授权的资源private发出未经身份验证的请求。
SpringSecurity的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
由于用户没有经过身份验证,ExceptionTranslationFilter将启动启动身份验证。配置的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint的实例,它发送一个WWWAuthenticate头。RequestCache通常是一个不保存请求的NullRequestCache,因为客户端能够重复它最初请求的请求。
当客户端接收到WWWAuthenticate报头时,它知道应该用用户名和密码重试。下面是正在处理的用户名和密码的流程。
验证用户名和密码
该图构建了我们的SecurityFilterChain图解。
当用户提交他们的用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种身份验证类型。
接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
如果身份验证失败,则失败清除SecurityContextHolder。RememberMeServices。loginFail被调用。请记住我没有配置,这是不允许操作的。AuthenticationEntryPoint被调用来触发WWWAuthenticate再次发送。
如果身份验证成功,则成功。在SecurityContextHolder上设置Authentication。RememberMeServices。loginSuccess被调用。请记住我没有配置,这是不允许操作的。BasicAuthenticationFilter调用FilterChain。doFilter(request,response)来继续应用程序逻辑的其余部分。
默认情况下,SpringSecurity的HTTP基本身份验证支持是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供HTTPBasic。
一个最小的,显式的配置如下所示:protectedvoidconfigure(HttpSecurityhttp){http。。。。httpBasic(withDefaults());}摘要式身份验证
下面详细介绍SpringSecurity如何提供摘要身份验证支持,摘要身份验证是由DigestAuthenticationFilter提供的。
您不应该在现代应用程序中使用摘要身份验证,因为它被认为不安全。最明显的问题是必须以明文、加密或MD5格式存储密码。所有这些存储格式都是不安全的。相反,您应该使用单向自适配密码哈希(即bCrypt,PBKDF2,SCrypt等)存储凭证,这是摘要认证不支持的。
摘要身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭证永远不会通过网络以明文发送。许多浏览器支持摘要身份验证。
管理HTTP摘要身份验证的标准由RFC2617定义,它更新了RFC2069规定的摘要身份验证标准的早期版本。大多数用户代理实现RFC2617。SpringSecurity的摘要认证支持与RFC2617规定的auth质量保护(qop)兼容,它还提供了与RFC2069的向后兼容性。摘要身份验证被认为是一个更有吸引力的选择,如果你需要使用未加密的HTTP(即没有TLSHTTPS),并希望最大限度地安全性的身份验证过程。无论如何,每个人都应该使用HTTPS。
摘要中心身份验证是一个nonce。这是服务器生成的值。SpringSecurity的nonce采用以下格式:
示例:摘要语法base64(expirationTime:md5Hex(expirationTime:key))expirationTime:Thedateandtimewhenthenonceexpires,expressedinmillisecondskey:Aprivatekeytopreventmodificationofthenoncetoken
你需要确保你配置了不安全的明文密码存储使用NoOpPasswordEncoder。以下是使用Java配置配置摘要认证的示例:AutowiredUserDetailsServiceuserDetailsSDigestAuthenticationEntryPointentryPoint(){DigestAuthenticationEntryPointresultnewDigestAuthenticationEntryPoint();result。setRealmName(MyAppRelam);result。setKey(3028472bda344501bfd8a355c42bdf92);}DigestAuthenticationFilterdigestAuthenticationFilter(){DigestAuthenticationFilterresultnewDigestAuthenticationFilter();result。setUserDetailsService(userDetailsService);result。setAuthenticationEntryPoint(entryPoint());}protectedvoidconfigure(HttpSecurityhttp)throwsException{http。。。。exceptionHandling(ee。authenticationEntryPoint(authenticationEntryPoint()))。addFilterBefore(digestFilter());}基于内存的身份验证
SpringSecurity的InMemoryUserDetailsManager实现了UserDetailsService,以支持在内存中检索的基于用户名密码的身份验证。InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。当SpringSecurity配置为接受用户名密码进行身份验证时,将使用基于UserDetails的身份验证。
在这个示例中,我们使用SpringBootCLI对password的密码进行编码,并获得编码后的密码{bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW。
示例:InMemoryUserDetailsManager的java配置示例BeanpublicUserDetailsServiceusers(){UserDetailsuserUser。builder()。username(user)。password({bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW)。roles(USER)。build();UserDetailsadminUser。builder()。username(admin)。password({bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW)。roles(USER,ADMIN)。build();returnnewInMemoryUserDetailsManager(user,admin);}
上面的示例以安全的格式存储密码,但是在开始体验方面还有很多不足之处。在下面的示例中,我们利用了User。withDefaultPasswordEncoder来确保存储在内存中的密码是受保护的。但是,它不能通过反编译源代码来防止获得密码。出于这个原因,User。withDefaultPasswordEncoder应该只用于入门,而不是用于生产。
示例:使用User。withDefaultPasswordEncoder的InMemoryUserDetailsManagerBeanpublicUserDetailsServiceusers(){ThebuilderwillensurethepasswordsareencodedbeforesavinginmemoryUserBuilderusersUser。withDefaultPasswordEncoder();UserDetailsuserusers。username(user)。password(password)。roles(USER)。build();UserDetailsadminusers。username(admin)。password(password)。roles(USER,ADMIN)。build();returnnewInMemoryUserDetailsManager(user,admin);}基于JDBC的身份验证
SpringSecurity的JdbcDaoImpl实现了UserDetailsService来提供对使用JDBC检索的基于用户名密码的身份验证的支持。JdbcUserDetailsManager扩展了JdbcDaoImpl,通过UserDetailsManager接口提供对UserDetails的管理。当SpringSecurity配置为接受用户名密码进行身份验证时,将使用基于UserDetails的身份验证。
在下面的内容中,我们将讨论:Spring安全JDBC身份验证使用的默认模式设置数据源JdbcUserDetailsManagerBean
默认模式
SpringSecurity为基于JDBC的身份验证提供默认查询。本节提供与默认查询相对应的默认模式。您将需要调整模式,以匹配与您正在使用的查询和数据库方言相匹配的定制。
用户模式
JdbcDaoImpl需要表来加载用户的密码、帐户状态(启用或禁用)和权限(角色)列表。需要的默认模式可以在下面找到。
默认模式也公开为一个名为orgspringframeworksecuritycoreuserdetailsjdbcusers。ddl的类路径资源。
示例:默认用户模式createtableusers(usernamevarcharignorecase(50)notnullprimarykey,passwordvarcharignorecase(500)notnull,enabledbooleannotnull);createtableauthorities(usernamevarcharignorecase(50)notnull,authorityvarcharignorecase(50)notnull,constraintfkauthoritiesusersforeignkey(username)referencesusers(username));createuniqueindexixauthusernameonauthorities(username,authority);
Oracle是一种流行的数据库选择,但是需要略微不同的模式。您可以在下面找到用于用户的默认Oracle模式。
针对Oracle的默认用户模式CREATETABLEUSERS(USERNAMENVARCHAR2(128)PRIMARYKEY,PASSWORDNVARCHAR2(128)NOTNULL,ENABLEDCHAR(1)CHECK(ENABLEDIN(Y,N))NOTNULL);CREATETABLEAUTHORITIES(USERNAMENVARCHAR2(128)NOTNULL,AUTHORITYNVARCHAR2(128)NOTNULL);ALTERTABLEAUTHORITIESADDCONSTRAINTAUTHORITIESUNIQUEUNIQUE(USERNAME,AUTHORITY);ALTERTABLEAUTHORITIESADDCONSTRAINTAUTHORITIESFK1FOREIGNKEY(USERNAME)REFERENCESUSERS(USERNAME)ENABLE;
组模式
如果您的应用程序需要组,您将需要提供组模式。组的默认模式可以在下面找到。
默认组模式:createtablegroups(idbigintgeneratedbydefaultasidentity(startwith0)primarykey,groupnamevarcharignorecase(50)notnull);createtablegroupauthorities(groupidbigintnotnull,authorityvarchar(50)notnull,constraintfkgroupauthoritiesgroupforeignkey(groupid)referencesgroups(id));createtablegroupmembers(idbigintgeneratedbydefaultasidentity(startwith0)primarykey,usernamevarchar(50)notnull,groupidbigintnotnull,constraintfkgroupmembersgroupforeignkey(groupid)referencesgroups(id));
设置数据源
在配置JdbcUserDetailsManager之前,必须创建一个数据源。在我们的示例中,我们将设置一个使用默认用户模式初始化的嵌入式数据源。
示例:嵌入式数据来源BeanDataSourcedataSource(){returnnewEmbeddedDatabaseBuilder()。setType(H2)。addScript(classpath:orgspringframeworksecuritycoreuserdetailsjdbcusers。ddl)。build();}
在生产环境中,您将希望确保建立到外部数据库的连接。
JdbcUserDetailsManagerBean
在这个示例中,我们使用SpringBootCLI对password的密码进行编码,并获得编码后的密码{bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW。
示例:JdbcUserDetailsManagerBeanUserDetailsManagerusers(DataSourcedataSource){UserDetailsuserUser。builder()。username(user)。password({bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW)。roles(USER)。build();UserDetailsadminUser。builder()。username(admin)。password({bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW)。roles(USER,ADMIN)。build();JdbcUserDetailsManagerusersnewJdbcUserDetailsManager(dataSource);users。createUser(user);users。createUser(admin);}
UserDetails
UserDetails由UserDetailsService返回。DaoAuthenticationProvider验证UserDetails,然后返回一个Authentication,该Authentication有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails。
UserDetailsService
DaoAuthenticationProvider使用UserDetailsService检索用户名、密码和其他属性,以验证用户名和密码。SpringSecurity提供了UserDetailsService的内存和JDBC实现。
您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。例如,以下将自定义身份验证,假设CustomUserDetailsService实现了UserDetailsService:
只有当AuthenticationManagerBuilder没有被填充并且AuthenticationProviderBean没有被定义时才会使用。BeanCustomUserDetailsServicecustomUserDetailsService(){returnnewCustomUserDetailsService();}
PasswordEncoder
SpringSecurity的servlet通过与PasswordEncoder集成来支持安全存储密码。定制SpringSecurity使用的PasswordEncoder实现可以通过公开PasswordEncoderBean来完成。
DaoAuthenticationProvider
DaoAuthenticationProvider是一个AuthenticationProvider实现,它利用UserDetailsService和PasswordEncoder来验证用户名和密码。让我们看看DaoAuthenticationProvider是如何在SpringSecurity中工作的。图中解释了读取用户名和密码中的AuthenticationManager如何工作的细节。
使用DaoAuthenticationProvider
读取用户名和密码的身份验证过滤器将UsernamePasswordAuthenticationToken传递给AuthenticationManager,这是由ProviderManager实现的。
ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider。
DaoAuthenticationProvider从UserDetailsService中查找UserDetails。
然后,DaoAuthenticationProvider使用PasswordEncoder验证上一步返回的UserDetails上的密码。
当身份验证成功时,返回的身份验证类型为UsernamePasswordAuthenticationToken,并且具有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails。最终,返回的UsernamePasswordAuthenticationToken将由身份验证过滤器在SecurityContextHolder上设置。基于LDAP的身份验证
LDAP经常被企业用作用户信息的中心存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。
当SpringSecurity被配置为接受用户名密码进行身份验证时,SpringSecurity将使用基于LDAP的身份验证。但是,尽管利用用户名密码进行身份验证,但它并没有使用UserDetailsService集成,因为在绑定身份验证中,LDAP服务器没有返回密码,因此应用程序不能执行密码验证。
对于如何配置LDAP服务器,有许多不同的场景,因此SpringSecurity的LDAP提供者是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可以配置为处理各种情况的缺省实现。
先决条件
在尝试将LDAP与SpringSecurity一起使用之前,您应该熟悉LDAP。下面的链接很好地介绍了相关的概念,并提供了使用免费LDAP服务器OpenLDAP设置目录的指南:https:www。zytrax。combooksldap。熟悉一些用于从Java访问LDAP的JNDIapi可能也很有用。我们在LDAP提供程序中没有使用任何第三方LDAP库(Mozilla、JLDAP等),但是SpringLDAP得到了广泛的使用,所以如果您计划添加自己的自定义,对该项目有所了解可能会有所帮助。
在使用LDAP身份验证时,一定要确保正确配置LDAP连接池。如果您不熟悉如何做到这一点,可以参考JavaLDAP文档(https:docs。oracle。comjavasejnditutorialldapconnectconfig。html)。
设置嵌入式LDAP服务器
您需要做的第一件事是确保有一个LDAPServer来指向您的配置。为简单起见,最好从嵌入式LDAPServer开始。SpringSecurity支持使用以下任意一种:嵌入式UnboundID服务器嵌入式ApacheDS服务器
在下面的示例中,我们将下面的users。ldif作为类路径资源来初始化嵌入的LDAP服务器,其中用户user和admin的密码都是password。
users。ldif的内容dn:ougroups,dcspringframework,dcorgobjectclass:topobjectclass:organizationalUnitou:groupsdn:oupeople,dcspringframework,dcorgobjectclass:topobjectclass:organizationalUnitou:peopledn:uidadmin,oupeople,dcspringframework,dcorgobjectclass:topobjectclass:personobjectclass:organizationalPersonobjectclass:inetOrgPersoncn:RodJohnsonsn:Johnsonuid:adminuserPassword:passworddn:uiduser,oupeople,dcspringframework,dcorgobjectclass:topobjectclass:personobjectclass:organizationalPersonobjectclass:inetOrgPersoncn:DianneEmusn:Emuuid:useruserPassword:passworddn:cnuser,ougroups,dcspringframework,dcorgobjectclass:topobjectclass:groupOfNamescn:useruniqueMember:uidadmin,oupeople,dcspringframework,dcorguniqueMember:uiduser,oupeople,dcspringframework,dcorgdn:cnadmin,ougroups,dcspringframework,dcorgobjectclass:topobjectclass:groupOfNamescn:adminuniqueMember:uidadmin,oupeople,dcspringframework,dcorg
嵌入式UnboundID服务器
如果你想使用UnboundID,请指定以下依赖项:
UnboundID依赖项Maven:dependencygroupIdcom。unboundidgroupIdunboundidldapsdkartifactIdversion4。0。14versionscoperuntimescopedependency
然后可以配置嵌入式LDAP服务器
示例:嵌入式LDAP服务器配置BeanUnboundIdContainerldapContainer(){returnnewUnboundIdContainer(dcspringframework,dcorg,classpath:users。ldif);}
嵌入式ApacheDS服务器
SpringSecurity使用不再维护的ApacheDS1。x。不幸的是ApacheDS2。x只发布了里程碑版本,没有稳定的版本。一旦ApacheDS2。x稳定版本发布了,我们会考虑更新。
如果你想使用ApacheDS,那么指定以下依赖项:
ApacheDS的Maven依赖项:dependencygroupIdorg。apache。directory。servergroupIdapachedscoreartifactIdversion1。5。5versionscoperuntimescopedependencydependencygroupIdorg。apache。directory。servergroupIdapachedsserverjndiartifactIdversion1。5。5versionscoperuntimescopedependency
然后可以配置嵌入式LDAP服务器:
示例:嵌入式LDAP服务器配置BeanApacheDSContainerldapContainer(){returnnewApacheDSContainer(dcspringframework,dcorg,classpath:users。ldif);}
LDAPContextSource
一旦LDAP服务器指向您的配置,您需要将SpringSecurity配置为指向应该用于对用户进行身份验证的LDAP服务器。这是通过创建LDAPContextSource来完成的,它相当于JDBC数据源。
示例:LDAPContextSourceContextSourcecontextSource(UnboundIdContainercontainer){returnnewDefaultSpringSecurityContextSource(ldap:localhost:53389dcspringframework,dcorg);}
Authentication
SpringSecurity的LDAP支持不使用UserDetailsService,因为LDAP绑定身份验证不允许客户端读取密码,甚至是密码的哈希版本。这意味着SpringSecurity无法读取密码并对其进行身份验证。
因此,LDAP支持是使用LdapAuthenticator接口实现的。LdapAuthenticator还负责检索任何必需的用户属性。这是因为属性上的权限可能取决于所使用的身份验证类型。例如,如果绑定为用户,可能需要使用用户自己的权限读取它们。
SpringSecurity提供了两个LdapAuthenticator实现:使用绑定验证使用密码身份验证
使用绑定验证
绑定身份验证是使用LDAP对用户进行身份验证的最常用机制。在绑定身份验证中,用户凭证(即用户名密码)被提交给LDAP服务器,由LDAP服务器对用户进行身份验证。使用绑定身份验证的好处是,用户的秘密信息(即密码)不需要暴露给客户端,这有助于防止它们泄露。
下面是绑定身份验证配置的示例。BeanBindAuthenticatorauthenticator(BaseLdapPathContextSourcecontextSource){BindAuthenticatorauthenticatornewBindAuthenticator(contextSource);authenticator。setUserDnPatterns(newString〔〕{uid{0},oupeople});}BeanLdapAuthenticationProviderauthenticationProvider(LdapAuthenticatorauthenticator){returnnewLdapAuthenticationProvider(authenticator);}
这个简单的示例通过在提供的模式中替换用户登录名并尝试将登录密码绑定为该用户来获得用户DN。如果您的所有用户都存储在目录中的单个节点下,那么这是可以的。如果你想要配置一个LDAP搜索过滤器来定位用户,你可以使用以下方法:BeanBindAuthenticatorauthenticator(BaseLdapPathContextSourcecontextSource){StringsearchBStringfilter(uid{0});FilterBasedLdapUserSearchsearchnewFilterBasedLdapUserSearch(searchBase,filter,contextSource);BindAuthenticatorauthenticatornewBindAuthenticator(contextSource);authenticator。setUserSearch(search);}BeanLdapAuthenticationProviderauthenticationProvider(LdapAuthenticatorauthenticator){returnnewLdapAuthenticationProvider(authenticator);}
如果与上面的ContextSource定义一起使用,这将使用(uid{0})作为过滤器在DNoupeople,dcspringframework,dcorg下执行搜索。同样,用户登录名被替换为筛选器名称中的参数,因此它将搜索uid属性等于用户名的条目。如果没有提供用户搜索库,则从根目录执行搜索。
使用密码身份验证
密码比较是将用户提供的密码与存储在存储库中的密码进行比较。这可以通过检索密码属性的值并在本地进行检查来完成,也可以通过执行LDAP比较操作来完成,在该操作中,将提供的密码传递给服务器进行比较,而永远不会检索真正的密码值。如果密码用随机的盐值正确哈希,则无法进行LDAP比较。
示例:最小密码比较配置BeanPasswordComparisonAuthenticatorauthenticator(BaseLdapPathContextSourcecontextSource){returnnewPasswordComparisonAuthenticator(contextSource);}BeanLdapAuthenticationProviderauthenticationProvider(LdapAuthenticatorauthenticator){returnnewLdapAuthenticationProvider(authenticator);}
下面是一个更高级的配置,包含一些自定义。
示例:密码比较配置BeanPasswordComparisonAuthenticatorauthenticator(BaseLdapPathContextSourcecontextSource){PasswordComparisonAuthenticatorauthenticatornewPasswordComparisonAuthenticator(contextSource);authenticator。setPasswordAttributeName(pwd);authenticator。setPasswordEncoder(newBCryptPasswordEncoder());}BeanLdapAuthenticationProviderauthenticationProvider(LdapAuthenticatorauthenticator){returnnewLdapAuthenticationProvider(authenticator);}
指定密码属性为pwd
使用BCryptPasswordEncoder
LdapAuthoritiesPopulator
SpringSecurity的ldapauthortiespopulator用于确定为用户返回什么权限。
示例:LdapAuthoritiesPopulator配置BeanLdapAuthoritiesPopulatorauthorities(BaseLdapPathContextSourcecontextSource){StringgroupSearchBDefaultLdapAuthoritiesPopulatorauthoritiesnewDefaultLdapAuthoritiesPopulator(contextSource,groupSearchBase);authorities。setGroupSearchFilter(member{0});}BeanLdapAuthenticationProviderauthenticationProvider(LdapAuthenticatorauthenticator,LdapAuthoritiesPopulatorauthorities){returnnewLdapAuthenticationProvider(authenticator,authorities);}
ActiveDirectory
ActiveDirectory支持它自己的非标准身份验证选项,并且正常的使用模式不太适合标准LdapAuthenticationProvider。通常使用域用户名(格式为userdomain)执行身份验证,而不是使用LDAP专有名称。为了简化这一点,SpringSecurity提供了一个为典型的ActiveDirectory设置定制的身份验证提供程序。
配置ActiveDirectoryLdapAuthenticationProvider非常简单。您只需要提供域名和提供服务器地址的LDAPURL。下面是一个配置示例:BeanActiveDirectoryLdapAuthenticationProviderauthenticationProvider(){returnnewActiveDirectoryLdapAuthenticationProvider(example。com,ldap:company。example。com);}会话管理
HTTP会话相关的功能是通过SessionManagementFilter和SessionAuthenticationStrategy接口的组合来处理的,该接口由过滤器委托给它。典型的应用包括会话固定保护、攻击预防、会话超时检测和限制通过身份验证的用户可以同时打开的会话数量。检测超时
您可以配置SpringSecurity来检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过会话管理元素实现的:protectedvoidconfigure(HttpSecurityhttp){http。sessionManagement()。invalidSessionUrl(invalidSession。htm);}
注意,如果您使用这种机制来检测会话超时,如果用户登出,然后在没有关闭浏览器的情况下重新登录,它可能会错误地报告错误。这是因为当您使会话失效时,会话cookie不会被清除,即使用户已经注销,它也会被重新提交。你可以在登出时显式地删除JSESSIONIDcookie,例如在登出处理程序中使用以下语法:protectedvoidconfigure(HttpSecurityhttp){http。logout()。deleteCookies(JSESSIONID);}
不幸的是,这不能保证对每个servlet容器都适用,所以您需要在您的环境中测试它。
如果您在代理服务器后运行应用程序,您还可以通过配置代理服务器来删除会话cookie。例如,使用ApacheHTTPD的modheaders,下面的指令会在登出请求的响应中使JSESSIONID过期,从而删除JSESSIONIDcookie(假设应用程序部署在路径为tutorial下):
HeaderalwayssetSetCookieJSESSIONID;PExpiresThu,01Jan197000:00:00GMT
LocationMatch并发会话控制
如果您希望对单个用户登录到您的应用程序的能力进行限制,SpringSecurity通过以下简单的附加功能提供了开箱即用的支持。首先,你需要将以下侦听器添加到你的web。xml文件中,以保持SpringSecurity关于会话生命周期事件的更新:listenerlistenerclassorg。springframework。security。web。session。HttpSessionEventPublisherlistenerclasslistener
然后将以下几行添加到你的应用程序上下文中:http。。。sessionmanagementconcurrencycontrolmaxsessions1sessionmanagementhttp
这将防止用户多次登录,第二次登录将导致第一次登录无效。通常您希望防止第二次登录,在这种情况下您可以使用:http。。。sessionmanagementconcurrencycontrolmaxsessions1errorifmaximumexceededtruesessionmanagementhttp
第二次登录将被拒绝。通过拒绝,我们的意思是,如果使用基于表单的登录,用户将被发送到authenticationfailureurl。如果第二次身份验证是通过另一种非交互机制进行的,比如rememberme,一个unauthorized(401)错误将被发送给客户端。如果希望使用错误页面,可以将属性sessionauthenticationerrorurl添加到会话管理元素。
如果您正在为基于表单的登录使用自定义身份验证过滤器,那么您必须显式地配置并发会话控制支持。会话固定攻击保护
会话固定攻击是一个潜在的风险,在这种情况下,恶意攻击者可能通过访问一个站点来创建一个会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。SpringSecurity通过在用户登录时创建新会话或更改会话ID来自动防止这种情况发生。如果您不需要这种保护,或者它与其他一些需求冲突,您可以使用上的sessionfixationprotection属性来控制行为,该属性有四个选项:none:不做任何事。原会话将保留。newSession:创建一个新的干净会话,而不复制现有的会话数据(与Springsecurity相关的属性仍将被复制)。migrateSession:创建一个新会话,并将所有现有会话属性复制到新会话。这是Servlet3。0或旧容器的默认设置。changeSessionId:不要创建新的会话。相反,使用Servlet容器提供的会话固定保护(HttpServletRequestchangeSessionId())。这个选项只在Servlet3。1(JavaEE7)和更新的容器中可用。在旧容器中指定它将导致异常。这是Servlet3。1和更新容器中的默认值。
当会话固定保护发生时,它会导致在应用程序上下文中发布SessionFixationProtectionEvent。如果您使用changeSessionId,此保护也将导致任何javax。servlet。http。httpessionidlistener被通知,所以如果您的代码侦听这两个事件,请谨慎使用。SessionManagementFilter
SessionManagementFilter检查SecurityContextRepository的内容和反对的当前SecurityContextHolder内容是否在当前请求用户已经通过身份验证,通常由非交互式验证机制,如preauthentication或rememberme。如果存储库包含安全上下文,则过滤器不执行任何操作。如果没有,并且本地线程SecurityContext包含一个(非匿名的)Authentication对象,那么过滤器假定它们已经通过堆栈中先前的过滤器进行了身份验证。然后它将调用已配置的SessionAuthenticationStrategy。
如果用户当前没有经过身份验证,该过滤器将检查是否请求了一个无效的会话ID(例如,由于超时),并将调用配置的InvalidSessionStrategy(如果设置了一个)。最常见的行为就是重定向到一个固定的URL,这被封装在标准实现SimpleRedirectInvalidSessionStrategy中。SessionAuthenticationStrategy
SessionAuthenticationStrategy被SessionManagementFilter和AbstractAuthenticationProcessingFilter使用,所以如果你使用一个定制的表单登录类,例如,你将需要将它注入到这两个类中。在这种情况下,结合命名空间和自定义bean的典型配置可能是这样的:httpcustomfilterpositionFORMLOGINFILTERrefmyAuthFiltersessionmanagementsessionauthenticationstrategyrefsashttpbeans:beanidmyAuthFilterclassorg。springframework。security。web。authentication。UsernamePasswordAuthenticationFilterbeans:propertynamesessionAuthenticationStrategyrefsas。。。beans:beanbeans:beanidsasclassorg。springframework。security。web。authentication。session。SessionFixationProtectionStrategy
请注意,如果您将bean存储在实现httpessionbindinglistener的会话中(包括Spring会话范围内的bean),使用缺省的SessionFixationProtectionStrategy可能会导致问题。有关这个类的更多信息,请参阅Javadoc。ConcurrencyControl
SpringSecurity能够防止主体对同一应用程序的并发身份验证次数超过指定的次数。许多isv利用这一点来实施许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,您可以阻止用户Batman从两个不同的会话登录到web应用程序。您可以使他们之前的登录失效,也可以在他们试图再次登录时报告错误,以防止第二次登录。注意,如果您使用第二种方法,没有显式登出的用户(例如,刚刚关闭浏览器的用户)将不能再次登录,直到原始会话过期。
命名空间支持并发控制,因此请检查前面的命名空间章节以获得最简单的配置。但有时你需要定制一些东西。
该实现使用了SessionAuthenticationStrategy的一个特殊版本,称为ConcurrentSessionControlAuthenticationStrategy。
以前,并发身份验证检查是由ProviderManager进行的,它可以被注入一个ConcurrentSessionController。后者将检查用户是否试图超过允许的会话数。但是,这种方法要求预先创建HTTP会话,这是不可取的。在SpringSecurity3中,用户首先通过AuthenticationManager进行身份验证,一旦验证成功,将创建一个会话,并检查是否允许打开另一个会话。