一切福田,不離方寸,從心而覓,感無不通。

Spring Boot:整合Spring Security

综合概述

Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。除了常规的认证(Authentication)和授权(Authorization)之外,Spring Security还提供了诸如ACLs,LDAP,JAAS,CAS等高级特性以满足复杂场景下的安全需求。另外,就目前而言,Spring Security和Shiro也是当前广大应用使用比较广泛的两个安全框架。

Spring Security 应用级别的安全主要包含两个主要部分,即登录认证(Authentication)和访问授权(Authorization),首先用户登录的时候传入登录信息,登录验证器完成登录认证并将登录认证好的信息存储到请求上下文,然后再进行其他操作,如在进行接口访问、方法调用时,权限认证器从上下文中获取登录认证信息,然后根据认证信息获取权限信息,通过权限信息和特定的授权策略决定是否授权。

本教程将首先给出一个完整的案例实现,然后再分别对登录认证和访问授权的执行流程进行剖析,希望大家可以通过实现案例和流程分析,充分理解Spring Security的登录认证和访问授权的执行原理,并且能够在理解原理的基础上熟练自主的使用Spring Security实现相关的需求。

实现案例

接下来,我们就通过一个具体的案例,来讲解如何进行Spring Security的整合,然后借助Spring Security实现登录认证和访问控制。

生成项目模板

为方便我们初始化项目,Spring Boot给我们提供一个项目模板生成网站。

1.  打开浏览器,访问:https://start.spring.io/

2.  根据页面提示,选择构建工具,开发语言,项目信息等。

3.  点击 Generate the project,生成项目模板,生成之后会将压缩包下载到本地。

4.  使用IDE导入项目,我这里使用Eclipse,通过导入Maven项目的方式导入。

添加相关依赖

清理掉不需要的测试类及测试依赖,添加 Maven 相关依赖,这里需要添加上web、swagger、spring security、jwt和fastjson的依赖,Swagge和fastjson的添加是为了方便接口测试。

pom.xml

添加相关配置

1.添加swagger 配置

添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类,除了常规配置外,加了一个令牌属性,可以在接口调用的时候传递令牌。

SwaggerConfig.java

加了令牌属性后的 Swagger 接口调用界面,会多出一个令牌参数,在发起请求的时候一起发送令牌。

2.添加跨域 配置

添加一个CORS跨域配置类,在工程下新建 config 包并添加一个 CorsConfig配置类。

CorsConfig.java

安全配置类

下面这个配置类是Spring Security的关键配置。

在这个配置类中,我们主要做了以下几个配置:

1. 访问路径URL的授权策略,如登录、Swagger访问免登录认证等

2. 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证

3. 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService

4. 指定了访问控制过滤器 JwtAuthenticationFilter,在授权时解析令牌和设置登录状态

5. 指定了退出登录处理器,因为是前后端分离,防止内置的登录处理器在后台进行跳转

WebSecurityConfig.java

登录认证触发过滤器

JwtLoginFilter 是在通过访问 /login 的POST请求是被首先被触发的过滤器,默认实现是 UsernamePasswordAuthenticationFilter,它继承了 AbstractAuthenticationProcessingFilter,抽象父类的 doFilter 定义了登录认证的大致操作流程,这里我们的 JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilter,并进行了两个主要内容的定制。

1. 覆写认证方法,修改用户名、密码的获取方式,具体原因看代码注释

2. 覆写认证成功后的操作,移除后台跳转,添加生成令牌并返回给客户端

JwtLoginFilter.java

登录控制器

除了使用上面的登录认证过滤器拦截 /login Post请求之外,我们也可以不使用上面的过滤器,通过自定义登录接口实现,只要在登录接口手动触发登录流程并生产令牌即可。

其实 Spring Security 的登录认证过程只需调用 AuthenticationManager 的 authenticate(Authentication authentication) 方法,最终返回认证成功的 Authentication 实现类并存储到SpringContexHolder 上下文即可,这样后面授权的时候就可以从 SpringContexHolder 中获取登录认证信息,并根据其中的用户信息和权限信息决定是否进行授权。

LoginController.java

注意:如果使用此登录控制器触发登录认证,需要禁用登录认证过滤器,即将 WebSecurityConfig 中的以下配置项注释即可,否则访问登录接口会被过滤拦截,执行不会再进入此登录接口,大家根据使用习惯二选一即可。

如下是登录认证的逻辑, 可以看到部分逻辑跟上面的登录认证过滤器差不多。

1. 执行登录认证过程,通过调用 AuthenticationManager 的 authenticate(token) 方法实现

2. 将认证成功的认证信息存储到上下文,供后续访问授权的时候获取使用

3. 通过JWT生成令牌并返回给客户端,后续访问和操作都需要携带此令牌

