访问控制与 RBAC 模型访问控制通常的多用户系统都会涉及到访问控制（Access Control）。所谓访问控制，是指通过某种方式允许或限制用户访问能力及范围的一种方法。这主要是由于系统需要对关键资源进行保护，防止由于非法入侵或者误操作对业务系统造成破坏。简而言之，访问控制即定义哪些用户可以访问哪些资源。一般而言，访问控制系统包括三个组成部分：主体（Subject）：发出访问请求的实体，通常指用户或者用户进程。客体（Object）：被保护的资源，通常是程序、数据、文件或者设备。访问策略（Access Policy）：主体和客体的映射规则，确定一个主体是否对客体具有访问能力。RBAC 模型基于角色的访问控制（Role-Based Access Control, RBAC）的概念在七十年代就已经提出，但是直到九十年代由于安全需求的发展才又引起了广泛关注。RBAC 的核心思想是将系统资源的访问权限进行分类或者建立层次关系，抽象为角色的概念，然后根据安全策略将用户和角色关联，从而实现了用户和权限之间的对照。RBAC 通过引入角色并将其作为权限管理的中介，将访问控制系统分为两个部分，即权限与角色的关联和角色与用户的关联，具有灵活易控制的优点。RBAC 模型的建立和实现是基于角色实现访问控制研究中的两大热点。RBAC96 模型因全面系统地描述了多层次的含义而得到广泛认可。美国国家标准技术协会（NIST）基于 RBAC96 制定了 RBAC 标准，将 RBAC 主要分为：Core RBAC、Hierarchical RBAC、Constraint RBAC。RBAC96 模型包含 4 个层次，如图 1 所示：图 1. RBAC96 模型其中：RBAC0：包含 RBAC 的核心部分即 Core RBAC，是最基本的模型，仅包含基本的 RBAC 元素，即用户、角色、权限、会话。角色之间是平级关系，对象之间没有附加的约束。RBAC1：在 RBAC0 的基础上定义了角色继承关系。RBAC2：同样包含了 RBAC0，并且定义了约束，实际中需要考虑的常见的约束包括角色互斥和角色基数两种。RBAC3：包含了 RBAC1 和 RBAC2，即既有角色的继承关系，又包含约束关系，是最复杂的一种模型。图 2 清晰描述了 RBAC 模型中的用户、角色、权限、继承关系及约束关系。图 2. RBAC 模型关系图约束是 RBAC 的一个重要组成部分，比较常见的约束包括：互斥角色约束：指的是同一用户只可以分配到互斥角色集合中最多一个角色，以支持“职责分离”的原则。必备角色约束：也称为先决条件约束，是定义在用户角色分配关系（UA）和权限角色分配关系（PA）上的重要约束，是指所分配的角色或者权限依赖于另外一种角色或者权限。基数约束：同样也是定义在 UA 和 PA 上的约束，规定了一个角色可以被分配的最大用户数或者一项权限可允许分配的角色数目，从而来控制高级权限在系统中的分配。流行的 Web 安全框架随着网络应用的不断发展，越来越多的应用系统开始移植到 Web 环境，并且具有不同程度的访问控制的需求。随着这种需求的积累，出现了很多基于 RBAC 的 Web 访问控制框架。这些框架的核心理论就是 RBAC，并且为开发人员提供了方便的接口来完成定制化的需求。Membership in ASP.NETASP.NET 中提供了非常好的用户身份认证的管理框架——Membership，提供的功能包括创建新用户、身份认证、密码管理等。默认情况下，ASP.NET Membership 可以支持所有的 ASP.NET 应用程序，并提供了非常友好的使用方式，甚至提供了向导来创建所需要的数据结构——aspnet_regsql.exe。图 3. Membership 数据模型.NET 平台的易用性毋庸置疑，开发者只需要配置 web.config 文件，引入 authentication 配置，增加 membership 节点，再配置其角色管理 roleManager 基本上就完成了对 Membership 的引用。并且 .NET 甚至提供了用户控件 login 和 createUserWizard 等，通过属性的简单配置即可激活 Membership 的认证，完成创建用户的功能等等。本文中的企业应用没有基于 .NET 平台的 Membership 来实现，因此 Membership 并不是本文的重点，不再赘述。Spring Security in JavaJava Web 开发领域中的用于进行认证授权的框架为数众多，单就从 RBAC 的角度而言，Spring Security 已经占据重要的地位，并且被越来越广泛地应用在各种 Web 系统中，为基于 J2EE 企业应用软件提供了全面安全服务。Spring Security 来源于早期的 Spring 开发者邮件列表中的一个问题——提供一个基于 Spring 的安全实现，最初被称之为 Acegi，在 2003 年初作为 Sourceforge 的项目出现，并得到不断的应用和完善。2007 年底，它正式成为 Spring 的组合项目，更名为 Spring Security。接下来，我们将基于其架构重点研究 Spring Security，并基于实际应用需求来讨论一些定制化的实现方法。Spring Security 概述项目概览Spring Security 的上手非常简单，当然，这需要您对基本的认证授权有一些应用背景，并能了解一些其中的核心概念，如上文提到的用户、角色、权限等等。通过 Spring Security 项目的首页，您可以方便地得到它的介绍、文档、样例等等，并且，您还可以基于 Spring Security 的源码构建您的工程。这样，您可以深入地了解它的底层实现，快速地定位问题，方便地进行进一步扩展。特性Spring Security 为企业级应用提供了方便、灵活的安全解决方案，在众多的项目应用中得到锤炼和修正，已经非常稳定和强大。Spring Security 具有如下的特性：易于部署：通过 Spring 的依赖注入，Spring Security 提供了自定义的 XML 命名空间，可以通过不超过 10 行的配置就可以让您的应用程序受到保护。当然，这并不要求您的应用程序一定是基于 Spring 的，Spring Security 和 Struts 的结合使用非常普遍。非侵入式引用：通过 Spring Security 的 Filter Chain，应用程序基本上不需要做额外的太多修改，通过可插拔的方式，保证了应用程序本身和 Security 相关的代码的各自独立。明晰的授权服务：通过基于表达式的配置，安全访问控制规则可以方便灵活地基于当前请求上下文进行定义，不需要额外进行编码。同时 Spring Security 支持多种认证方式，并提供了定义良好的 API、多样化的认证后端等等，并且允许客户应用程序使用多种授权库，比如 XML 文件、数据库等，方便和既有的数据库系统进行集成。而且，它还提供了很多实用的功能，比如密码加密、Remember-me 持久化登录信息等等。RBAC 中的主要概念在 Spring Security 中的对照RBAC 中的几个主要构成在 Spring Security 中得到了很好的对应：主体：Spring Security 中的 Authentication 对象就是 RBAC 中的主体，多数情况下，可以被强制转换为 UserDetails 对象。这是一个 Spring Security 的核心接口，它代表一个主体，是扩展的，而且是为特定程序服务的。获得用户信息的最常用方法就是 loadUserByUsername(String username.html)，当成功通过验证时，UserDetails 会被用来建立 Authentication 对象，保存在 SecurityContextHolder 里。客体：Spring Security 中的客体可以是 URL，也可以是 Method。访问策略接口：Spring Security 通过 getAuthorities() 方法提供了 GrantedAuthority 对象数组，代表赋予到主体的权限。这些权限通常使用角色表示，比如本文中计费系统中声明的角色 ROLE_ADMINISTRATOR 或 ROLE_PROVIDER。核心服务Security Filter ChainSpring Security 框架提供了一个过滤器链，来提供各种服务。通过下面的过滤器链表格，我们可以看到 Spring Security 为我们提供了哪些实用的服务。表 1. Spring Security 过滤器清单别名过滤器类对应的命名空间元素CHANNEL_FILTERChannelProcessingFilterhttp/intercept-url@requires-channelCONCURRENT_SESSION_FILTERConcurrentSessionFiltersession-management/concurrency-controlSECURITY_CONTEXT_FILTERSecurityContextPersistenceFilterhttpLOGOUT_FILTERLogoutFilterhttp/logoutX509_FILTERX509AuthenticationFilterhttp/x509PRE_AUTH_FILTERAbstractPreAuthenticatedProcessingFilterN/ACAS_FILTERCasAuthenticationFilterN/AFORM_LOGIN_FILTERUsernamePasswordAuthenticationFilterhttp/form-loginBASIC_AUTH_FILTERBasicAuthenticationFilterhttp/http-basicSERVLET_API_SUPPORT_FILTERSecurityContextHolderAwareFilterhttp/@servlet-api-provisionREMEMBER_ME_FILTERRememberMeAuthenticationFilterhttp/remember-meANONYMOUS_FILTERAnonymousAuthenticationFilterhttp/anonymousSESSION_MANAGEMENT_FILTERSessionManagementFiltersession-managementEXCEPTION_TRANSLATION_FILTERExceptionTranslationFilterhttpFILTER_SECURITY_INTERCEPTORFilterSecurityInterceptorhttpSWITCH_USER_FILTERSwitchUserAuthenticationFilterN/A开发人员可以使用 before 或者 after 属性来将自定义的过滤器添加到过滤器链中。本文中的计费系统中采用了自定义的 loginFilter 和 logoutFilter，并使用 before 属性将其分别置于 FORM_LOGIN_FILTER 和 LOGOUT_FILTER 之前，如清单 1 所示：清单 1. 过滤器配置示例<custom-filter before="FORM_LOGIN_FILTER" ref="loginFilter" /> <custom-filter before="LOGOUT_FILTER" ref="logoutFilter" />AuthenticationManagerAuthenticationManager 只是一个接口，其实现可以让开发人员灵活选择。在 Spring Security 中的默认实现是 ProviderManager，Spring Security 通过委派一系列配置好的 AuthenticationProvider，遍历 Providers 的过程中调用 authenticate() 方法，以保证获取不同来源的身份认证。若某个 Provider 能成功确认当前用户的身份，authenticate() 方法会返回一个完整的包含用户授权信息的 Authentication 对象，否则会抛出一个 AuthenticationException。AccessDecisionManager当我们使用命名空间配置时，默认的 AccessDecisionManager 实例会自动注册，基于我们设置的 intercept-url 和 protect-pointcut 权限属性内容，来为方法级别调用和 Web 请求级别访问提供认证服务。基于 Spring Security 实现认证授权数据模型多数基于 RBAC 的框架都提供了基础的数据模型，通常，我们需要对其进行扩展来满足企业级应用系统的需求。扩展的方式一般有两种，即在原有数据结构上进行修改，扩展自定义字段；或者，通过表之间的关联将应用系统的数据结构和 RBAC 的数据结构分离。两种方式各有优劣，通常需要看应用的复杂程度及对框架的开放程度进行不同的选择。本文中计费系统因为用户角色权限关系清晰，故采用在原有数据结构基础上进行自定义数据字段的方式，比如，在 Users 表中增加了系统用户的一些属性，如联系方式、电话等等。快速实现计费系统认证授权需求Spring Security 可以被应用在多种不同的认证环境下，因为它的目标是自管理，即不依赖于容器，所以备受推崇，因为这样可以方便地进行部署和移植。计费系统中主要包含三类用户，即超级管理员、运营商管理员和普通用户。每种用户可以使用的系统功能模块集合和数据集合是不同的。比如，运营商管理员不能使用网关设备管理模块，并且只能管理属于自己的用户，而超级管理员可以管理网关设备，并且可以管理全部用户。因为 Spring Security 基于 URL 实现对资源的授权，所以在开发初期，我们需要规划好资源的路径，以便于进行授权配置，即建立 admin、provider、customer 目录，网关设备的管理目录放在 admin 目录下，而超级管理员和运营商管理员都具有的功能模块放在 provider 目录下。引入 Spring Security通过上面的需求分析，可以看出，使用 Spring Security 完全可以实现计费系统的认证授权功能，并且可以非常快速地验证。首先，我们要将需要的 jar 文件引入到工程中，包括：spring-security-acl-3.0.3.RELEASE.jarspring-security-config-3.0.3.RELEASE.jarspring-security-core-3.0.3.RELEASE.jarspring-security-taglibs-3.0.3.RELEASE.jarspring-security-web-3.0.3.RELEASE.jar其次，我们需要完成以下几个部分的修改：修改 web.xml 设置过滤器，代码如清单 2 所示。清单 2. Web.xml 配置示例<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>修改 applicationContext-Security.xml，这个文件可以根据需要设定，在容器启动时加载，其中设置了实现 HTTP 安全和认证授权相关的服务 Bean 来应用框架的认证授权功能，如清单 3 所示。清单 3. applicationContext-Security.xml 配置示例<http entry-point-ref="loginUrlEntryPoint" access-denied-page="/common/403.jsp"> <intercept-url pattern="/image/**" filters="none" /> <intercept-url pattern="/common/**" filters="none" /> <intercept-url pattern="/admin/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_PROVIDER" /> <custom-filter before="FORM_LOGIN_FILTER" ref="loginFilter" /> <custom-filter position="FORM_LOGIN_FILTER" ref="adminLoginFilter" /> <custom-filter before="LOGOUT_FILTER" ref="logoutFilter" /> <custom-filter position="LOGOUT_FILTER" ref="adminLogoutFilter" /> </http> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="usersService"> </authentication-provider> </authentication-manager>实现自定义的 userDetailsService。通常我们需要对认证授权相关的数据结构进行其他的操作，这些操作并没有集成在 Spring Security 中，因此多数我们需要实现自己的 userDetailsService，如清单 4 所示。清单 4. userDetailsService Bean 配置示例<beans:bean id="usersService" class="com.fengfan.service.impl.UsersServiceImpl"> <beans:property name="dataSource" ref="dataSource"/> <beans:property name="authenticationManager" ref="authenticationManager"/> <beans:property name="usersDAO" ref="usersDAO"/> </beans:bean>通过以上几部分的修改，我们就可以方便地集成 Spring Security，进入企业应用系统的认证授权开发了。接下来，我们将讨论更多的定制化需求的实现方式，以此来说明 Spring Security 在企业级应用系统开发中的可扩展性和灵活性及易用性。定制化需求计费系统中有很多定制化的需求实现，下面，我们将就各个需求点进行说明，Spring Security 的成熟度和灵活性在这里得到了充分的体现。基于 URL 的授权实现通过上面的系统需求，我们实现了清单 5 所示的基于 URL 的权限约束：清单 5. 基于 URL 的权限约束<intercept-url pattern="/admin/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_ADMIN" /> <intercept-url pattern="/admin_provider/**" access="ROLE_PROVIDER" />这种资源的授权方式要求在开发初期对目录结构进行较清晰的规划，从而避免相同功能页面的重复拷贝的问题。当然，我们可以将资源及授权操作通过数据结构的进一步扩展都保存在数据库中，这样具有更大的灵活性。自定义的 UserDetailsService除了认证授权涉及到系统用户外，系统本身需要提供对系统用户的信息维护功能，因此，我们需要自定义 UserDetailsService，这也非常方便，我们只需要继承 JdbcUserDetailsManager，并实现 IUsersService 接口即可。这样，我们就可以在 UsersServiceImpl 中实现定制的和系统用户相关的方法，同时通过继承也得到了和安全认证相关的方法，如清单 6 所示。清单 6. UsersServiceImpl 方法声明public class UsersServiceImpl extends JdbcUserDetailsManager implements IUsersService如何使用 Remember-Me通常我们在应用系统中如果要实现这个功能，需要增加一定的编码工作才能实现，需要操作 Cookies 对象。但是 Spring Security 为 Remember-Me 实现提供了必要的调用钩子，并提供了两个 Remember-Me 的具体实现。其中一个使用散列来保护基于 Cookie 标记的安全性，另一个使用了数据库或其他持久化存储机制来保存生成的标记。我们只需要在 <http> 段下面添加 <remember-me> 元素就可以使用 Remember-Me 认证。会话同步控制首先，您需要把下面的监听器添加到您的 web.xml 文件里，让 Spring Security 获得 Session 生存周期事件：<listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener>然后可以在 HTTP 安全配置中增加一个 session-management 段的配置，设置其中的 concurrency-control 来控制一个帐号最多允许登录多少次，比如 <concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>，这样，当重复登录时就会出现错误提示。不同的登录页面在上面的 HTTP 安全的设置部分，我们已经看到为不同的用户的登录和退出设置了不同的自定义过滤器，下面我们通过超级管理的 loginFilter 的配置来说明如何实现不同的用户登录之后到不同的主页，如清单 7 所示。清单 7. 超级管理员的登录配置 <beans:bean id="adminLoginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationFailureHandler" ref="adminFailureHandler" /> <beans:property name="authenticationSuccessHandler" ref="adminSuccessHandler" /> <beans:property name="filterProcessesUrl" value="/login_admin" /> </beans:bean>从上面的配置我们看到当登录成功后，使用 authenticationSuccessHandler 来处理后续动作，我们来看一下如下的这个 Bean 的配置，就可以明白如何通过 defaultTargetUrl 属性来设置其登录成功之后跳转的页面了。我们只需要再配置另外一个 Bean，设置不同的属性值，就可以实现不同用户登录之后到达不同的主页面的需求，如清单 8 所示。清单 8. 不同主页面跳转配置<beans:bean id="adminSuccessHandler" class="com.fengfan.security.AppAuthenticationSuccessHandler"> <beans:property name="alwaysUseDefaultTargetUrl" value="true" /> <beans:property name="defaultTargetUrl" value="/admin/index.jsp" /> <beans:property name="usersDAO" ref="usersDAO"/> </beans:bean>同样的，我们可以设置不同的 logoutFilter，这样不同角色的用户选择退出系统之后可以拥有不同的退出成功的页面或者跳转的相应的重新登录的页面。修改认证信息后不需要重新登录很多时候，用户在登录之后可能对自身的信息进行修改，但是，我们不需要用户来重新登录，而是希望修改的信息在当前的会话中立即生效。Spring Security 提供了相应的接口方便我们实现这种需求，比如我们在用户修改密码之后，将主体中的信息也同时更新，如清单 9 所示。清单 9. 修改密码代码示范userCache.removeUserFromCache(username.html); SecurityContextHolder.getContext().setAuthentication( createNewAuthentication(currentUser, newPassword.html));总结本文从计费系统的实际应用出发，通过分析系统的认证授权需求，研究 RBAC 的基础模型和 Spring Security 框架的核心组件及服务，充分发挥 Spring Security 的成熟度和灵活性的优势，快速完善地实现了计费管理中的认证授权系统，取得了很好的效果。相关主题查看文章"Role Based Access Control（RBAC) and Role Based Security"，了解 RBAC 模型的基本原理。查看文章"An Introduction to Role-based Access Control"，了解常见 RBAC 模型及应用。查看文章"Spring Security"，了解 Spring Security 的设计和使用。查看文章"Introduction to Membership"，了解微软身份认证管理框架。developerWorks Java 技术专区：这里有数百篇关于 Java 编程各个方面的文章。说明：本文基于 Spring Security 3.x 版本编写（文中提及最新版本为 3.1.0），当前 Spring Security 主流版本已更新至 6.x，部分配置方式、类名及命名空间可能存在差异，实际开发中请参考官方最新文档。
目录从一个 Spring Security 的例子开始创建不受保护的应用加入 Spring Security 保护应用关闭 HTTP Basic，使用 Form 表单页面登录角色 - 资源访问控制获取当前登录用户信息小结Spring Security 核心组件SecurityContextSecurityContextHolderAuthenticationUserDetailsUserDetailsServiceAuthenticationManager小结Spring Security 的一些工作原理Spring Security 在 Web 应用中是基于 Filter 的DelegatingFilterProxySpring Security 入口——springSecurityFilterChainFilterChainProxy 和 SecurityFilterChain小结再说 SecurityFilterChainSpring Security 的一些实战通过数据库查询、存储用户和角色实现安全认证添加 Spring Data JPA，创建数据表，并添加数据自定义 UserDetailsService验证效果Spring Security Session 无状态前后端分离应用中自定义 Token 整合 Spring Security总结在 Web 应用开发中，安全无疑是十分重要的，选择 Spring Security 来保护 Web 应用是一个非常好的选择。Spring Security 是 Spring 项目之中的一个安全模块，可以非常方便地与 Spring 项目无缝集成。特别是在 Spring Boot 项目中加入 Spring Security 更是十分简单。本篇我们介绍 Spring Security，以及 Spring Security 在 Web 应用中的使用。说明：本文最初发表之时，使用的是 Spring Boot 1.x 的版本。Spring Boot 2.x 版本之后有些不同，2019 年 12 月 18 日有稍微补充。同时也补充了可以运行的实例源码，基于 Spring Boot 2.1.8.RELEASE。从一个 Spring Security 的例子开始创建不受保护的应用假设我们现在创建好了一个 Spring Boot 的 Web 应用。如果没有的话，可以在这里下载代码：GitHub 示例代码 (springboot2.x)。其中有一个控制器如下：@Controller public class AppController { @RequestMapping("/hello".html) @ResponseBody String home() { return "Hello ,spring security!"; } }我们启动应用，假设端口是 8080，那么当我们在浏览器访问 http://localhost:8080/hello 的时候，可以在浏览器看到 Hello ,spring security!。加入 Spring Security 保护应用此时，/hello 是可以自由访问的。假设我们需要具有某个角色的用户才能访问，我们可以引入 Spring Security 来进行保护。加入如下依赖，并重启应用：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>再次访问 /hello，我们可以得到一个 HTTP Basic 的认证弹窗，如下：说明 Spring Security 已经起作用了。如果我们点击取消，则会看到错误信息，如下所示：There was an unexpected error (type=Unauthorized, status=401.html).2019 年 12 月补充说明：升级 Spring Boot 到 2.x 之后，没有这个 HTTP Basic 的弹窗了，这个后面说。关闭 HTTP Basic，使用 Form 表单页面登录我们在实际项目中不可能会使用上面 HTTP Basic 方式的弹窗来让用户完成登录，而是会有一个登录页面。所以，我们需要关闭 HTTP Basic 的方式。关闭 HTTP Basic 方式的认证弹窗的配置如下（针对 Spring Boot 1.x）：security.basic.enabled=falseSpring Security 默认提供了表单登录的功能。我们新建一个类 SecurityConfiguration，并加入一些代码，如下所示：@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http.html) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } }上面的代码其实是一种配置，authorizeRequests() 定义哪些 URL 需要被保护、哪些不需要被保护。formLogin() 定义当需要用户登录时候，转到的登录页面。此时，我们并没有写登录页面，但是 Spring Security 默认提供了一个登录页面，以及登录控制器。加完了上面的配置类之后，我们重启应用。然后继续访问 http://localhost:8080/hello。会发现自动跳转到一个登录页面了，如下所示：升级 Spring Boot 到 2.x 之后，这个页面变漂亮了，是这样的：这个页面是 Spring Security 提供的默认的登录页面，其 HTML 内容如下： Login Page Login with Username and Password User: Password: 我们可以发现，这里有个 form。action="/login"，这个 /login 依然是 Spring Security 提供的。Form 表单提交了三个数据：username 用户名password 密码_csrf CSRF 保护方面的内容，暂时先不展开解释为了登录系统，我们需要知道用户名密码。Spring Security 默认的用户名是 user，Spring Security 启动的时候会生成默认密码（在启动日志中可以看到）。本例，我们指定一个用户名密码，在配置文件中加入如下内容：# security security.basic.enabled=false security.user.name=admin security.user.password=admin重启项目，访问被保护的 /hello 页面。自动跳转到了 Spring Security 默认的登录页面，我们输入用户名 admin 密码 admin。点击 Login 按钮。会发现登录成功并跳转到了 /hello。除了登录，Spring Security 还提供了 RememberMe 功能，这里不做过多解释。补充说明：Spring Boot 2.x 之后，上面的配置 security.user.name 会提示错误 Deprecated configuration property 'security.user.password。因为 Spring Boot 2.x 之后，Spring Security 升级了，删了一些配置。如果使用的旧的，需要迁移的话，可以参考 Spring-Boot-2.0-Migration-Guide。角色 - 资源访问控制通常情况下，我们需要实现“特定资源只能由特定角色访问”的功能。假设我们的系统有如下两个角色：ADMIN 可以访问所有资源USER 只能访问特定资源现在我们给系统增加 /product 代表商品信息方面的资源（USER 可以访问）；增加 /admin 代表管理员方面的资源（USER 不能访问）。代码如下：@Controller @RequestMapping("/product".html) public class ProductTestController { @RequestMapping("/info".html) @ResponseBody public String productInfo(){ return " some product info "; } }@Controller @RequestMapping("/admin".html) public class AdminTestController { @RequestMapping("/home".html) @ResponseBody public String productInfo(){ return " admin home page "; } }在正式的应用中，我们的用户和角色是保存在数据库中的；本例为了方便演示，我们来创建两个存放于内存的用户和角色。我们在上一步中创建的 SecurityConfiguration 中增加角色用户，如下代码：@Override protected void configure(AuthenticationManagerBuilder auth.html) throws Exception { auth .inMemoryAuthentication() .withUser("admin1".html) // 管理员，同时具有 ADMIN,USER 权限，可以访问所有资源 .password("admin1".html) .roles("ADMIN", "USER".html) .and() .withUser("user1".html).password("user1".html) // 普通用户，只能访问 /product/** .roles("USER".html); }这里，我们增加了管理员（admin1，密码 admin1），以及普通用户（user1，密码 user1）。补充说明：上面的代码是 Spring Boot 1.x 的。Spring Boot 2.x 之后，上面的配置就得稍作调整了，否则会报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"。在 Spring Boot 2.x 的版本中，上面的代码会调整成这样：@Override protected void configure(AuthenticationManagerBuilder auth.html) throws Exception { auth .inMemoryAuthentication() .withUser("admin1".html) // 管理员，同时具有 ADMIN,USER 权限，可以访问所有资源 .password("{noop}admin1".html) // .roles("ADMIN", "USER".html) .and() .withUser("user1".html).password("{noop}user1".html) // 普通用户，只能访问 /product/** .roles("USER".html); }变化的地方在：.password("{noop}admin1".html) 这里。继续增加“链接 - 角色”控制配置，代码如下：@Override protected void configure(HttpSecurity http.html) throws Exception { http .authorizeRequests() .antMatchers("/product/**".html).hasRole("USER".html) .antMatchers("/admin/**".html).hasRole("ADMIN".html) .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }这个配置在上一步中登录配置的基础上增加了链接对应的角色配置。上面的配置，我们可以知道：使用 user1 登录，只能访问 /product/**使用 admin1 登录，可以访问所有。下面来验证一下普通用户登录。重启项目，在浏览器中输入：http://localhost:8080/admin/home。同样，我们会到达登录页面，我们输入用户名 user1，密码也为 user1，结果错误页面了，拒绝访问了，信息为：There was an unexpected error (type=Forbidden, status=403.html). Access is denied我们把浏览器中的 URI 修改成：/product/info，结果访问成功。可以看到 some product info。说明 user1 只能访问 product/**，这个结果与我们预期一致。再来验证一下管理员用户登录。重启浏览器之后，输入 http://localhost:8080/admin/home。在登录页面中输入用户名 admin1，密码 admin1，提交之后，可以看到 admin home page，说明访问管理员资源了。我们再将浏览器 URI 修改成 /product/info，刷新之后，也能看到 some product info，说明 admin1 用户可以访问所有资源，这个也和我们的预期一致。获取当前登录用户信息上面我们实现了“资源 - 角色”的访问控制，效果和我们预期的一致，但是并不直观。我们不妨尝试在控制器中获取“当前登录用户”的信息，直接输出，看看效果。以 /product/info 为例，我们修改其代码，如下：@RequestMapping("/info".html) @ResponseBody public String productInfo(){ String currentUser = ""; Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if(principal instanceof UserDetails.html) { currentUser = ((UserDetails.html)principal).getUsername(); } else { currentUser = principal.toString(); } return " some product info,currentUser is: "+currentUser; }这里，我们通过 SecurityContextHolder 来获取了用户信息，并拼接成字符串输出。重启项目，在浏览器访问 http://localhost:8080/product/info。使用 admin1 的身份登录，可以看到浏览器显示 some product info,currentUser is: admin1。小结至此，我们已经对 Spring Security 有了一个基本的认识了。了解了如何在项目中加入 Spring Security，以及如何控制资源的角色访问控制。Spring Security 原不止这么简单，我们才刚刚开始。为了能够更好的在实战中使用 Spring Security，我们需要更深入的了解。下面我们先来了解 Spring Security 的一些核心概念。Spring Security 核心组件Spring Security 核心组件有：SecurityContext、SecurityContextHolder、Authentication、UserDetails 和 AuthenticationManager，下面分别介绍。SecurityContext安全上下文，用户通过 Spring Security 的校验之后，验证信息存储在 SecurityContext 中。SecurityContext 的接口定义如下：public interface SecurityContext extends Serializable { /** * Obtains the currently authenticated principal, or an authentication request token. * * @return the Authentication or null if no authentication * information is available */ Authentication getAuthentication(); /** * Changes the currently authenticated principal, or removes the authentication * information. * * @param authentication the new Authentication token, or * null if no further authentication information should be stored */ void setAuthentication(Authentication authentication.html); }可以看到 SecurityContext 接口只定义了两个方法，实际上其主要作用就是获取 Authentication 对象。SecurityContextHolderSecurityContextHolder 顾名思义，是一个 Holder，用来 Hold 住 SecurityContext 实例的。在典型的 Web 应用程序中，用户登录一次，然后由其会话 ID 标识。服务器缓存持续时间会话的主体信息。在 Spring Security 中，在请求之间存储 SecurityContext 的责任落在 SecurityContextPersistenceFilter 上。默认情况下，该上下文将上下文存储为 HTTP 请求之间的 HttpSession 属性。它会为每个请求恢复上下文 SecurityContextHolder，并且最重要的是，在请求完成时清除 SecurityContextHolder。SecurityContextHolder 是一个类，他的功能方法都是静态的（static）。SecurityContextHolder 可以设置指定 JVM 策略（SecurityContext 的存储策略），这个策略有三种：MODE_THREADLOCAL：SecurityContext 存储在线程中。MODE_INHERITABLETHREADLOCAL：SecurityContext 存储在线程中，但子线程可以获取到父线程中的 SecurityContext。MODE_GLOBAL：SecurityContext 在所有线程中都相同。SecurityContextHolder 默认使用 MODE_THREADLOCAL 模式，即存储在当前线程中。在 Spring Security 应用中，我们通常能看到类似如下的代码：SecurityContextHolder.getContext().setAuthentication(token.html);其作用就是存储当前认证信息。AuthenticationAuthentication 直译过来是“认证”的意思。在 Spring Security 中，Authentication 用来表示当前用户是谁，一般来讲你可以理解为 Authentication 就是一组用户名密码信息。Authentication 也是一个接口，其定义如下：public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated.html) throws IllegalArgumentException; }接口有 4 个 get 方法，分别获取：Authorities：填充的是用户角色信息。Credentials：直译，证书。填充的是密码。Details：用户信息。Principal：直译，形容词是“主要的，最重要的”，名词是“负责人，资本，本金”。感觉很别扭，所以，还是不翻译了，直接用原词 principal 来表示这个概念，其填充的是用户名。因此可以推断其实现类有这 4 个属性。这几个方法作用如下：getAuthorities：获取用户权限，一般情况下获取到的是用户的角色信息。getCredentials：获取证明用户认证的信息，通常情况下获取到的是密码等信息。getDetails：获取用户的额外信息（这部分信息可以是我们的用户表中的信息）。getPrincipal：获取用户身份信息。在未认证的情况下获取到的是用户名，在已认证的情况下获取的是 UserDetails（UserDetails 也是一个接口，里边的方法有 getUsername、getPassword 等）。isAuthenticated：获取当前 Authentication 是否已认证。setAuthenticated：设置当前 Authentication 是否已认证（true or false）。UserDetailsUserDetails，顾名思义，是用户信息的意思。其存储的就是用户信息，其定义如下：public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }方法含义如下：getAuthorities：获取用户权限，本质上是用户的角色信息。getPassword：获取密码。getUsername：获取用户名。isAccountNonExpired：账户是否过期。isAccountNonLocked：账户是否被锁定。isCredentialsNonExpired：密码是否过期。isEnabled：账户是否可用。UserDetailsService提到了 UserDetails 就必须得提到 UserDetailsService。UserDetailsService 也是一个接口，且只有一个方法 loadUserByUsername，它可以用来获取 UserDetails。通常在 Spring Security 应用中，我们会自定义一个 CustomUserDetailsService 来实现 UserDetailsService 接口，并实现其 public UserDetails loadUserByUsername(final String login.html); 方法。我们在实现 loadUserByUsername 方法的时候，就可以通过查询数据库（或者是缓存、或者是其他的存储形式）来获取用户信息，然后组装成一个 UserDetails（通常是一个 org.springframework.security.core.userdetails.User，它继承自 UserDetails）并返回。在实现 loadUserByUsername 方法的时候，如果我们通过查库没有查到相关记录，需要抛出一个异常来告诉 Spring Security 来“善后”。这个异常是 org.springframework.security.core.userdetails.UsernameNotFoundException。AuthenticationManagerAuthenticationManager 是一个接口，它只有一个方法，接收参数为 Authentication，其定义如下：public interface AuthenticationManager { Authentication authenticate(Authentication authentication.html) throws AuthenticationException; }AuthenticationManager 的作用就是校验 Authentication，如果验证失败会抛出 AuthenticationException 异常。AuthenticationException 是一个抽象类，因此代码逻辑并不能实例化一个 AuthenticationException 异常并抛出。实际上抛出的异常通常是其实现类，如 DisabledException、LockedException、BadCredentialsException 等。BadCredentialsException 可能会比较常见，即密码错误的时候。小结这里，我们只是简单的了解了 Spring Security 中有哪些东西，先混个脸熟。这里并不需要我们一下子全记住这些名词和概念。先大概看看，有个印象。Spring Security 的一些工作原理在第一节中，我们通过在 pom 文件中增加 spring-boot-starter-security 依赖，便使得我们的项目受到了 Spring Security 保护。又通过增加 SecurityConfiguration 实现了一些安全配置，实现了链接资源的个性化访问控制。那么这是如何实现的呢？了解其原理，可以使我们使用起来得心应手。Spring Security 在 Web 应用中是基于 Filter 的在 Spring Security 的官方文档中，我们可以看到这么一句话：Spring Security's web infrastructure is based entirely on standard servlet filters.我们可以得知，Spring Security 在 Web 应用中是基于 Filter 的。Filter 我们就很熟了，在没有 Struts、没有 Spring MVC 之前，我们就是通过一个个 Servlet、一个个 Filter 来实现业务功能的。通常我们会有多个 Filter，他们按序执行，一个执行完之后，调用 FilterChain 中的下一个 doFilter。Spring Security 在 Filter 中创建 Authentication 对象，并调用 AuthenticationManager 进行校验。Spring Security 维护了一个 Filter Chain，Chain 中的每一个 Filter 都具有特定的责任，并根据所需的服务在配置总添加。Filter 的顺序很重要，因为他们之间存在依赖关系。Spring Security 中有如下 Filter（按顺序的）：ChannelProcessingFilter：因为它可能需要重定向到不同的协议。SecurityContextPersistenceFilter：可以在 Web 请求开头的 SecurityContextHolder 中设置 SecurityContext，并且 SecurityContext 的任何更改都可以复制到 HttpSession，当 Web 请求结束时（准备好与下一个 Web 请求一起使用）。ConcurrentSessionFilter。身份验证处理：UsernamePasswordAuthenticationFilter、CasAuthenticationFilter、BasicAuthenticationFilter 等。以便 SecurityContextHolder 可以修改为包含有效的 Authentication 请求令牌。SecurityContextHolderAwareRequestFilter。JaasApiIntegrationFilter。RememberMeAuthenticationFilter：记住我服务处理。AnonymousAuthenticationFilter：匿名身份处理，更新 SecurityContextHolder。ExceptionTranslationFilter：获取任何 Spring Security 异常，以便可以返回 HTTP 错误响应或启动适当的 AuthenticationEntryPoint。FilterSecurityInterceptor：用于保护 Web URI 并在访问被拒绝时引发异常。这里我们列举了几乎所有的 Spring Security Filter。正是这些 Filter 完成了 Spring Security 的各种功能。目前我们只是知道了有这些 Filter，并不清楚他们是怎么集成到应用中的。在继续深入了解之前，我们需要了解一下 DelegatingFilterProxy。DelegatingFilterProxyDelegatingFilterProxy 是一个特殊的 Filter，存在于 spring-web 模块中。DelegatingFilterProxy 通过继承 GenericFilterBean 使得自己变为了一个 Filter（因为 GenericFilterBean implements Filter）。它是一个 Filter，其命名却以 Proxy 结尾。非常有意思，为了了解其功能，我们看一下它的使用配置：<filter> <filter-name>myFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这个配置是我们使用 web.xml 配置 Filter 时的做法。但是与普通的 Filter 不同的是，DelegatingFilterProxy 并没有实际的过滤逻辑。他会尝试寻找 filter-name 节点所配置的 myFilter，并将过滤行为委托给 myFilter 来处理。这种方式能够利用 Spring 丰富的依赖注入工具和生命周期接口，因此 DelegatingFilterProxy 提供了 web.xml 与应用程序上下文之间的链接。非常有意思，可以慢慢体会。Spring Security 入口——springSecurityFilterChainSpring Security 的入口 Filter 就是 springSecurityFilterChain。在没有 Spring Boot 之前，我们要使用 Spring Security 的话，通常在 web.xml 中添加如下配置：<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>看到没，这里配置的是 DelegatingFilterProxy。有了上面的介绍之后，我们就知道，它实际上会去找到 filter-name 节点中的 Filter——springSecurityFilterChain，并将实际的过滤工作交给 springSecurityFilterChain 处理。在使用 Spring Boot 之后，这一 XML 配置被 Java 类配置给代替了。我们前面在代码中使用过 @EnableWebSecurity 注解，通过跟踪源码可以发现 @EnableWebSecurity 会加载 WebSecurityConfiguration 类，而 WebSecurityConfiguration 类中就有创建 springSecurityFilterChain 这个 Filter 的代码：@Bean(name = {"springSecurityFilterChain"}.html) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty(); if (!hasConfigurers.html) { WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter.html)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter(.html) { }); this.webSecurity.apply(adapter.html); } return (Filter.html)this.webSecurity.build(); }这里，我们介绍了 Spring Security 的入口——springSecurityFilterChain，也介绍了它的两种配置形式。但是，springSecurityFilterChain 是谁，怎么起作用的，我们还不清楚，下面继续看。FilterChainProxy 和 SecurityFilterChain在 Spring 的官方文档中，我们可以发现这么一句话：Spring Security's web infrastructure should only be used by delegating to an instance of FilterChainProxy. The security filters should not be used by themselves.Spring Security 的 Web 基础设施（上面介绍的那一堆 Filter）只能通过委托给 FilterChainProxy 实例的方式来使用。而不能直接使用那些安全 Filter。这句话似乎透漏了一个信号，上面说的入口 springSecurityFilterChain 其实就是 FilterChainProxy。如果不信，调试一下代码也能发现，确实就是 FilterChainProxy。它的全路径名称是 org.springframework.security.web.FilterChainProxy。打开其源码，第一行注释是这样：Delegates {@code Filter} requests to a list of Spring-managed filter beans.所以，没错了。它就是 DelegatingFilterProxy 要找的人，它就是 DelegatingFilterProxy 要委托过滤任务的人。下面贴出其部分代码：public class FilterChainProxy extends GenericFilterBean { private List<SecurityFilterChain> filterChains; public FilterChainProxy(SecurityFilterChain chain.html) { this(Arrays.asList(chain.html)); } public FilterChainProxy(List<SecurityFilterChain> filterChains.html) { this.filterChains = filterChains; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilterInternal(request, response, chain.html); } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest.html) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse.html) response); List<Filter> filters = getFilters(fwRequest.html); if (filters == null || filters.size(.html) == 0) { fwRequest.reset(); chain.doFilter(fwRequest, fwResponse.html); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters.html); vfc.doFilter(fwRequest, fwResponse.html); } private List<Filter> getFilters(HttpServletRequest request.html) { for (SecurityFilterChain chain : filterChains.html) { if (chain.matches(request.html)) { return chain.getFilters(); } } return null; } }可以看到，里边有个 SecurityFilterChain 的集合。这个才是众多 Security Filter 藏身之处。doFilter 的时候会从 SecurityFilterChain 取出第一个匹配的 Filter 集合并返回。小结说到这里，可能有点模糊了。这里小结一下，梳理一下。Spring Security 的核心是基于 Filter。入口 Filter 是 springSecurityFilterChain（它会被 DelegatingFilterProxy 委托来执行过滤任务）。springSecurityFilterChain 实际上是 FilterChainProxy（一个 Filter）。FilterChainProxy 里边有一个 SecurityFilterChain 集合，doFilter 的时候会从其中取。到这里，思路清楚多了，现在还不知道 SecurityFilterChain 是怎么来的。下面介绍。再说 SecurityFilterChain前面我们介绍了 springSecurityFilterChain，它是由 XML 配置的，或者是由 @EnableWebSecurity 注解的作用下初始化的（@Import({WebSecurityConfiguration.class}））。具体是在 WebSecurityConfiguration 类中。上面我们贴过代码，你可以返回看，这里再次贴出删减版：@Bean(name = {"springSecurityFilterChain"}.html) public Filter springSecurityFilterChain() throws Exception { // 删除部分代码 return (Filter.html)this.webSecurity.build(); }最后一行，发现 webSecurity.build() 产生了 FilterChainProxy。因此，推断 SecurityFilterChain 就是 webSecurity 里边弄的。贴出源码：public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements SecurityBuilder<Filter>, ApplicationContextAware { @Override protected Filter performBuild() throws Exception { int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); // 我们要找的 securityFilterChains List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests.html) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest.html)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders.html) { securityFilterChains.add(securityFilterChainBuilder.build(.html)); } // 创建 FilterChainProxy，传入 securityFilterChains FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains.html); if (httpFirewall != null.html) { filterChainProxy.setFirewall(httpFirewall.html); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; postBuildAction.run(); return result; } }至此，我们清楚了，Spring Security 是怎么在 Spring Web 应用中工作的了。具体的细节就是执行 Filter 里的代码了，这里不再继续深入了。我们的目的是摸清楚他是怎么工作的，大致的脉路是怎样，目前整理的内容已经达到这个目的了。Spring Security 的一些实战下面开始一些实战使用 Spring Security 的实例。依然依托开篇的例子，并在此基础上调整。通过数据库查询、存储用户和角色实现安全认证开篇的例子中，我们使用了内存用户角色来演示登录认证。但是实际项目我们肯定是通过数据库完成的。实际项目中，我们可能会有 3 张表：用户表，角色表，用户角色关联表。当然，不同的系统会有不同的设计，不一定非得是这样的三张表。本例演示的意义在于：如果我们想在已有项目中增加 Spring Security 的话，就需要调整登录了。主要是自定义 UserDetailsService。此外，可能还需要处理密码的问题，因为 Spring 并不知道我们怎么加密用户登录密码的。这时，我们可能需要自定义 PasswordEncoder，下面也会提到。添加 Spring Data JPA，创建数据表，并添加数据继续完善开篇的项目，现在给项目添加 Spring Data JPA，并使用 MySQL 数据库。因此在 POM 文件中加入如下配置：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>在 application.properties 文件中加入数据库连接信息：spring.datasource.url=jdbc:mysql://localhost:3306/yourDB?useUnicode=true&characterEncoding=UTF-8 spring.datasource.username=dbuser spring.datasource.password=****** spring.datasource.driver-class-name=com.mysql.jdbc.Driver这里，为了简单方便演示，我们只创建一张表，字段如下：@Entity public class User implements java.io.Serializable{ @Id @Column private Long id; @Column private String login; @Column private String password; @Column private String role; // 省略 get set 等 }然后我们添加 2 条数据，如下：idloginpasswordrole1user1$2a$10$PhynBxXaIYdBzd/OgXrKzeVr3Bj4fiDxdii14fMOVIwJTqoDoFL1eUSER2admin$2a$10$PhynBxXaIYdBzd/OgXrKzeVr3Bj4fiDxdii14fMOVIwJTqoDoFL1eADMIN密码这里都是使用了 BCryptPasswordEncoder，需在 SecurityConfiguration 中加入配置，后面会贴。自定义 UserDetailsService前面我们提到过，UserDetailsService。Spring Security 在认证过程中需要查找用户，会调用 UserDetailsService 的 loadUserByUsername 方法得到一个 UserDetails，下面我们来实现它。代码如下：@Component("userDetailsService".html) public class CustomUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepository; @Override public UserDetails loadUserByUsername(String login.html) throws UsernameNotFoundException { // 1. 查询用户 User userFromDatabase = userRepository.findOneByLogin(login.html); if (userFromDatabase == null.html) { //log.warn("User: {} not found", login.html); throw new UsernameNotFoundException("User " + login + " was not found in db".html); //这里找不到必须抛异常 } // 2. 设置角色 Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>(); GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole(.html)); grantedAuthorities.add(grantedAuthority.html); return new org.springframework.security.core.userdetails.User(login, userFromDatabase.getPassword(), grantedAuthorities); } }这个方法做了 2 件事情：查询用户以及设置角色。通常一个用户会有多个角色，即上面的 userFromDatabase.getRole() 通常是一个 List，所以设置角色的时候，就是 for 循环 new 多个 SimpleGrantedAuthority 并设置。（本例为了简单没有设置角色表以及用户角色关联表，只在用户中增加了一个角色字段，所以 grantedAuthorities 只有一个）同时修改之前的 SecurityConfiguration，加入 CustomUserDetailsService Bean 配置，如下：@Override protected void configure(AuthenticationManagerBuilder auth.html) throws Exception { auth.userDetailsService(userDetailsService.html)// 设置自定义的 userDetailsService .passwordEncoder(passwordEncoder(.html)); /*auth .inMemoryAuthentication() .withUser("admin1".html) .password("admin1".html) .roles("ADMIN", "USER".html) .and() .withUser("user1".html).password("user1".html) .roles("USER".html);*/ } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }验证效果上面我们自定义了 userDetailsService。此时，Spring Security 在其作用流程中会调用。不出意外的话，重启系统，我们使用 user1 登录可以看到 /product/info，但是不能看 /admin/home。下面我们来重启项目验证一下。先输入 user1，以及错误密码，结果如下：再输入 user1，以及正确密码，结果如下：再将浏览器链接修改为 /admin/home，结果显示：There was an unexpected error (type=Forbidden, status=403.html). Access is denied这与我们的预期完全一致。至此，我们已经在项目中加入了 Spring Security，并且能够通过查询数据库用户、角色信息交给 Spring Security 完成认证授权。Spring Security Session 无状态还记得我们开篇所举的例子吗？我们使用管理员账号密码登录之后，就可以访问 /admin/home 了。此时修改浏览器地址栏为 /product/info 之后刷新页面，仍然可以访问，说明认证状态被保持了；如果关闭浏览器重新输入 /admin/home 就会提示我们重新登录，这有点 Session 的感觉。如果此时，我们将浏览器 Cookie 禁用掉，你会发现登录之后自动跳转只会得到 403。403 是拒绝访问的意思，是没有权限的意思，说明这种情况下授权状态和 Session 是挂钩的。即这时 Spring Security 使用了 Session。但是不是所有的系统都需要 Session，我们能让 Spring Security 不使用 Session 吗？答案是可以！使用 Spring Security 我们可以准确控制 Session 何时创建以及 Spring Security 如何与之交互：always – a session will always be created if one doesn't already exist，没有 Session 就创建。ifRequired – a session will be created only if required (default.html)，如果需要就创建（默认）。never – the framework will never create a session itself but it will use one if it already exists.stateless – no session will be created or used by Spring Security，不创建不使用 Session。这里，我们要关注的是 stateless，通常称为无状态的。为啥要关注这个 Stateless 无状态的情况的呢？因为目前，我们的应用基本都是前后端分离的应用。比方说，你的一套 Java API 是给 React 前端、安卓端、IOS 端调用的。这个时候你还提什么 Session 啊，这时候我们需要的是无状态，通常以一种 Token 的方式来交互。Spring Security 配置 Stateless 的方式如下，依然是修改我们之前定义的 SecurityConfiguration：http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS)前后端分离应用中自定义 Token 整合 Spring Security上面我们提到了 Stateless。实际中我们的前后端分离项目都是无状态的，并没有登录状态保持，服务器通过客户端调用传递的 Token 来识别调用者是谁。通常我们的系统流程是这样的：客户端（React 前端，IOS，安卓）调用"登录接口"获得一个包含 Token 的响应（通常是个 JSON，如 {"token":"abcd","expires":1234567890}）。客户端获取数据，并携带 Token 参数。服务端根据 Token 发现 Token 过期/错误，返回"请登录"状态码。服务器发现 Token 正常，并解析出来是 A，返回 A 的数据。……如果我们想在 Spring Security 项目中使用自定义的 Token，那么我们需要思考下面的问题：怎么发 Token（即怎么登录？）发 Token 怎么和 Spring Security 整合。Spring Security 怎么根据 Token 得到授权认证信息。下面从登录发 Token 开始，这里需要使用到 UsernamePasswordAuthenticationToken，以及 SecurityContextHolder，代码如下：@RequestMapping(value = "/authenticate", method = RequestMethod.POST) public Token authorize(@RequestParam String username, @RequestParam String password.html) { // 1 创建 UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password.html); // 2 认证 Authentication authentication = this.authenticationManager.authenticate(token.html); // 3 保存认证信息 SecurityContextHolder.getContext().setAuthentication(authentication.html); // 4 加载 UserDetails UserDetails details = this.userDetailsService.loadUserByUsername(username.html); // 5 生成自定义 token return tokenProvider.createToken(details.html); } @Inject private AuthenticationManager authenticationManager;上面代码中 1、2、3、4 步骤都是和 Spring Security 交互的。只有第 5 步是我们自己定义的。这里 tokenProvider 就是我们系统中 Token 的生成方式（这个完全是个性化的，通常是个加密串，通常可能会包含用户信息、过期时间等）。其中的 Token 也是我们自定义的返回对象，其中包含 Token 信息类似 {"token":"abcd","expires":1234567890}。我们的 tokenProvider 通常至少具有两个方法，即：生成 Token，验证 Token。大致如下：public class TokenProvider { private final String secretKey; private final int tokenValidity; public TokenProvider(String secretKey, int tokenValidity.html) { this.secretKey = secretKey; this.tokenValidity = tokenValidity; } // 生成 token public Token createToken(UserDetails userDetails.html) { long expires = System.currentTimeMillis() + 1000L * tokenValidity; String token = computeSignature(userDetails, expires.html); return new Token(token, expires.html); } // 验证 token public boolean validateToken(String authToken, UserDetails userDetails.html) { // check token return true or false; } // 从 token 中识别用户 public String getUserNameFromToken(String authToken.html) { // …… return login; } public String computeSignature(UserDetails userDetails, long expires.html) { // 一些特有的信息组装，并结合某种加密或摘要算法 return 例如 something+"|"+something2+MD5(s.html); } }至此，我们客户端可以通过调用 http://host/context/authenticate 来获得一个 Token 了，类似这样的：{"token":"abcd","expires":1234567890}。那么下次请求的时候，我们带上 token=abcd 这个参数（或者也可以是自定义的请求头中），如何在 Spring Security 中复原"Session"呢？我们需要一个 Filter：public class MyTokenFilter extends GenericFilterBean { private final Logger log = LoggerFactory.getLogger(XAuthTokenFilter.class); private final static String XAUTH_TOKEN_HEADER_NAME = "my-auth-token"; private UserDetailsService detailsService; private TokenProvider tokenProvider; public XAuthTokenFilter(UserDetailsService detailsService, TokenProvider tokenProvider.html) { this.detailsService = detailsService; this.tokenProvider = tokenProvider; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain.html) throws IOException, ServletException { try { HttpServletRequest httpServletRequest = (HttpServletRequest.html) servletRequest; String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME.html); if (StringUtils.hasText(authToken.html)) { // 从自定义 tokenProvider 中解析用户 String username = this.tokenProvider.getUserNameFromToken(authToken.html); // 这里仍然是调用我们自定义的 UserDetailsService，查库，检查用户名是否存在， // 如果是伪造的 token，可能 DB 中就找不到 username 这个人了，抛出异常，认证失败 UserDetails details = this.detailsService.loadUserByUsername(username.html); if (this.tokenProvider.validateToken(authToken, details.html)) { log.debug(" validateToken ok...".html); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details, details.getPassword(.html), details.getAuthorities()); // 这里还是上面见过的，存放认证信息，如果没有走这一步，下面的 doFilter 就会提示登录了 SecurityContextHolder.getContext().setAuthentication(token.html); } } // 调用后续的 Filter，如果上面的代码逻辑未能复原"Session"，SecurityContext 中没有想过信息，后面的流程会检测出"需要登录" filterChain.doFilter(servletRequest, servletResponse.html); } catch (Exception ex.html) { throw new RuntimeException(ex.html); } } }目前为止，我们实现了自定义的 Token 生成类，以及通过一个 Filter 来拦截客户端请求，解析其中的 Token，复原无状态下的"Session"，让当前请求处理线程中具有认证授权数据，后面的业务逻辑才能执行。下面，我们需要将自定义的内容整合到 Spring Security 中。首先编写一个类，继承 SecurityConfigurerAdapter：public class MyAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private TokenProvider tokenProvider; // 我们之前自定义的 token 功能类 private UserDetailsService detailsService;// 也是我实现的 UserDetailsService public MyAuthTokenConfigurer(UserDetailsService detailsService, TokenProvider tokenProvider.html) { this.detailsService = detailsService; this.tokenProvider = tokenProvider; } @Override public void configure(HttpSecurity http.html) throws Exception { MyAuthTokenFilter customFilter = new MyAuthTokenFilter(detailsService, tokenProvider.html); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }在 SecurityConfiguration 配置类中加入如下内容：// 增加方法 private MyAuthTokenConfigurer securityConfigurerAdapter() { return new MyAuthTokenConfigurer(userDetailsService, tokenProvider.html); } // 依赖注入 @Inject private UserDetailsService userDetailsService; @Inject private TokenProvider tokenProvider; // 方法修改，增加 securityConfigurerAdapter @Override protected void configure(HttpSecurity http.html) throws Exception { http .authorizeRequests() .anyRequest().authenticated() // .... 其他配置 .and() .apply(securityConfigurerAdapter(.html));// 这里增加 securityConfigurerAdapter }至此我们就完成了无状态应用中 Token 认证结合 Spring Security。总结本篇内容，我们通过一个小例子开始介绍了如何给 Web 应用引入 Spring Security 保护；在展示了 HTTP Basic 验证之后，我们使用了内存用户实验了“角色 - 资源”访问控制；然后我们介绍了 Spring Security 的一些核心概念；之后我们介绍了 Spring Security 是通过 Filter 的形式在 Web 应用中发生作用的，并列举了 Filter 列表，介绍了入口 Filter，介绍了 Spring Boot 是如何载入 Spring Security 入口 Filter 的。最后我们通过两个实战中的例子展示了 Spring Security 的使用。Spring Security 功能也非常强大，但是还是挺复杂的，本篇内容如有差错还请指出。示例代码：https://github.com/xudeming/spring-security-demo参考文档：Spring Security 官方文档Spring-Security-Session版本说明：本文内容主要基于 Spring Boot 1.x 编写，部分补充内容适用于 Spring Boot 2.1.8.RELEASE。由于 Spring Security 在不同版本间存在配置差异（如密码编码器、默认登录页、配置属性等），在实际使用时请参考对应版本的官方文档进行适配。