Spring Boot中集成Spring Security 专题
check to see if spring security is applied that the appropriate resources are permitted:
@Configuration public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { http .csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler) .accessDeniedHandler(accessDeniedHandler) .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().authorizeRequests() //.antMatchers("/actuator/**").permitAll() .antMatchers("/actuator/**").hasAuthority("ADMIN") .antMatchers( HttpMethod.GET, "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html**", "/webjars/**", "favicon.ico" ).permitAll() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated();http .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">) .headers() .cacheControl(); }
}
Or we could simply ignore these resources altogether
@Configuration public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception {web.ignoring() .antMatchers( "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html**", "/webjars/**");} }
https://github.com/springfox/springfox/blob/master/docs/asciidoc/common-problems.adoc
if 语句中条件判断就是检查当前的 url 请求是否是 logout-url 的配置值,接下来,获取用户的 authentication,并循环调用处理器链中各个处理器的 logout() 函数,前面在 parse 阶段说过,处理器链中有两个实例,处理会话的 SecurityContextLogoutHandler 及 remember-me 服务,我们来一一看看它们的 logout 函数实现:
2.1.0 SecurityContextLogoutHandler
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { Assert.notNull(request, "HttpServletRequest required"); if (this.invalidateHttpSession) { HttpSession session = request.getSession(false); if (session != null) {session.invalidate(); //使当前会话失效 } }SecurityContextHolder.clearContext(); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">清空安全上下文</span>
}
很简单,如果配置了登出以后使会话失效,则它调用 session 的 invalidate() 让会话过期,另外,清空安全上下文。
Spring Security
Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的 Security 框架。除了常规的 Authentication 和 Authorization 之外,Spring Security 还提供了诸如 ACLs,LDAP,JAAS,CAS 等高级特性以满足复杂场景下的安全需求。虽然功能强大,Spring Security 的配置并不算复杂(得益于官方详尽的文档),尤其在 3.2 版本加入 Java Configuration 的支持之后,可以彻底告别令不少初学者望而却步的 XML Configuration。在使用层面,Spring Security 提供了多种方式进行业务集成,包括注解,Servlet API,JSP Tag,系统 API 等。下面就结合一些示例代码介绍 Boot 应用中集成 Spring Security 的几个关键点。
1 核心概念
Principle(User), Authority(Role) 和 Permission 是 Spring Security 的 3 个核心概念。
跟通常理解上 Role 和 Permission 之间一对多的关系不同,在 Spring Security 中,Authority 和 Permission 是两个完全独立的概念,两者并没有必然的联系,但可以通过配置进行关联。
应用级别的安全主要分为“验证 (authentication) ”和“(授权) authorization ”两个部分。
这也是 Spring Security 主要需要处理的两个部分:
在 Spring Security 中,认证过程称之为 Authentication(验证),指的是建立系统使用者信息 (principal) 的过程。使用者可以是一个用户、设备、或者其他可以在我们的应用中执行某种操作的其他系统。
"Authorization" 指的是判断某个 principal 在我们的应用是否允许执行某个操作。在 进行授权判断之前,要求其所要使用到的规则必须在验证过程中已经建立好了。
这些概念是通用的,并不是只针对 "Spring Security"。
关于 Authentication:
Spring Security 中的验证 authentication 到底是什么?
让我们考虑一个每个人都熟悉的标准验证场景:
1、一个用户被提示使用用户名和密码登录
2、系统成功的验证了用户名与密码是匹配的
3、获取到用户的上下文信息 (角色列表等)
4、建立这个用户的安全上下文 (security context)
5、用户可能继续进行一些受到访问控制机制保护的操作,访问控制机制会依据当前安全上下文信息检查这个操作所需的权限。
前三条组成了验证过程,因此我们要看一下在 Spring Security 中这是如何发生的:
1、用户名和密码被获取到,并放入一个 UsernamePasswordAuthenticationToken 实例中 (Authentication 接口的一个实例,我们之前已经看到过)。
2、这个 token 被传递到一个 AuthenticationManager 实例中进行验证
3、在成功验证后, AuthenticationManager 返回一个所有字段都被赋值的 Authentication 对象实例
4、通过调用 SecurityContextHolder.getContext().setAuthentication(…) 创建安全上下文,通过返回的验证对象进行传递。
从这个角度来说,用户被认为已经成功验证。让我们来看一段样例代码:
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;public class AuthenticationExample {
private static AuthenticationManager authenticationManager = new SampleAuthenticationManager();</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span> main(String[] args) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { BufferedReader in </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> BufferedReader(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> InputStreamReader(System.in)); </span><span style="color: rgba(0, 0, 255, 1)">while</span> (<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) { System.out.println(</span>"Please enter your username:"<span style="color: rgba(0, 0, 0, 1)">); String name </span>=<span style="color: rgba(0, 0, 0, 1)"> in.readLine(); System.out.println(</span>"Please enter your password:"<span style="color: rgba(0, 0, 0, 1)">); String password </span>=<span style="color: rgba(0, 0, 0, 1)"> in.readLine(); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { Authentication request </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> UsernamePasswordAuthenticationToken(name, password); System.out.println(</span>"before:" +<span style="color: rgba(0, 0, 0, 1)"> request); Authentication result </span>=<span style="color: rgba(0, 0, 0, 1)"> authenticationManager.authenticate(request); System.out.println(</span>"after:" +<span style="color: rgba(0, 0, 0, 1)"> result); SecurityContextHolder.getContext().setAuthentication(result); </span><span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (AuthenticationException e) { System.out.println(</span>"Authentication failed: " +<span style="color: rgba(0, 0, 0, 1)"> e.getMessage()); } } System.out.println(</span>"Successfully authenticated. Security context contains: " +<span style="color: rgba(0, 0, 0, 1)"> SecurityContextHolder.getContext().getAuthentication()); }
}
class SampleAuthenticationManager implements AuthenticationManager {
private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();</span><span style="color: rgba(0, 0, 255, 1)">static</span><span style="color: rgba(0, 0, 0, 1)"> { AUTHORITIES.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span> SimpleGrantedAuthority("ROLE_USER"<span style="color: rgba(0, 0, 0, 1)">)); } </span><span style="color: rgba(0, 0, 255, 1)">public</span> Authentication authenticate(Authentication auth) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> AuthenticationException { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (auth.getName().equals(auth.getCredentials())) { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> BadCredentialsException("Bad Credentials"<span style="color: rgba(0, 0, 0, 1)">); }
}
这里我们编写了一个小程序,要求用户输入用户名和密码并执行以上的验证流程。我们这里实现的 AuthenticationManager 将会任何用户输入的用户名和密码是否相同。为了每个用户分配一个单独的角色。上面代码输出将会类似以下:
Please enter your username: 123 Please enter your password: 345 before:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@7bd9: Principal: 123; Credentials: [PROTECTED]; Authenticated: false; Details: null; Not granted any authorities Authentication failed: Bad Credentials Please enter your username: 123 Please enter your password: 123 before:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f: Principal: 123; Credentials: [PROTECTED]; Authenticated: false; Details: null; Not granted any authorities after:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: 123; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: 123; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
注意你不需要编写这样的代码。这个过程通常情况下会在内部发生,例如在一个 web 验证的过滤器中。我们在这里介绍这段代码仅仅是为了展示在 Spring Security 中构建验证过程是非常简单的。用户在 SecurityContextHolder 包含了一个完全赋值的 Authentication d 的时候被验证。
直接设置 SecurityContextHolder 中的内容
事实上,Spring Security 并不关心你如何将 Authentication 对象放入 SecurityContextHolder 中。
唯一的关键要求是在 AbstractSecurityInterceptor 验证一个用户请求之前确保 SecurityContextHolder 包含一个用于表示 principal 的 Authentication 对象。
你可以 (许多用户都这样做) 编写自己的 Filter 或者 MVC controller,来提供与那些不是基于 Spring Security 的验证系统的互操作能力。
例如,你可能会使用容器管理的验证机制,通过 ThreadLocal 或者 JNDI 地址来使当前用户可用。或者你可能在为一个有着遗留验证系统的公司工作。
在这类场景下,很容易可以让 Spring Security 工作,并且仍然提供验证能力。
所有你需要做的是编写一个过滤器,从某个位置读取第三方用户信息,构建一个特定的 Spring Security Authentication 对象,并将其放入 SecurityContextHolder 中。在这种情况下,你需要考虑在内置的验证基础结构上自动应用这些。
例如,你可能需要在返回给客户端响应之前,预先创建一个 Http Session 对象来为不同线程缓存安全上下文。
5.3 在 web 应用中进行验证
现在让我们来探索当你在一个 web 应用中使用 Spring Security 的情形 (不使用 web.xml 配置安全)。这时一个用户如何被验证以及安全上下文如何被建立?
考虑一个传统的 web 应用中的验证过程:
1、你访问主页,并且点击一个链接
2、一个请求被发送给服务器,服务器判断你是否在请求一个受保护的资源
3、如果你当前没有经过验证,服务器返回一个响应表明你必须要进行验证。响应可以是通过 HTTP 响应码或者直接重定向到一个特定的网页。
4、根据验证机制,你的浏览器可能会重定向到一个特定的网页以至于你可以填写表单,或者浏览器会检索你的身份 (通过一个基础验证对话框,一个 cookie 或者 X.509 证书,等等)。
5、浏览器给服务器回复一个响应。这可能是一个包含你填充好的表单内容的 HTTP POST 请求,或者一个包含你的验证信息的 HTTP 请求头。
6、下一步服务器会判断当前的验证信息是否是正确的。如果是,可以继续下一步。如果不是,通常你的浏览器会被要求重试 (因此你又回到了上两步)。
7、你的原始的验证过程的请求将会被重试。希望你验证后能被赋予足够的权限来访问受保护的资源。如果是,请求将会成功,否则,你将会获得一个 403 HTTP 响应码,表示 "禁止"。
对于以上提到的大部分步骤,Spring Security 都有不同的类来负责。
主要的参与者 (按照使用的顺序) 是 ExceptionTranslationFilter, AuthenticationEntryPoint 和验证机制,负责调用我们之前提到的 AuthenticationManager。
ExceptionTranslationFilter
ExceptionTranslationFilter 是一个 Spring Security 的过滤器,负责检测任何 Spring Security 抛出的异常。这些异常通常是通过一个 AbstractSecurityInterceptor 抛出,这是验证服务的一个主要提供者。我们将会在下一节讨论 AbstractSecurityInterceptor ,但是现在我们仅仅需要知道其是用于产生 Java 异常,并不知道 HTTP 或者如何验证一个 principal。
取而代之的是 ExceptionTranslationFilter 来提供这个服务,负责返回 403 错误码 (如果 principal 已经被验证并且缺乏足够的访问权限 - 上面的第七步)
或者
启动一个 AuthenticationEntryPoint (如果 principal 没有被验证,我们需要进行上面的第三步)。
AuthenticationEntryPoint
AuthenticationEntryPoint 负责上述列表的第三步。你可以想象,每一个 web 应用会有一个默认的验证策略 (在 Spring Security 中,这可以像其他内容一样进行配置,但是现在我们需要继续保持简单)。
每个验证系统会有自己的 AuthenticationEntryPoint 实现,这将会执行第三步中描述的步骤。
验证机制
一旦你的浏览器提交了你的验证凭证 (不论是 HTTP 表单提交或者 HTTP 请求头),服务器端就需要一些机制来收集这些验证信息。到目前我们已经到了第六步。
在 Spring Security 中对于从用户代理 (通常是一个 web 浏览器) 收集验证信息的功能,我们有一个特殊的名字来描述,称之为 "authentication mechanism"(验证机制)。
一旦从用户代理 (浏览器) 中收集到验证信息,一个验证请求对象会被创建,之后传递给 AuthenticationManager。
在验证机制接收到被完全赋值后的 Authentication 对象后,如果确定请求可以执行,会将 Authentication 放到 SecurityContextHolder 中,并引起原始的请求被重试 (上述第七步)。如果 AuthenticationManager 拒绝了请求,验证机制将会要求用户代理(浏览器) 重试(上述第二步)。
在不同的请求中存储 SecurityContext
根据应用的类型,可能需要有一个策略在不同的用户请求中来存储安全上下文。在一个传统的 web 应用中,一个用户被记录并在接下来的过程中通过 session id 进行区分。服务器在 session 中缓存 principal 的信息。在 Spring Security 中,存储 SecurityContext 的责任交由 SecurityContextPersistenceFilter 负责,默认情况下会将安全上下文当做 HttpSession 的一个属性存储。对于每一次请求,会重新存储安全上下文到 SecurityContextHolder 中,在请求完成时清空 SecurityContextHolder 。你不应该出于安全的目的而直接与 HttpSession 交互,而是总是应该使用 SecurityContextHolder 代替。
很多其他类型的应用 (例如无状态的 RESTFul web 服务) 并不使用 Http Session,并且会重新验证每一个请求。但是,在过滤器链中包含 SecurityContextPersistenceFilter 依然很重要,这可以确保每次请求后 SecurityContextHolder 被清空。
提示:在一个单独的 session 中接受并发请求的应用中,同一个 SecurityContext 实例将会在线程间共享。即使一个 ThreadLocal 被使用,也是为每一个线程从 HttpSession 中获取同一个 SecurityContext 实例。如果你希望一个线程在运行时临时改变安全上下文,这是一个提示。如果你仅仅使用 SecurityContextHolder.getContext(),并且在返回的安全上下文对象上调用 setAuthentication(anAuthentication),所有共享同一个 SecurityContext 实例的多个并发线程中的 Authentication 对象都会改变。你可以自定义 SecurityContextPersistenceFilter 的行为来为每个请求创建一个全新的 SecurityContext,从而阻止在一个线程中的改变影响到其他线程。作为替选方案,你也可以在需要临时改变安全上下的时候来创建一个新的实例。
SecurityContextHolder.createEmptyContext() 方法总是返回一个新的安全上下文实例。
5.4 Spring Security 中的访问控制 (授权)
Spring Security 中负责做出访问控制决定的主要接口是 AccessDecisionManager,其有一个 decide 方法,接受一个描述 principal 的 Authentication 对象,一个“security object”(见下文) 和一个应用于这个对象的安全元数据属性列表 (例如访问一个资源必须被授予的角色列表)。
Secuirty 和 AOP Advice
如果你对 AOP 的概念熟悉,你可能知道有不同类型的 advice 可以使用:before、after、throws 和 around。around advice 非常有用,因为一个 advisor 可以选择是否执行一个方法调用,是否修改返回结果,以及是否抛出一个异常。
Spring Security 对于 web 请求和方法调用都提供了一个 around advice。我们可以使用 Spring 标准的 AOP 支持来获取一个方法的调用的 around advice,也可以通过一个标准的过滤器来获得 web 请求的 around advice。
对于那些不熟悉 AOP 的用户,理解这些的核心在于知道 Spring Security 可以帮助你像保护 web 请求一样来保护方法调用。
大部分用户对于保护业务层的方法调用感兴趣。这是因为在当前的 JAVA EE 开发中,服务层包含了大部分的业务逻辑代码。
如果你需要保护服务层的方法调用,Spring 标准的 AOP 是合适的选择。如果你想直接保护域对象,你会发现 AspectJ 是值得考虑的。
你可以选择使用 AspectJ 或者 Spring AOP 来执行方法授权,或者你可以选择使用过滤器来进行 web 请求授权。
你可以联合使用 0、1、2 或者 3 种方式。
主流的用法是执行一些 web 请求授权,并且在服务层联合 Spring AOP 方法调用授权。
6 核心服务 (Core Services)
现在我们已经在较高的层面对 SpringSecuirty 的架构和核心类有所了解,现在我们来仔细的查看一到两个其核心接口的实现,特别是 AuthenticationManager、UserDetailsService 和 AccessDecisionManager。这三个接口会贯穿我们后面文档中的所有内容,因此掌握他们是如何配置以及使用是非常重要的。
6.1 AuthenticationManager, ProviderManager 与 AuthenticationProvider
AuthenticationManager 仅仅是一个接口,所以我们可以任意的选择器实现,但是在实际过程中其是如何工作的呢?如果我们的验证信息位于多个数据库实例或者我们想联合多个使用多个验证服务,例如同时使用 JDBC 认证和 LDAP 认证,我们需要做什么呢?
在 Spring Secuirty 中,AuthenticationManager 默认的实现类是 ProviderManager ,ProviderManager 并不是自己直接对请求进行验证,而是将其委派给一个 AuthenticationProvider 列表。列表中的每一个 AuthenticationProvider 将会被依次查询其是否需要通过其进行验证。每个 provider 的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication 对象的所有属性。你是否还记得我们的好朋友 UserDetails 和 UserDetailsService?如果已经不记得了,请返回之前的章节重新阅读来刷新的你的记忆。验证一个请求最常见的方式是加载对应的 UserDetails 来检查用户输入的密码与其是否匹配。DaoAuthenticationProvider 使用的就是这种方式 (见下文)。被加载的 UserDetails 对象 (包含了 GrantedAuthority s), 在认证成功后,将会被用于填充的 Authentication 对象,并且存储在 SecurityContext 中。
如果你使用基于命名空间的配置,将会框架内部自动创建并维护一个 ProviderManager 对象,你可以在命名空间的 authentication provider 元素中往 ProviderManager 对象中添加 provider。在这种情下,你不必在 application context 中声明一个 ProviderManager Bean。但是如果你没有使用命名空间,你应该使用类似以下的方式进行声明:
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager"> <constructor-arg> <list> <ref local="daoAuthenticationProvider"/> <ref local="anonymousAuthenticationProvider"/> <ref local="ldapAuthenticationProvider"/> </list> </constructor-arg> </bean>
在上面这个案例中,我们有三个 provider,他们将会被按顺序进行尝试 (内部使用了一个 List),每个 provider 都可以尝试进行认证,或者简单的通过返回 null 来跳过认证。如果所有的 provider 都返回了 null,ProviderManager 将会抛出一个 ProviderNotFoundException 异常。如果你对联合使用多个 provider 感兴趣,请参考 ProviderManager 的 javaDoc。
类似于表单登录的处理过滤器这样的验证机制,会以引用的形式注入到 ProviderManager 中,从而调用其来对请求进行验证。使用这样的验证机制,我们的 provider 有时是可以相互替代的。例如 DaoAuthenticationProvider 和 LdapAuthenticationProvider 对于任何通过用户名 / 密码形式提交验证请求的这样的验证机制都是可以使用的(如基于表单的登录和 Http 基础验证)。另一方面,某些验证机制创建的验证请求只能由特定的 AuthenticationProvider 处理,例如 JA-SIG CAS,其使用了服务 ticket 的概念,因此只能通过 CasAuthenticationProvider 验证。你不需要要过多的考虑这些内容,因为如果你忘记了注册一个合适的 provider,你仅仅是会收到一个验证失败的时的 ProviderNotFoundException 。
成功认证后删除验证信息
从 Spring Security 3.1 之后,在请求认证成功后 ProviderManager 将会删除 Authentication 中的认证信息,这可以阻止一些敏感信息例如密码在不需要的时候还长时间保留。
这可能会导致一些问题,例如你希望通过缓存 user 对象来提高一个无状态应用的性能。如果 Authentication 对象包含了位于缓存中的并且移除了认证信息的对象 (例如 UserDetails) 的引用,那么就无法再继续使用缓存的对象来进行验证。在你使用缓存的时候,你需要考虑这个因素。一个显而易见的解决方案是,无论是在 AuthenticationProvider 还是在缓存的实现中,在创建 Authentication 对象的时候,将这个对象拷贝一份。或者你也可以禁用 ProviderManager 的 eraseCredentialsAfterAuthentication 属性。
DaoAuthenticationProvider
Spring Security 提供了一个 AuthenticationProvider 的简单实现 DaoAuthenticationProvider,这也是框架最早支持的 provider。它使用了一个 UserDetailsService 来查询用户名、密码和 GrantedAuthority 。其简单的通过比较封装了用户的密码信息的 UsernamePasswordAuthenticationToken 和通过 UserDetailsService 查询出来的用户的密码是否相同来验证用户。对于这个 provider 的配置非常简单。
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
PasswordEncoder 是可选的。PasswordEncoder 提供了对通过 UserDetailsService 查询返回的 UserDetails 对象中的密码进行编码和解码的功能,更多的细节将在后面讲解。
像本教程之前提到的一样,大部分验证 provider 都利用了 UserDetails 和 UserDetailsService 接口。回顾一下 UserDetailsService 中只定义了一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的 UserDetails 也是一个接口,其只提供了 getters 方法来保证用户的验证信息例如用户名、密码和被授予的权利等不会因为用户账号的禁用或者启用而被置为空。大部分验证提供者都会使用 UserDetailsService,即使 username 和 password 在验证过程中不需要使用到。他们也可能只使用 UserDetails 对象中 GrantedAuthority 信息。因为一些其他的系统 (例如 LDAP or X.509 or CAS) 接管了验证用户信息的责任。
在验证 (Authentication) 层面, Spring Security 提供了不同的验证模型。
大部分的 authentication 模型来自于第三方或者权威机构或者由一些相关的标准制定组织 (如 IETF) 开发。
此外,Spring Security 也提供了一些验证特性。
特别的,Spring Security 目前支持对以下所有验证方式的整合:
HTTP BASIC authentication headers (an IETF RFC-based standard) HTTP Digest authentication headers (an IETF RFC-based standard) HTTP X.509 client certificate exchange (an IETF RFC-based standard) LDAP (a very common approach to cross-platform authentication needs, especially in large environments) Form-based authentication (for simple user interface needs) OpenID authentication Authentication based on pre-established request headers (such as Computer Associates Siteminder) JA-SIG Central Authentication Service (otherwise known as CAS, which is a popular open source single sign-on system) Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (a Spring remoting protocol) Automatic "remember-me" authentication (so you can tick a box to avoid re-authentication for a predetermined period of time) Anonymous authentication (allowing every unauthenticated call to automatically assume a particular security identity) Run-as authentication (which is useful if one call should proceed with a different security identity) Java Authentication and Authorization Service (JAAS) JEE container autentication (so you can still use Container Managed Authentication if desired) Kerberos Java Open Source Single Sign On (JOSSO) * OpenNMS Network Management Platform * AppFuse * AndroMDA * Mule ESB * Direct Web Request (DWR) * Grails * Tapestry * JTrac * Jasypt * Roller * Elastic Path * Atlassian Crowd * Your own authentication systems (see below)
注:* 号标记的部分由第三方提供。
很多独立软件厂家 (ISV) 接受 Spring Security 的原因就是可以灵活的选择验证模型。这样无论客户的需求是什么,他们都可以快速的整合进自己的解决方案,不需要进行太多额外的研究或者要求客户改变他们的运行环境。如果上述验证模型都不能满足我们的需求,由于 Spring Security 是一个开放的平台,我们也可以很容易的就编写出自己的验证机制。
许多 Spring Security 的 企业用户需要将一些并不遵循任何安全标准的 "遗留" 系统中整合进安全特性,Spring Security 很适合用来处理这类系统。
除了验证机制, Spring Security 也提供了一系列的授权能力。主要感兴趣的是以下三个方面:
1、对 web 请求进行授权
2、授权某个方法是否可以被调用
3、授权访问单个领域对象实例
为了理解其中的不同,考虑一下 Servlet 规范 web 模式安全中的授权能力、EJB 容器管理的安全、文件系统安全。SpringSecurity 对于这些重要的方面提供了深入的支持
将以上代码部署到 Tomcat 中运行,通过浏览器访问任何页面都会被重新定位到一个登陆页面:
这个页面是 Spring Security 自动帮我们生成的。我们可以使用之前配置的用户名和密码进行登录。
自动生成这个页面的代码位于类DefaultLoginPageGeneratingFilter的 generateLoginPageHtml 方法中。
Spring Security 如何知道我们希望所有的用户都被验证呢?
Spring Security 是怎么知道我们希望支持表单形式的验证呢?
原因是我们的 SecurityConfig 类继承了 WebSecurityConfigurerAdapter,而其在 configure(HttpSecurity http) 中提供了一些默认的配置。
默认的配置如下:
protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests().anyRequest().authenticated().and() .formLogin().and() .httpBasic();}
上面的默认配置的作用:
* 要求访问应用的所有用户都要被验证
* 允许所有用户可以通过表单进行验证
* 允许所有请求通过 Http Basic 验证 (译者注:关于什么是 Http Basic 验证,读者可以查看维基百科。)
你会发现,这个配置与以下 xml 格式的配置是类似的:
<http> <intercept-url pattern="/**" access="authenticated"/> <form-login /> <http-basic /> </http>
Java 配置中的and() 方法类似于 xml 配置中的结束标签,and() 方法返回的对象还是 HttpSecurity,方便我们继续对 HttpSecurity 进行配置。
HTTP 基本认证
在 HTTP 中,基本认证是一种用来允许 Web 浏览器或其他客户端程序在请求时提供用户名和口令形式的身份凭证的一种登录验证方式。
在发送之前是以用户名追加一个冒号然后串接上口令,并将得出的结果字符串再用Base64 算法编码。
例如,提供的用户名是 Aladdin、口令是 open sesame,则拼接后的结果就是Aladdin:open sesame,然后再将其用 Base64 编码,得到 QWxhZGRpbjpvcGVuIHNlc2FtZQ==。
最终将 Base64 编码的字符串发送出去,由接收者解码得到一个由冒号分隔的用户名和口令的字符串。
虽然对用户名和口令的 Base64 算法编码结果很难用肉眼识别解码,但它仍可以极为轻松地被计算机所解码,就像其容易编码一样。
编码这一步骤的目的并不是安全与隐私,而是为将用户名和口令中的不兼容的字符转换为均与 HTTP 协议兼容的字符集。
总的来说:
HttpSecurity 是 SecurityBuilder 接口的一个实现类,从名字上我们就可以看出这是一个 HTTP 安全相关的构建器。当然我们在构建的时候可能需要一些配置,当我们调用 HttpSecurity 对象的方法时,实际上就是在进行配置。
例如在默认的安全配置中 authorizeRequests(),formLogin()、httpBasic() 这三个方法返回的分别是 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer,他们都是 SecurityConfigurer 接口的实现类,分别代表的是不同类型的安全配置器。
因此,从总的流程上来说,当我们在进行配置的时候,需要一个安全构建器 SecurityBuilder(例如我们这里的 HttpSecurity),SecurityBuilder 实例的创建需要有若干安全配置器 SecurityConfigurer 实例的配合。
配置的最终结果是什么?启用指定的 Filter 并完成相关配置
基本上每个 SecurityConfigurer 子类都对应一个或多个过滤器。
我们分别查看 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer 三个类的 JavaDoc:
可见 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer 三个配置器对应的 Filter 分别是 FilterSecurityInterceptor、UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter。
而 HttpSecuirty 内部维护了一个Filter 的 List 集合,我们添加的各种安全配置器对应的 Filter 最终都会被加入到这个 List 集合中。
从更高的层面来说,SecurityBuilder 和 SecurityConfigurer 实现类都有很多。
SecurityBuilder 的类图
SecurityConfigurer 的类图
由于 SecurityConfigurer 的类图过于复杂,以下只列目前我们已经接触到的几个 (红色圈标记)
虽然 SecurityBuilder 是需要使用到 SecurityConfigurer,不过根据功能划分,一个 SecurityBuilder 只能支持部分的 SecurityConfigurer,而不是所有。
例如对于 HttpSecurity 来说,其支持的 SecurityConfigurer 定义在 HttpSecurity 类的源码中:
可以看到这些方法的返回类型都是 XXXConfigurer,表示的是 HttpSecurity 这个 SecurityBuilder 支持的 SecurityConfiguer。
对于其他的 SecurityBuilder 实现类也是类似,其支持的 SecurityConfiguer 都定义在自己的源码中。
前面我们已经提到,当我们在一个类上添加 @EnableWebSecurity 注解后,Spring Security 会自动帮助我们创建一个名字为的 springSecurityFilterChain 过滤器。
这个过滤器实际上只是 Spring Security 框架验证请求的一个入口,到底如何验证请求实际上是要依赖于我们如何配置 Spring Security。
我们以上面提到的 WebSecurityConfigurerAdapter 默认的 configuer(HttpSecurity) 方法进行说明。
配置的核心代码如下:
http.authorizeRequests().anyRequest().authenticated().and()
.formLogin().and()
.httpBasic();
以 and() 方法作为切分,可以划分为 3 个部分,你可以认为每个部分实际上都是配置了一个过滤器。
首先我们来观察 authorizeRequests(),formLogin()、httpBasic() 三个方法的实现:
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity>{ ... public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>()).getRegistry(); }</span><span style="color: rgba(0, 0, 255, 1)">public</span> FormLoginConfigurer<HttpSecurity> formLogin() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { </span><span style="color: rgba(0, 0, 255, 1)">return</span> getOrApply(<span style="color: rgba(0, 0, 255, 1)">new</span> FormLoginConfigurer<HttpSecurity><span style="color: rgba(0, 0, 0, 1)">()); } </span><span style="color: rgba(0, 0, 255, 1)">public</span> HttpBasicConfigurer<HttpSecurity> httpBasic() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { </span><span style="color: rgba(0, 0, 255, 1)">return</span> getOrApply(<span style="color: rgba(0, 0, 255, 1)">new</span> HttpBasicConfigurer<HttpSecurity><span style="color: rgba(0, 0, 0, 1)">()); } ...
}
这三个方法最终返回的分别是 ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer。事实上,这都是 SecurityConfigurerAdapter 的子类,SecurityConfigurerAdapter 是的 SecurityConfigurer 接口抽象子类。
而这 3 个方法内部又都调用了 getOrApply 方法:
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer) throws Exception { C existingConfig = (C) getConfigurer(configurer.getClass()); if (existingConfig != null) { return existingConfig; } return apply(configurer); }
可以看到这段代码的主要作用就是在 HttpSecurity 对象中,添加一个基础验证过滤器 BasicAuthenticationFilter。
这也验证我们我们之前的说法,可以认为通过 and() 分割后的每段配置,实际上都是在 HttpSecuirty 中添加一个过滤器。
当然并不是每个 SecurityConfigurer 都是通过这种方式来创建过滤器的,
例如 FormLoginConfigurer 就直接在构造方法中来创建一个类型为 UsernamePasswordAuthenticationFilter 的过滤器,
源码如下:
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password");}
大多数情况下,我们可能会希望使用自己的登录页面。此时我们可以按照类似以下的方式配置:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().anyRequest().authenticated().and() .formLogin() .loginPage( "/login")// 1 如果没有此行指定,则会使用内置的登陆页面 .permitAll(); // 2 因为是自定义的 URL,则需要指定认证要求 }
1、更新后的配置,指定了登录页面的位置
2、我们必须允许所有的用户,不管是否登录,都可以访问这个页面。 formLogin().permitAll() 允许所有用户访问这个页面。
以下是一个 JSP 形式登录页面的实现:
<c:url value="/login" var="loginUrl"/> <form action="${loginUrl}" method="post"> 1 <c:if test="${param.error != null}" > 2</span><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(0, 0, 0, 1)"> Invalid username and password. </span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">c:if</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">c:if </span><span style="color: rgba(255, 0, 0, 1)">test</span><span style="color: rgba(0, 0, 255, 1)">="${param.logout != null}"</span> <span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(0, 0, 0, 1)"> 3 </span><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(0, 0, 0, 1)"> You have been logged out. </span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">c:if</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">label </span><span style="color: rgba(255, 0, 0, 1)">for</span><span style="color: rgba(0, 0, 255, 1)">="username"</span> <span style="color: rgba(0, 0, 255, 1)">></span>Username<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)"> label</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="text"</span><span style="color: rgba(255, 0, 0, 1)"> id</span><span style="color: rgba(0, 0, 255, 1)">="username"</span><span style="color: rgba(255, 0, 0, 1)"> name</span><span style="color: rgba(0, 0, 255, 1)">="<span style="color: rgba(255, 0, 0, 1); font-size: 14pt"><strong>username</strong></span>"</span> <span style="color: rgba(0, 0, 255, 1)">/></span><span style="color: rgba(0, 0, 0, 1)"> 4 </span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">label </span><span style="color: rgba(255, 0, 0, 1)">for</span><span style="color: rgba(0, 0, 255, 1)">="password"</span> <span style="color: rgba(0, 0, 255, 1)">></span>Password<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)"> label</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="password"</span><span style="color: rgba(255, 0, 0, 1)"> id</span><span style="color: rgba(0, 0, 255, 1)">="password"</span><span style="color: rgba(255, 0, 0, 1)"> name</span><span style="color: rgba(0, 0, 255, 1)">="<span style="color: rgba(255, 0, 0, 1); font-size: 14pt"><strong>password</strong></span>"</span> <span style="color: rgba(0, 0, 255, 1)">/></span><span style="color: rgba(0, 0, 0, 1)"> 5 </span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="hidden"</span><span style="color: rgba(255, 0, 0, 1)"> 6 name</span><span style="color: rgba(0, 0, 255, 1)">="${_csrf.parameterName}"</span><span style="color: rgba(255, 0, 0, 1)"> value</span><span style="color: rgba(0, 0, 255, 1)">="${_csrf.token}"</span> <span style="color: rgba(0, 0, 255, 1)">/></span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">type</span><span style="color: rgba(0, 0, 255, 1)">="submit"</span><span style="color: rgba(255, 0, 0, 1)"> class</span><span style="color: rgba(0, 0, 255, 1)">="btn"</span><span style="color: rgba(0, 0, 255, 1)">></span> Log in<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(0, 0, 255, 1)">></span>
</form>
1、向 /login URL 发送 post 请求尝试验证用户
2、如果参数有错,验证失败
3、如果注销参数存在,用户成功注销
4、HTTP 用户名参数必须以 username 命名
5、http 密码参数必须以 password 命名
6、我们可以通过 16.4 节的 "引入 csrf token" 和跨站请求伪造章节查看更多的参考信息。
源码解读
在这里,SpringSecurity 强制要求我们的表单登录页面必须是以 POST 方式向 /login URL 提交请求,而且要求用户名和密码的参数名必须是 username 和 password。如果不符合,则不能正常工作。
原因在于,当我们调用了 HttpSecurity 对象的 formLogin 方法时,其最终会给我们注册一个过滤器 UsernamePasswordAuthenticationFilter。
我们来看一下这个过滤器的源码:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // =====================================================================================public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";//默认的用户名参数名
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";//默认的密码参数名private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;// ~ Constructors
// ===================================================================================================public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));//只对 POST 请求方式的 /login 进行拦截
}
...
}
此时一切显得很明了,因为这个过滤器中强制指定了必须使用这些值。
当我们也可以有方式进行修改用户名和密码的参数名,如下:
... .formLogin() .loginPage( "/login") .usernameParameter("uname")//自定义用户名参数名称 .passwordParameter("pwd")//自定义密码参数名称
不过由于 POST 方法请求 /login 这个 URL,因为已经代码中写死,因此无法修改。
请求授权 (Authorize Requests)
1、http.authorizeRequests() 方法有很多子方法,每个子匹配器将会按照声明的顺序起作用。
2、指定用户可以访问的多个 url 模式。特别的,任何用户可以访问以 "/resources" 开头的 url 资源,或者等于 "/signup" 或 about
3、任何以 "/admin" 开头的请求限制用户具有 "ROLE_ADMIN" 角色。你可能已经注意的,尽管我们调用的 hasRole 方法,但是不用传入 "ROLE_" 前缀
4、任何以 "/db" 开头的请求同时要求用户具有 "ROLE_ADMIN" 和 "ROLE_DBA" 角色。
5、任何没有匹配上的其他的 url 请求,只需要用户被验证。
源码解读
在这个案例中我们调用了 antMatchers 方法来定义什么样的请求可以放过,什么样的请求需要验证。antMatchers 使用的是 Ant 风格的路径匹配模式 (在下一节我们会详细讲解)。
这个方法中定以在 AbstractRequestMatcherRegistry 中,我们查看一下这个方法的源码:
public C antMatchers(String... antPatterns) { return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); }
这个方法内部又调用了 RequestMatchers 对象的静态方法 antMatchers 方法,源码如下
public static List<RequestMatcher> antMatchers(HttpMethod httpMethod, String... antPatterns) { String method = httpMethod == null ? null : httpMethod.toString(); List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(); for (String pattern : antPatterns) { matchers.add(new AntPathRequestMatcher(pattern, method)); } return matchers; }
可见最终返回的是一个 RequestMatcher 列表,事实上,SpringSecurity 在工作过程中,就可以利用 RequestMatcher 对象来进行路径匹配了。
除了 ANT 风格的路径匹配模式,我们还可以使用基于正则表达式的路径匹配模式,对应的方法是 regexMatchers(..)。
3.6 Ant Parttern 语法
antMatcher 使用的是 ant 风格的路径匹配模式。
Apache Ant 样式的路径有三种通配符匹配方法(在下面的表格中列出 ) 这些可以组合出很多种灵活的路径模式:
Ant 风格路径匹配的通配符
Wildcard Description
? 匹配任何单字符
* 匹配 0 或者任意数量的字符,不包含 "/"
** 匹配 0 或者更多的目录,包含 "/"
ANT 风格路径匹配案例
Path Description
/app/*.x 匹配 (Matches) 所有在 app 路径下的.x 文件
/app/p?ttern 匹配 (Matches) /app/pattern 和 /app/pXttern, 但是不包括 /app/pttern
/**/example 匹配 (Matches) /app/example, /app/foo/example, 和 /example
/app/**/dir/file. 匹配 (Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf, 和 /app/dir/file.java
/**/*.jsp 匹配 (Matches) 任何的.jsp 文件
3.7 注销处理
当使用 WebSecurityConfigurerAdapter,注销功能将会被自动应用,也就是说,就算不写也有用。
默认情况下访问 /logout 将会将用户注销,包含的内容有:
1、使 HttpSession 失效
2、清空已配置的 RememberMe验证
3、清空 SecurityContextHolder
4、重定向到 /login?success
类似于配置登录功能,你同样可以自定义自己的注销需求
protected void configure(HttpSecurity http) throws Exception { http .logout() 1 .logoutUrl("/my/logout") 2 .logoutSuccessUrl("/my/index") 3 .logoutSuccessHandler(logoutSuccessHandler) 4 .invalidateHttpSession(true) 5 .addLogoutHandler(logoutHandler) 6 .deleteCookies(cookieNamesToClear) 7 .and()...}
1、提供注销支持,当使用 WebSecurityConfigurerAdapter 时这将会被自动应用
2、触发注销操作的 url,默认是 /logout。如果开启了CSRF 保护(默认开启), 那么请求必须是 POST 方式。
3、注销操作发生后重定向到的 url,默认为 /login?logout。
4、让你指定自定义的 LogoutSuccessHandler。如果指定了, logoutSuccessUrl() 将会被忽略。
5、指定在注销的时候是否销毁 HttpSession 。默认为 True。
6、添加一个 LogoutHandler。默认情况下, SecurityContextLogoutHandler 被作为最后一个 LogoutHandler 。
7、允许指定当注销成功时要移除的 cookie 的名称。这是显式添加 CookieClearingLogoutHandler 的一种快捷处理方式。
一般情况下,为了自定义注销功能,你可以添加 LogoutHandler 或者 LogoutSuccessHandler 的实现。
对于很多场景,这些 handler 通过流式 API 的方式进行使用。
LogoutHandler
一般而言, LogoutHandler
的实现表示那些可以参与处理注销操作的类。它们被期望用于执行一些必要的清理操作。这些实现不应该抛出异常。默认提供了以下这些实现:
除了直接提供的 LogoutHandler
的实现,流式 API 同样为每个 LogoutHandler
提供了快捷方式的实现,
如 deleteCookies()允许指定一个或多个当注销成功时需要移除的 cookie 的名称, 它是 CookieClearingLogoutHandler 的快捷方式。
LogoutSuccessHandler
LogoutSuccessHandler
在 LogoutFilter 成功执行之后被调用,来重定向或者转发到合适的目的地上,注意这个接口和 LogoutHandler
几乎一样,但是可以抛出异常。默认提供了以下实现:
-
HttpStatusReturningLogoutSuccessHandler
正如上面提到的,你不需要直接的指定 SimpleUrlLogoutSuccessHandler
。流式 API 提供了一个快捷方法来 logoutSuccessUrl()代替。底层还是使用 SimpleUrlLogoutSuccessHandler
来实现。
当注销发生之后,会重定向到提供的 URL 上,默认是 /login?logout
.。
在REST API的场景下,可能会对 HttpStatusReturningLogoutSuccessHandler
感兴趣。
当注销成功后,不是重定向到一个 URL,这个 LogoutSuccessHandler
允许你提供一个纯文本的 HTTP 状态码来返回。如果没有配置,默认情况下返回的是 200。
更多注销相关的文档参考
-
Logging Out in section CSRF Caveats
-
Section Single Logout (CAS protocol)
-
Documentation for the logout element in the Spring Security XML Namespace section
3.9 多个 HttpSecurity
我们可以配置多个 HttpSecurity 实例,就像我们可以在 xml 文件中配置多个 <http> 一样。关键在于多次扩展 WebSecurityConfigurationAdapter。
例如,以下是一个对于以 /api/ 开头的 url 的不同配置
@EnableWebSecurity public class MultiHttpSecurityConfig { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER").and() .withUser("admin").password("password").roles("USER", "ADMIN");}@Configuration @Order(</span>1<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">class</span> ApiWebSecurityConfigurationAdapter <span style="color: rgba(0, 0, 255, 1)">extends</span><span style="color: rgba(0, 0, 0, 1)"> WebSecurityConfigurerAdapter { </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(HttpSecurity http) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { http .antMatcher(</span>"/api/**"<span style="color: rgba(0, 0, 0, 1)">) .authorizeRequests() .anyRequest().hasRole(</span>"ADMIN"<span style="color: rgba(0, 0, 0, 1)">) .and() .httpBasic(); } } @Configuration </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">class</span> FormLoginWebSecurityConfigurerAdapter <span style="color: rgba(0, 0, 255, 1)">extends</span><span style="color: rgba(0, 0, 0, 1)"> WebSecurityConfigurerAdapter { @Override </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(HttpSecurity http) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); } }
}
1、按照正常的方式配置验证
2、创建一个包含 @Order 的 WebSecurityConfigurerAdapter 实例来指定哪一个 WebSecurityConfigurerAdapter 应该被首先考虑。
3、http.antMatcher 表明这个 HttpSecurity 只适用于以 /api/ 开头的 URL。
4、创建另一个 WebSecurityConfigurerAdapter 实例。如果 URL 没有以 /api/ 开头,这个配置将会被使用。
这个配置在 ApiWebSecurityConfigurationAdapter 之后生效,因为其含有一个 @Order 值为 1.没有 @Order 默认是最后一个生效。
译者注:对于一些小的应用,可能我们的应用的后台管理界面和前台用户界面都集成在一个应用中,这个时候使用这种方式的配置就非常有用。
3.10 方法安全
从 2.0 版本开始,Spring Security 开始对服务层的方法提供事实上的安全支持。其提供对 JSR-250 中注解的支持,同时自身提供了 @Secured 注解。
从 3.0 开始,你可以使用基于表达式的注解。你可以针对单独的某个 Bean 应用安全策略,使用拦截方法来装饰 Bean 的声明,或者你可以 Aspect 风格的切点来对整个 service 层中多个 Bean 进行安全保护。
EnableGlobalMethodSecurity
我们可以通过在任何 @Configuration 实例上使用 @EnableGlobalMethodSecurity 开启基于注解的安全验证。
例如,如下配置将会使 SpringSecurity 的 @Secured 注解生效。
@EnableGlobalMethodSecurity(securedEnabled = true) public class MethodSecurityConfig { // ... }
添加一个注解到一个方法 (或者类、接口) 将会限制对这个方法的访问。
Spring Secuirty 自带的注解支持为方法定义一些列属性。这将会被传递给 AccessDecisionManager 来做出事实上的决定。
public interface BankService { @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public Account readAccount(Long id);@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
添加一个密码编码器
密码应该始终使用一个安全的,其设计目的就是用来进行密码编码的哈希算法进行编码 (而不是标准的算法如 SHA 和 MD5), <password-encoder> 元素提供了这种支持。
要想 使用 bcrypt 来编码密码,原来的 authentication provider 配置看起来应该像这样:
<beans:bean name="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/><authentication-manager>
<authentication-provider>
<password-encoder ref="bcryptEncoder"/>
<user-service>
<user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f"
authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f"
authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
在大多数情况下, Bcrypt 是一个好的选择,除非你有一个遗留系统强制你使用其他的算法。
如果你使用一个简单的哈希算法,如果更坏的情况,存储纯文本形式的密码,那么你应该考虑迁移到一个更安全的选择,例如 Bcrypt 。
添加 HTTP/HTTPS 通道安全
如果你的应用同时支持 HTTP 和 HTTPS,并且你需要特定的 URL 只能通过 HTTPS 进行访问, <intercept-url> 上的属性 requires-channel 对此提供了直接的支持。
<http> <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/> <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/> ... </http>
进行了这样的配置后,如果一个用户尝试通过 HTTP 访问匹配 "/secure/**" 模式的任何资源,首先将会被重定向到一个 HTTPS URL。可用的选项有 "http"、"https" 和 "any",使用 any 表示 HTTP 和 HTTPS 都可以使用。
如果你的应用使用非标准的 HTTP、HTTPS 端口,你可以按照如下方式指定一个端口映射列表:
<http> ... <port-mappings> <port-mapping http="9080" https="9443"/> </port-mappings> </http>
注意为了可以真正的受到保护,一个应用根本不应该使用 HTTP 或者在 HTTP 与 HTTPS 之间进行切换。应该通过 HTTPS,使用安全的链接来避免可能的 "中间人" 攻击。
Session 管理
检测超时
你可以配置 spring security 来支持检测一个过期的 session ID 并将用户重定向到一个合适的 URL。这通过 session 管理元素实现。
<http> ... <session-management invalid-session-url="/invalidSession.htm" /> </http>
注意,如果你使用这种机制来检测 session 超时,如果用户注销后,没有关闭浏览器又重新登录了,可能会错误的报告一个异常。
这是因为当你让 session 失效时没有清空 session 的 cookie。你可以在注销的时候显式的指定删除名为 JSESSIONID 的 cookie。
例如你可以在注销处理器中使用以下的语法:
<http> <logout delete-cookies="JSESSIONID" /> </http>
不幸的是并不能保证在每一个 servlet 容器中这都功能起到作用,所以你需要在自己的环境中进行测试。
提示:如果你在一个代理之后运行自己的应用,你同样可以通过配置代理服务器来删除 session cookie。
例如使用 apache HTTPD 的 mod_headers,以下的指令将会删除 JSESSIONID cookie,原理是在一个注销请求的响应中设置 cookie 过期。(假设应用的部署路径是 /tutorial)
<LocationMatch "/tutorial/logout">Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT" </LocationMatch>
当前 session 控制
如果你想限制只有一个用户可以登录你的应用,Spring Security 通过以下的简单附加配置对此提供支持。
首先你需要在你的 web.xml 文件中添加以下的监听器来保证 session 生命周期事件发生时 Spring Security 被更新。
<listener> <listener-class> org.springframework.security.web.session.HttpSessionEventPublisher </listener-class> </listener>
接着在你的 application context 中添加以下内容
<http> ... <session-management> <concurrency-control max-sessions="1" /> </session-management> </http>
这将会阻止一个用户多次进行登录 -- 第二次登录将会导致第一个 session 失效。
通常情况下你可能会阻止第二次登录,在这种场景下你可以使用:
<http> ... <session-management> <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" /> </session-management> </http>
二次登录就会被拒绝,"拒绝" 的意思是用户将会被带到 authentication-failure-url指定的 url(如果使用了基于表单登录的话)。
如果二次验证是通过另外的非交互的机制发生的,例如 "remember-me",一个“ unauthorized”401 错误将会被发送给客户端。
如果你想用一个错误页面来替代。你可以在 session-management 元素中添加属性 session-authentication-error-url。
如果你为表单登录使用了一个自定义的验证过滤器,那么你必须显式的配置当前 session 控制的支持。在 Session Management chapter. 有更多的细节。
Session Fixation Attack Protection
会话固定攻击是一个潜在的风险。一个恶意攻击者通过访问一个站点来创建一个 session。接着让另外一个用户使用同样的 session 进行登录 (例如通过发送一个包含当前 session 标识符作为参数的连接)。Spring Security 对此可以自动进行包括,通过创建一个新的 session 或者当用户登录后改变 session 的标识符。如果你不需要这种保护,或者这与其他的需求由冲突,你可以通过 <session-management> 的属性 session-fixation-protection 来对这种行为进行控制,其值有四个选项:
none - 不做任何事,当前 session 仍然被保留。
newSession - 创建一个新的干净的 session,不拷贝已经存在的 session 中的数据。 (Spring Security 相关的属性依然会被拷贝).
migrateSession - 创建一个新的 session 并其拷贝现有 session 的所有属性到新的 session 中。在 servlet 3.0 或者更早的容器中,这是默认的。
changeSessionId - 不创建一个新的 session. 取而代之的是使用 Servelt 容器提供固定会话保护 (调用 HttpServletRequest#changeSessionId()) 方法. 这个选项只有在 Servlet 3.1 (Java EE 7) 或者更新的容器中才是有效的。
在老版本的容器中指定这个将会产生一个异常. 在 servlet 3.1 或者更新的容器中,这是默认的。
当固定会话攻击发生时,会导致 application context 中一个 SessionFixationProtectionEvent 事件的发布。如果你使用 changeSessionId,这个保护将会所有 javax.servlet.http.HttpSessionIdListener 被通知。所以使用的时候需要小心,如果你的代码对两种事件都进行了监听。阅读 session 管理章节获取更多的细节。
4.4 默认的 AccessDecisionManager
当你使用一个命名空间配置,将会为你注册一个默认的 AccessDecisionManager 实例,其基于 intercept-url 和 protect-pointcut 的声明 (或者是注解),来对 web URL 和方法调用做出一些访问判断。
默认的策略是使用带有 RoleVoter 和 AuthenticatedVoter 的 AffirmativeBased 、AccessDecisionManager 。
你可以在 authorization 章节查看更多的信息。
自定义 AccessDecisionManager
如果你需要使用一个更加复杂的访问控制策略,那么你要知道对于方法安全和 web 安全设置一个替代者都非常简单。
对于方法安全,你通过设置 global-method-securit 元素的 access-decision-manager-ref 属性 来指向一个在 application context 中定义的合适的 AccessDecisionManager bean 的 id。
<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean"> ... </global-method-security>
web 安全的语法是相同的,但是是放在 <http> 元素上
<http access-decision-manager-ref="myAccessDecisionManagerBean"> ... </http>
4.5 验证管理器和命名空间
Spring Secuirty 中提供验证服务的主要接口是 AuthenticationManager。其是 Spring Security ProviderManager 类的一个实例
运行时环境
Spring Security 3.0 需要 Java 5.0 或者更高的版本。
由于 Spring Security 致力于以一种自包含的方式运行,因此没有必要在 Java 运行时环境中放置任何特别的配置文件。
5.1 核心组件
SecurityContextHolder, SecurityContext 和 Authentication 对象
最基础的对象是 SecurityContextHolder,这是我们在应用中存储当前安全细节的地方,包含了当前在应用中使用到的 principal 细节。
默认情况下, SecurityContextHolder 使用一个 ThreadLocal 对象来存储这些细节,这表示对于在同一个线程中执行的方法,安全上下文 (security context) 都是可用的,即使安全上下文没有显式的当做方法的参数进行传递。
通过这种方式使用 ThreadLocal 是很安全的,如果当前使用到的规则需要在请求处理完之后被清空。
当然,Spring Security 帮你自动的处理了这些,你不需要考虑。
一些应用并不完全适用使用一个 ThreadLocal,这需要考虑使用线程完成工作的方式。
例如,对于一个 Swing 客户端,可能希望在虚拟机中运行的所有线程使用同一个安全上下文 (security context)。在应用启动的时候我们可以配置一个策略来指定 SecurityContextHolder 如何来存储安全上下文。对于一个本地应用,你可能会使用 SecurityContextHolder.MODE_GLOBAL 策略。
其他的应用可能希望由安全线程 (secure thread) 启动的其他线程拥有同样的安全特性。
这可以通过 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 实现。
你可以通过 2 种方式来概念默认的安全策略 (SecurityContextHolder.MODE_THREADLOCAL).
第一种方式是设置系统属性,
第二种方式是调用 SecurityContextHolder 的静态方法。
大部分应用不需要改变默认的配置,如果你确实需要,查看 SecurityContextHolder 对象的 java doc 来获取更多的信息。
获取当前用户信息
在 SecurityContextHolder 中我们存储了与系统交互的 principal 相关细节。Spring Security 使用 Authentication 对象来描述这些信息。
通常你不需要自己创建 Authentication 对象,但是对于 Authentication 对象的查询是非常常见的。你可以在应用中的任何地方使用以下代码块来获取当前已验证的用户的名字,
例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通过调用 getContext() 方法返回的对象是 SecurityContext 接口的一个实例。这个对象被存储在 thread local 里。就像我们接下来要看到的,Spring Security 大部分的验证机制通过返回一个 UserDetails 实例作为 principal。
UserDetailsService
在上面的代码片段,我们需要注意的是可以从一个 Authentication 对象中获取一个 principal。principal 就是一个 Object 而已。
大部分情况下,可以强制转换为一个 UserDetails 对象。 UserDetails 是 Spring Secuirty 中的一个核心接口。它代表了一个 principal,其是是可扩展的并且是应用相关的。
你可以认为 UserDetails 是你自己的用户数据与 Spring Security 在 SecurityContextHolder 对象中需要使用到的用户数据的适配器。
作为你自己的用户数据的某种表现,你会经常强制转换 UserDetails 为应用中原来提供的数据类型,因此你可以调用特定的业务方法,(例如 getEmail(), getEmployeeNumber()等等)。
到现在你可能想知道,什么时候我应该提供一个 UserDetails 对象?如何来做?我认为你仅仅是在声明这件事情,并不需要编写任何 java 代码。简单的回答是有一个特定的接口 UserDetailsService。这个接口中定义的唯一的方法接受一个 String 类型的用户名参数,返回 UserDetails 对象。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
这是在 Spring Secuirty 中加载用户信息最常用的方式,并且你将会看到在框架中任何需要获取一个用户的信息的时候使用的都是这种方式。
在验证成功的情况下, UserDetails
被用来构建 Authentication
对象,并存储在 SecurityContextHolder
中。一个好消息是我们已经提供了大量的 UserDetailsService
的实现,包括使用内存中的 map( InMemoryDaoImpl),和使用 JDBC( JdbcDaoImpl)。尽管如此,大部分用户倾向于基于使用应用中已经存在的用于描述员工、客户或者其他类型用户的数据访问对象 (Data Access Object) 来编写他们自己的实现。记住无论你的 UserDetailsService
返回类型是什么,总是可以在 SecurityContextHolder
中通过以上的代码片段来获取。
提示:读者经常会对 UserDetailsService 产生疑惑。它是一个纯粹用于获取用户数据的 DAO,没有任何其他功能,除了提供框架中其他组件需要的数据。特别的,其并不验证用户,验证是通过 AuthenticationManager 完成。在很多场景下,如果我们需要自定义的验证过程,需要直接实现 AuthenticationProvider。
GrantedAuthority
除了 principal, Authentication
提供的另一个重要的方法是 getAuthorities()。这个方法用于提供一个 GrantedAuthority
对象数组。 GrantedAuthority
表示的是授予当前的 principal 权利。这些权利通常是角色 ("roles"),例如 ROLE_ADMINISTRATOR
或者 ROLE_HR_SUPERVISOR。这些角色的配置是为了接下来的 web 授权、方法授权和域对象授权。Spring Security 的其他部分可以解析角色中权利,执行某些操作时必须拥有这些权利。 GrantedAuthority
对象通常通过 UserDetailsService 加载。
通常 GrantedAuthority
是整个应用范围的权利许可,并不是针对于某个特定的领域对象。因此,你不应该用一个 GrantedAuthority 对象表示一个编号为 54 的 Employee
对象的许可,因为如果有数以千计的这样的权利,内存很快会被耗尽 (或者至少会引起应用要花费很长的时间才能验证一个用户)。当然,Spring Security 明确的设计可以处理这种常见需求,但是作为替代你应该使用项目的域对象的安全能力来实现这个目标。
总结
这里仅仅是简要概括,目前我们所看到的 Spring Security 的主要组成部分是:
SecurityContextHolder,提供对 SecurityContext 的访问
SecurityContext,维护了 Authentication 和可能的特定请求的安全信息
Authentication,以 Spring Security 的方式描述 principal。
GrantedAuthority,表示在应用范围内授予 principal 的权利许可。
UserDetailsService,用来根据传递的字符串形式的用户名 (或者验证 id 等类似信息) 来创建 UserDetails 对象。
2 基础配置
首先在项目的 pom.xml 中引入 spring-boot-starter-security 依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
和其余 Spring 框架一样,XML Configuration 和 Java Configuration 是 Spring Security 的两种常用配置方式。Spring 3.2 版本之后,Java Configuration 因其流式 API 支持,强类型校验等特性,逐渐替代 XML Configuration 成为更广泛的配置方式。下面是一个示例 Java Configuration。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowired MyUserDetailsService detailsService; @Override </span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(HttpSecurity http) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { http.authorizeRequests() .and().formLogin().loginPage(</span>"/login").permitAll().defaultSuccessUrl("/", <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) .and().logout().logoutUrl(</span>"/logout"<span style="color: rgba(0, 0, 0, 1)">) .and().sessionManagement().maximumSessions(</span>1).expiredUrl("/expired"<span style="color: rgba(0, 0, 0, 1)">) .and() .and().exceptionHandling().accessDeniedPage(</span>"/accessDenied"<span style="color: rgba(0, 0, 0, 1)">); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(WebSecurity web) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { web.ignoring().antMatchers(</span>"/js/**", "/css/**", "/images/**", "/**/favicon.ico"<span style="color: rgba(0, 0, 0, 1)">); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> configure(AuthenticationManagerBuilder auth) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Exception { auth.userDetailsService(detailsService).passwordEncoder(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> BCryptPasswordEncoder()); }
}
- @EnableWebSecurity: 禁用 Boot 的默认 Security 配置,配合 @Configuration 启用自定义配置(需要扩展 WebSecurityConfigurerAdapter)
- @EnableGlobalMethodSecurity(prePostEnabled = true): 启用 Security 注解,例如最常用的 @PreAuthorize
- configure(HttpSecurity): Request 层面的配置,对应 XML Configuration 中的
<http>
元素 - configure(WebSecurity): Web 层面的配置,一般用来配置无需安全检查的路径
- configure(AuthenticationManagerBuilder): 身份验证配置,用于注入自定义身份验证 Bean 和密码校验规则
3 扩展配置
完成基础配置之后,下一步就是实现自己的 UserDetailsService 和 PermissionEvaluator,分别用于自定义 Principle, Authority 和 Permission。
@Component public class MyUserDetailsService implements UserDetailsService {@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> LoginService loginService; @Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> RoleService roleService; @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> UserDetails loadUserByUsername(String username) { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (StringUtils.isBlank(username)) { </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> UsernameNotFoundException("用户名为空"<span style="color: rgba(0, 0, 0, 1)">); } Login login </span>= loginService.findByUsername(username).orElseThrow(() -> <span style="color: rgba(0, 0, 255, 1)">new</span> UsernameNotFoundException("用户不存在"<span style="color: rgba(0, 0, 0, 1)">)); Set</span><GrantedAuthority> authorities = <span style="color: rgba(0, 0, 255, 1)">new</span> HashSet<><span style="color: rgba(0, 0, 0, 1)">(); roleService.getRoles(login.getId()).forEach(r </span>-> authorities.add(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SimpleGrantedAuthority(r.getName()))); </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> org.springframework.security.core.userdetails.User( username, login.getPassword(), </span><span style="color: rgba(0, 0, 255, 1)">true</span>,<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">是否可用</span> <span style="color: rgba(0, 0, 255, 1)">true</span>,<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">是否过期</span> <span style="color: rgba(0, 0, 255, 1)">true</span>,<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">证书不过期为true</span> <span style="color: rgba(0, 0, 255, 1)">true</span>,<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">账户未锁定为true</span>
authorities);
}
}
创建 GrantedAuthority 对象时,一般名称加上 ROLE_ 前缀。
@Component public class MyPermissionEvaluator implements PermissionEvaluator {@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> LoginService loginService; @Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> RoleService roleService; @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { String username </span>=<span style="color: rgba(0, 0, 0, 1)"> authentication.getName(); Login login </span>=<span style="color: rgba(0, 0, 0, 1)"> loginService.findByUsername(username).get(); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> roleService.authorized(login.getId(), targetDomainObject.toString(), permission.toString()); } @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> not supported</span> <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">; } }</span></pre>
- hasPermission(Authentication, Object, Object) 和 hasPermission(Authentication, Serializable, String, Object) 两个方法分别对应 Spring Security 中两个同名的表达式。
4 业务集成
Spring Security 提供了注解,Servlet API,JSP Tag,系统 API 等多种方式进行集成,最常用的是第一种方式,包含 @Secured, @PreAuthorize, @PreFilter, @PostAuthorize 和 @PostFilter 五个注解。@Secure 是最初版本中的一个注解,自 3.0 版本引入了支持 Spring EL 表达式的其余四个注解之后,就很少使用了。
@RequestMapping(value = "/hello", method = RequestMethod.GET) @PreAuthorize("authenticated and hasPermission('hello','view')") public String hello(Model model) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); model.addAttribute("message", username); return "hello"; }
- @PreAuthorize("authenticated and hasPermission('hello','view')"): 表示只有当前已登录的并且拥有 ("hello", "view") 权限的用户才能访问此页面
- SecurityContextHolder.getContext().getAuthentication().getName(): 获取当前登录的用户,也可以通过 HttpServletRequest.getRemoteUser() 获取
总结
以上就是 Spring Security 的一般集成步骤,更多细节和高级特性可参考官方文档。
参考文章:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/
http://emacoo.cn/blog/spring-boot-security
http://www.tianshouzhi.com/api/tutorials/spring_security_4
https://zh.wikipedia.org/wiki/HTTP%E5%9F%BA%E6%9C%AC%E8%AE%A4%E8%AF%81