有关登录过程的逻辑,参见SecurityUtils的login方法。

SecurityUtils.java

令牌生成器

我们令牌是使用JWT生成的,令牌生成的逻辑,参见源码JwtTokenUtils的generateToken相关方法。

JwtTokenUtils.java

登录身份认证组件

上面说到登录认证是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的,而 AuthenticationManager 又是通过调用 AuthenticationProvider 的 authenticate(Authentication authentication) 来完成认证的,所以通过定制 AuthenticationProvider 也可以完成各种自定义的需求,我们这里只是简单的继承 DaoAuthenticationProvider 展示如何自定义,具体的大家可以根据各自的需求按需定制。

JwtAuthenticationProvider.java

认证信息获取服务

通过跟踪代码运行,我们发现像默认使用的 DaoAuthenticationProvider,在认证的使用都是通过一个叫 UserDetailsService 的来获取用户认证所需信息的。

AbstractUserDetailsAuthenticationProvider 定义了在 authenticate 方法中通过 retrieveUser 方法获取用户信息,子类 DaoAuthenticationProvider 通过 UserDetailsService 来进行获取,一般情况,这个UserDetailsService需要我们自定义,实现从用户服务获取用户和权限信息封装到 UserDetails 的实现类。

AbstractUserDetailsAuthenticationProvider.java

DaoAuthenticationProvider.java

我们自定义的 UserDetailsService,从我们的用户服务 UserService 中获取用户和权限信息。

UserDetailsServiceImpl.java

一般而言,定制 UserDetailsService 就可以满足大部分需求了,在 UserDetailsService 满足不了我们的需求的时候考虑定制 AuthenticationProvider。

如果直接定制UserDetailsService ,而不自定义 AuthenticationProvider,可以直接在配置文件 WebSecurityConfig 中这样配置。

用户认证信息

上面 UserDetailsService 加载好用户认证信息后会封装认证信息到一个 UserDetails 的实现类。

默认实现是 User 类,我们这里没有特殊需要,简单继承即可,复杂需求可以在此基础上进行拓展。

JwtUserDetails.java

用户操作代码

简单的用户模型,包含用户名密码。

User.java

用户服务接口,只提供简单的用户查询和权限查询接口用于模拟。

UserService.java

用户服务实现,只简单获取返回模拟数据,实际场景根据情况从DAO获取即可。

SysUserServiceImpl.java

用户控制器,提供三个测试接口,其中权限列表中未包含删除接口定义的权限(’sys:user:delete’),登录之后也将无权限调用。

UserController.java

登录认证检查过滤器

访问接口的时候,登录认证检查过滤器 JwtAuthenticationFilter 会拦截请求校验令牌和登录状态,并根据情况设置登录状态。

JwtAuthenticationFilter.java

具体详细获取token和检查登录状态代码请查看SecurityUtils的checkAuthentication方法。

编译测试运行

1.  右键项目 -> Run as -> Maven install,开始执行Maven构建,第一次会下载Maven依赖,可能需要点时间,如果出现如下信息,就说明项目编译打包成功了。

2.  右键文件 DemoApplication.java -> Run as -> Java Application,开始启动应用,当出现如下信息的时候,就说明应用启动成功了,默认启动端口是8080。

3.  打开浏览器,访问:http://localhost:8080/swagger-ui.html,进入swagger接口文档界面。

4.我们先再未登录没有令牌的时候直接访问接口,发现都返回无权限,禁止访问的结果。

发现接口调用失败,返回状态码为403的错误,表示因为权限的问题拒绝访问。

打开 LoginController,输入我们用户名和密码(username:amdin, password:123,密码是我们在SysUserServiceImpl中设置的)

登录成功之后,会成功返回令牌,如下图所示。

拷贝返回的令牌,粘贴到令牌参数输入框,再次访问 /user/edit 接口。

这个时候,成功的返回了结果: the edit service is called success.

同样的,拷贝返回的令牌,粘贴到令牌参数输入框,访问 /user/delete 接口。

发现还是返回拒绝访问的结果,那是因为访问这个接口需要 'sys:user:delete' 权限,而我们之前返回的权限列表中并没有包含,所以授权访问失败。

我们可以修改一下 SysUserServiceImpl,添加上‘sys:user:delete’ 权限,重新登录,再次访问一遍。

发现删除接口也可以访问了,记住务必要重新调用登录接口,获取令牌后拷贝到删除接口,再次访问删除接口。

到此,一个简单但相对完整的Spring Security案例就实现了,我们通过Spring Security实现了简单的登录认证和访问控制,读者可以在此基础上拓展出更为丰富的功能。

流程剖析

