Spring Security 接口认证鉴权入门实践指南
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security 接口认证鉴权入门实践指南相关的知识,希望对你有一定的参考价值。
参考技术AWeb API 接口服务场景里,用户的认证和鉴权是很常见的需求,Spring Security 据说是这个领域里事实上的标准,实践下来整体设计上确实有不少可圈可点之处,也在一定程度上印证了小伙们经常提到的 “太复杂了” 的说法也是很有道理的。
本文以一个简单的 SpringBoot Web 以应用为例,重点介绍以下内容:
创建 SpringBoot 示例,用于演示 Spring Security 在 SpringBoot 环境下的应用,简要介绍四部分内容:pom.xml、application.yml、IndexController 和 HelloController。
boot-example 是用于演示的 SpringBoot 项目子模块(Module)。
SpringBoot 应用名称为 example,实例端口为 9999 。
IndexController 实现一个接口:/。
HelloController 实现两个接口:/hello/world 和 /hello/name。
编译启动 SpringBoot 应用,通过浏览器请求接口,请求路径和响应结果:
SpringBoot 示例准备完成。
SpringBoot 集成 Spring Security 仅需要在 pom.xml 中添加相应的依赖: spring-boot-starter-security ,如下:
编译启动应用,相对于普通的 SpringBoot 应用,我们可以在命令行终端看到 特别 的两行日志:
表示 Spring Security 已在 SpringBoot 应用中生效。 默认 情况下,Spring Security 自动化 地帮助我们完成以下三件事件:
使用 Spring Security 默认为我们生成的用户名和密码进行登录(Sign in),成功之后会自动重定向至 / :
之后我们就可以通过浏览器正常请求 /hello/world 和 /hello/name。
默认情况下,Spring Security 仅支持基于 FormLogin 方式的认证,只能使用固定的用户名和随机生成的密码,且不支持鉴权。如果想要使用更丰富的安全特性:
本文以 Java Configuration 的方式为例进行介绍,需要我们提供一个继承自 WebSecurityConfigurerAdapter 配置类,然后通过重写若干方法进而实现自定义配置。
SecurityConfig 使用 @Configuration 注解(配置类),继承自 WebSecurityConfigurerAdapter ,本文通过重写 configure 方法实现自定义配置。
需要注意: WebSecurityConfigurerAdapter 中有多个名称为 configure 重载方法,这里使用的是参数类型为 HttpSecurity 的方法。
注:Spring Security 默认自动化配置参考 Spring Boot Auto Configuration 。
用以指定哪些请求需要什么样的认证或授权,这里使用 anyRequest() 和 authenticated() 表示所有的请求均需要认证。
表示我们使用 HttpBasic 认证。
编译启动应用,会发现终端仍会输出密码:
因为,我们仅仅改变的是认证方式。
为方便演示,我们使用 CURL 直接请求接口:
会提示我们 Unauthorized ,即:没有认证。
我们按照 HttpBasic 要求添加请求头部参数 Authorization ,它的值:
即:
再次请求接口:
认证成功,接口正常响应。
使用默认用户名和随机密码的方式不够灵活,大部分场景都需要我们支持多个用户,且分别为他们设置相应的密码,这就涉及到两个问题:
对于 读取 ,Spring Security 设计了 UserDetailsService 接口:
实现按照用户名(username)从某个存储介质中加载相对应的用户信息(UserDetails)。
用户名,客户端发送请求时写入的用于用户名。
用户信息,包括用户名、密码、权限等相关信息。
注意:用户信息不只用户名和用户密码。
对于 存储 ,Spring Security 设计了 UserDetailsManager 接口:
创建用户信息
修改用户信息
删除用户信息
修改当前用户的密码
检查用户是否存在
注意: UserDetailsManager 继承自 UserDetailsService 。
也就是说,我们可以通过提供一个已实现接口 UserDetailsManager * 的类,并重写其中的若干方法,基于某种存储介质,定义用户名、密码等信息的存储和读取逻辑;然后将这个类的实例以 Bean 的形式注入 Spring Security,就可以实现用户名和密码的自定义。
实际上,Spring Security 仅关心如何 读取 , 存储 可以由业务系统自行实现;相当于,只实现接口 UserDetailsService 即可。
Spring Security 已经为我们预置了两种常见的存储介质实现:
InMemoryUserDetailsManager和 JdbcUserDetailsManager 均实现接口 UserDetailsManager ,本质就是对于 UserDetails 的 CRUD 。我们先介绍 UserDetails ,然后再分别介绍基于内存和数据库的实现。
UserDetails是用户信息的抽象接口:
获取用户名。
获取密码。
获取权限,可以简单理解为角色名称(字符串),用于实现接口基于角色的授权访问,详情见后文。
获取用户是否可用,或用户/密码是否过期或锁定。
Spring Security 提供了一个 UserDetails 的实现类 User ,用于用户信息的实例表示。另外, User 提供 Builder 模式的对象构建方式。
设置用户名称。
设置密码,Spring Security 不建议使用明文字符串存储密码,密码格式:
其中,id 为加密算法标识,encodedPassword 为密码加密后的字符串。这里以加密算法 bcrypt 为例,详细内容可参考 Password Storage 。
设置角色,支持多个。
UserDetails实例创建完成之后,就可以使用 UserDetailsManager 的具体实现进行存储和读取。
InMemoryUserDetailsManager是 Spring Security 为我们提供的基于内存实现的 UserDetailsManager 。
使用 @Bean 将 InMemoryUserDetailsManager 实例注入 Spring Security。
创建 InMemoryUserDetailsManager 实例之后,并不是必须立即调用 createUser 添加用户信息,也可以在业务系统的其它地方获取已注入的 InMemoryUserDetailsManager 动态存储 UserDetails 实例。
编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:
基于内存介质自定义的用户名和密码已生效,接口正常响应。
JdbcUserDetailsManager是 Spring Security 为我们提供的基于数据库实现的 UserDetailsManager ,相较于 InMemoryUserDetailsManager 使用略复杂,需要我们创建数据表,并准备好数据库连接需要的数据源(DataSource), JdbcUserDetailsManager 实例的创建依赖于数据源。
JdbcUserDetailsManager可以与业务系统共用一个数据库数据源实例,本文不讨论数据源的相关配置。
以 MySQL 为例,创建数据表语句:
其他数据库语句可参考 User Schema 。
JdbcUserDetailsManager实例的创建与注入,除
之外,整体流程与 InMemoryUserDetailsManager 类似,不再赘述。
在业务系统中获取已注入的 JdbcUserDetailsManager 实例,可以动态存储 UserDetails 实例。
编译启动应用,使用我们自己创建的用户名和密码(userA/123456)访问接口:
基于数据库介质自定义的用户名和密码已生效,接口正常响应。
Spring Security 可以提供基于角色的权限控制:
假设,存在两个角色 USER(普通用户) 和 ADMIN(管理员),
角色 USER 可以访问接口 /hello/name,
角色 ADMIN 可以访问接口 /hello/world,
所有用户认证后可以访问接口 /。
我们需要按上述需求重新设置 HttpSecurity :
设置角色 USER 可以访问接口 /hello/name。
设置角色 ADMIN 可以访问接口 /hello/world。
设置其他接口认证后即可访问。
mvcMatchers 支持使用通配符。
创建属于角色 USER 和 ADMIN 的用户:
用户名:userA,密码:123456,角色:USER
用户名:userB,密码:abcdef,角色:ADMIN
对于用户 userA :
使用用户 userA 的用户名和密码访问接口 /:
认证通过,可正常访问。
使用用户 userA 的用户名和密码访问接口 /hello/name:
认证通过,鉴权通过,可正常访问。
使用用户 userA 的用户名和密码访问接口 /hello/world:
认证通过,用户 userA 不属于角色 ADMIN,禁止访问。
使用用户 userA 的用户名和密码访问接口 /:
认证通过,可正常访问。
对于用户 userB :
使用用户 userB 的用户名和密码访问接口 /:
认证通过,可正常访问。
使用用户 userB 的用户名和密码访问接口 /hello/world:
认证通过,鉴权通过,可正常访问。
使用用户 userB 的用户名和密码访问接口 /hello/name:
认证通过,用户 userB 不属于角色 USER,禁止访问。
这里可能会有一点奇怪,一般情况下我们会认为 管理员 应该拥有 普通用户 的全部权限,即普通用户 可以访问接口 /hello/name,那么 管理员 应该也是可以访问接口 /hello/name 的。如何实现呢?
方式一,设置用户 userB 同时拥有角色 USER 和 ADMIN;
这种方式有点不够“优雅”。
方式二,设置角色 ADMIN 包含 USER;
Spring Security 有一个 Hierarchical Roles 的特性,可以支持角色之间的 包含 操作。
使用这个特性要特别注意两个地方:
前文使用的是 HttpSecurity.authorizeHttpRequests 方法,此处需要变更为 HttpSecurity.authorizeRequests 方法。
使用 RoleHierarchy 以 Bean 的方式定义角色之间的 层级关系 ;其中,“ROLE_” 是 Spring Security 要求的固定前缀。
编译启动应用,使用用户 userB 的用户名和密码访问接口 /hello/name:
认证通过,鉴权通过,可正常访问。
如果开启 Spring Security 的 debug 日志级别,访问接口时可以看到如下的日志输出:
可以看出,Spring Security 可以从角色 ADMIN 推导出用户实际拥有 USER 和 ADMIN 两个角色。
Hierarchical Roles 文档中的示例有明显错误:
接口 RoleHierarchy 中并不存在方法 setHierarchy 。前文所述 authorizeRequests 和 RoleHierarchy 结合使用的方法是结合网络搜索和自身实践得出的,仅供参考。
另外, authorizeHttpRequests 和 RoleHierarchy 结合是没有效果的, authorizeRequests 和 authorizeHttpRequests 两者之间的区别可以分别参考 Authorize HttpServletRequests with AuthorizationFilter 和 Authorize HttpServletRequest with FilterSecurityInterceptor 。
鉴权的前提需要认证通过;认证不通过的状态码为401,鉴权不通过的状态码为403,两者是不同的。
Spring Security 异常主要分为两种:认证失败异常和鉴权失败异常,发生异常时会分别使用相应的默认异常处理器进行处理,即:认证失败异常处理器和鉴权失败异常处理器。
使用的认证或鉴权实现机制不同,可能使用的默认异常处理器也不相同。
Spring Security 认证失败异常处理器:
如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:
如果想要自定义返回内容,则可以通过自定义认证失败处理器实现:
authenticationEntryPoint()会创建返回一个自定义的 AuthenticationEntryPoint 实例;其中,使用 HttpServletResponse.getWriter().print() 写入我们想要返回的内容:401。
httpBasic().authenticationEntryPoint(authenticationEntryPoint())使用我们自定义的 AuthenticationEntryPoint 替换 HttpBasic 默认的 BasicAuthenticationEntryPoint 。
编译启动应用,使用不正确的用户名和密码访问接口 /:
认证不通过,使用我们自定义的内容 401 返回。
Spring Security 鉴权失败异常处理器:
如前文所述,认证失败时,Spring Security 使用默认的认证失败处理器实现返回:
如果想要自定义返回内容,则可以通过自定义鉴权失败处理器实现:
自定义鉴权失败处理器与认证失败处理器过程类似,不再赘述。
编译启动应用,使用用户 userA 的用户名和密码访问接口 /hello/world:
鉴权不通过,使用我们自定义的内容 403 返回。
exceptionHandling()也是有一个 authenticationEntryPoint() 方法的;对于 HttpBasic 而言,使用 exceptionHandling().authenticationEntryPoint() 设置自定义认证失败处理器是不生效的,具体原因需要大家自行研究。
前文介绍两种认证方式: FormLogin 和 HttpBasic ,Spring Security 还提供其他若干种认证方式,详情可参考 Authentication Mechanisms 。
如果我们想实现自己的认证方式,也是比较简单的。Spring Security 本质就是 过滤器 ,我们可以实现自己的认证过滤器,然后加入到 Spring Security 中即可。
认证过滤器核心实现流程:
除去抛出异常的情况外, filterChain.doFilter(servletRequest, servletResponse); 是必须保证被执行的。
理解认证过滤器涉及的概念会比较多,详情参考 Servlet Authentication Architecture 。
认证过滤器创建完成之后,就可以加入到 Spring Security 中:
Spring Security 根据我们配置的不同,会为我们自动按照一定的次序组装一条 过滤器链 ,通过这条链上的若干过滤器完成认证鉴权的。我们需要把自定义的认证过滤器加到这个链的 合适位置 ,这是选取的位置是在 ExceptionTranslationFilter 的前面。
过滤器链的顺序可以参考 Security Filters 。
ExceptionTranslationFilter 的作用可以参考 Handling Security Exceptions 。
使用自定义认证过滤器时,自定义认证失败异常处理器和鉴权失败异常处理器的设置方法。
编译启动应用,我们会发现可以在不填入任何认证信息的情况下直接访问接口 / 和 /hello/name,因为模拟用户已认证且角色为 USER;访问接口 /hello/world 时会出现提示 403。
Spring Security 自身包含的内容很多,官方文档也不能很好的讲述清楚每个功能特性的使用方法,很多时候需要我们自己根据文档、示例、源码以及他人的分享,尽可能多的实践,逐步加深理解。
以上是关于Spring Security 接口认证鉴权入门实践指南的主要内容,如果未能解决你的问题,请参考以下文章
Spring Security入门(3-6)Spring Security 的鉴权 - 自定义权限前缀
Spring Security入门(3-5)Spring Security 的鉴权 - 决策管理器和投票器
搭建SpringCloud微服务框架:Spring-Security-OAuth 服务接口鉴权