Spring Security的安全主要包含两部分内容,即登录认证和访问授权,接下来,我们别对这两个部分的流程进行追踪和分析,分析过程中,读者最好同时对比查看相应源码,以更好的学习和了解相关的内容。

登录认证

登录认证过滤器

如果在继承 WebSecurityConfigurerAdapter 的配置类中的 configure(HttpSecurity http) 方法中有配置 HttpSecurity 的 formLogin,则会返回一个 FormLoginConfigurer 对象。如下是一个 Spring Security 的配置样例, formLogin().x.x 就是配置使用内置的登录验证过滤器,默认实现为 UsernamePasswordAuthenticationFilter。

WebSecurityConfig.java

查看 HttpSecurity的formLogion 方法,发现返回的是一个 FormLoginConfigurer 对象。

HttpSecurity.java

而 FormLoginConfigurer 的构造函数内绑定了一个 UsernamePasswordAuthenticationFilter 过滤器。

FormLoginConfigurer.java

接着查看 UsernamePasswordAuthenticationFilter 过滤器,发现其构造函数内绑定了 POST 类型的 /login 请求,也就是说,如果配置了 formLogin 的相关信息,那么在使用 POST 类型的 /login URL进行登录的时候就会被这个过滤器拦截,并进行登录验证,登录验证过程我们下面继续分析。

UsernamePasswordAuthenticationFilter.java

查看 UsernamePasswordAuthenticationFilter,发现它继承了 AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter 中的 doFilter 包含了触发登录认证执行流程的相关逻辑。

AbstractAuthenticationProcessingFilter.java

上面的登录逻辑主要步骤有两个:

1. attemptAuthentication(request, response)

这是 AbstractAuthenticationProcessingFilter  中的一个抽象方法,包含登录主逻辑,由其子类实现具体的登录验证,如 UsernamePasswordAuthenticationFilter 是使用表单方式登录的具体实现。如果是非表单登录的方式,如JNDI等其他方式登录的可以通过继承 AbstractAuthenticationProcessingFilter 自定义登录实现。UsernamePasswordAuthenticationFilter 的登录实现逻辑如下。

UsernamePasswordAuthenticationFilter.java

2. successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)

登录成功之后,将认证后的 Authentication 对象存储到请求线程上下文,这样在授权阶段就可以获取到 Authentication 认证信息,并利用 Authentication 内的权限信息进行访问控制判断。

AbstractAuthenticationProcessingFilter.java

从上面的登录逻辑我们可以看到,Spring Security的登录认证过程是委托给 AuthenticationManager 完成的,它先是解析出用户名和密码,然后把用户名和密码封装到一个UsernamePasswordAuthenticationToken 中,传递给 AuthenticationManager,交由 AuthenticationManager 完成实际的登录认证过程。

AuthenticationManager.java

AuthenticationManager 提供了一个默认的 实现 ProviderManager,而 ProviderManager 又将验证委托给了 AuthenticationProvider。

ProviderManager.java

根据验证方式的多样化,AuthenticationProvider 衍生出多种类型的实现,AbstractUserDetailsAuthenticationProvider 是 AuthenticationProvider 的抽象实现,定义了较为统一的验证逻辑,各种验证方式可以选择直接继承 AbstractUserDetailsAuthenticationProvider 完成登录认证,如 DaoAuthenticationProvider 就是继承了此抽象类,完成了从DAO方式获取验证需要的用户信息的。

AbstractUserDetailsAuthenticationProvider.java

如上面所述, AuthenticationProvider 通过 retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) 获取验证信息,对于我们一般所用的 DaoAuthenticationProvider 是由 UserDetailsService 专门负责获取验证信息的。

DaoAuthenticationProvider.java

UserDetailsService 接口只有一个方法,loadUserByUsername(String username),一般需要我们实现此接口方法,根据用户名加载登录认证和访问授权所需要的信息,并返回一个 UserDetails的实现类,后面登录认证和访问授权都需要用到此中的信息。

UserDetails 提供了一个默认实现 User,主要包含用户名(username)、密码(password)、权限(authorities)和一些账号或密码状态的标识。

如果默认实现满足不了你的需求,可以根据需求定制自己的 UserDetails,然后在 UserDetailsService 的 loadUserByUsername 中返回即可。

退出登录

Spring Security 提供了一个默认的登出过滤器 LogoutFilter,默认拦截路径是 /logout,当访问 /logout 路径的时候,LogoutFilter 会进行退出处理。

LogoutFilter.java

如下是 SecurityContextLogoutHandler 中的登出处理实现。

SecurityContextLogoutHandler.java

访问授权

访问授权主要分为两种:通过URL方式的接口访问控制和方法调用的权限控制。

接口访问权限

在通过比如浏览器使用URL访问后台接口时,是否允许访问此URL,就是接口访问权限。

在进行接口访问时,会由 FilterSecurityInterceptor 进行拦截并进行授权。

FilterSecurityInterceptor 继承了 AbstractSecurityInterceptor 并实现了 javax.servlet.Filter 接口, 所以在URL访问的时候都会被过滤器拦截,doFilter 实现如下。

FilterSecurityInterceptor.java

doFilter 方法又调用了自身的 invoke 方法, invoke 方法又调用了父类 AbstractSecurityInterceptor 的 beforeInvocation 方法。

FilterSecurityInterceptor.java

方法调用权限

在进行后台方法调用时,是否允许该方法调用,就是方法调用权限。比如在方法上添加了此类注解 @PreAuthorize("hasRole('ROLE_ADMIN')") ,Security 方法注解的支持需要在任何配置类中(如 WebSecurityConfigurerAdapter )添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启,才能够使用。

在进行方法调用时,会由 MethodSecurityInterceptor 进行拦截并进行授权。

MethodSecurityInterceptor 继承了 AbstractSecurityInterceptor 并实现了AOP 的 org.aopalliance.intercept.MethodInterceptor 接口, 所以可以在方法调用时进行拦截。

MethodSecurityInterceptor .java

我们看到,MethodSecurityInterceptor 跟 FilterSecurityInterceptor 一样, 都是通过调用父类 AbstractSecurityInterceptor 的相关方法完成授权,其中 beforeInvocation 是完成权限认证的关键。

AbstractSecurityInterceptor.java

上面代码显示 AbstractSecurityInterceptor 又是委托授权认证器 AccessDecisionManager 完成授权认证,默认实现是 AffirmativeBased, decide 方法实现如下。

AffirmativeBased.java

而 AccessDecisionManager 决定授权又是通过一个授权策略集合(AccessDecisionVoter )决定的,授权决定的原则是:

1. 遍历所有授权策略, 如果有其中一个返回 ACCESS_GRANTED,则同意授权。

2. 否则,等待遍历结束,统计 ACCESS_DENIED 个数,只要拒绝数大于1,则不同意授权。

对于接口访问授权,也就是 FilterSecurityInterceptor 管理的URL授权,默认对应的授权策略只有一个,就是 WebExpressionVoter,它的授权策略主要是根据 WebSecurityConfigurerAdapter 内配置的路径访问策略进行匹配,然后决定是否授权。

WebExpressionVoter.java

对于方法调用授权,在全局方法安全配置类里,可以看到给 MethodSecurityInterceptor 默认配置的有 RoleVoter、AuthenticatedVoter、Jsr250Voter、和 PreInvocationAuthorizationAdviceVoter,其中 Jsr250Voter、PreInvocationAuthorizationAdviceVoter 都需要打开指定的开关,才会添加支持。

GlobalMethodSecurityConfiguration.java

RoleVoter 是根据角色进行匹配授权的策略。

RoleVoter.java

AuthenticatedVoter 主要是针对有配置以下几个属性来决定授权的策略。

IS_AUTHENTICATED_REMEMBERED:记住我登录状态

IS_AUTHENTICATED_ANONYMOUSLY:匿名认证状态

IS_AUTHENTICATED_FULLY: 完全登录状态,即非上面两种类型

AuthenticatedVoter.java

PreInvocationAuthorizationAdviceVoter 是针对类似  @PreAuthorize("hasRole('ROLE_ADMIN')")  注解解析并进行授权的策略。

PreInvocationAuthorizationAdviceVoter.java

PreInvocationAuthorizationAdviceVoter 解析出注解属性配置, 然后通过调用 PreInvocationAuthorizationAdvice 的前置通知方法进行授权认证,默认实现类似 ExpressionBasedPreInvocationAdvice,通知内主要进行了内容的过滤和权限表达式的匹配。

ExpressionBasedPreInvocationAdvice.java

到这里,我们对Spring Securiy的登录认证和访问授权两部分的执行流程大致进行了追踪和分析,希望读者可以亲自跟随源码调试这个过程,经过反复对比论证,进一步的加深对Spring Securiy整体流程的理解,从而提高自身在实际项目运用中的分析能力和解决能力。

参考资料

官方网站:https://spring.io/projects/spring-security

W3C资料:https://www.w3cschool.cn/springsecurity/

参考手册:https://springcloud.cc/spring-security-zhcn.html

相关导航

Spring Boot 系列教程目录导航

Spring Boot:快速入门教程

Spring Boot:整合Swagger文档

Spring Boot:整合MyBatis框架

Spring Boot:实现MyBatis分页

源码下载

码云:https://gitee.com/liuge1988/spring-boot-demo.git


作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/p/11106220.html
版权所有,欢迎转载,转载请注明原文作者及出处。