保存登陆用户
如果使用 Spring Security 这一类安全管理框架,大部分的开发者可能会将登陆用户数据保存在 Session 中,事实上,Spring Security 也是这么做的,并在此基础上做了一些改进,其中最主要的一个变化就是线程绑定。
当用户登陆成功后,Spring Security 会将登陆成功的用户信息保存到 SecurityContextHolder 中,SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其它线程访问和修改,也就是用户数据和请求线程绑定在一起。当登陆请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContextHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会从 Session 中取出用户登陆数据,保存到 SecurityContextHolder 中,方便在该请求的后续护理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 session 中,然后将 SecurityContextHolder 的数据清空。
这种策略在子线程中想要获取用户登陆数据时比较麻烦,Spring Security 对此也提供来相应的解决方案,如果开发者使用 @Async
注解来开启异步任务的话,那么只需要添加如下配置,使用 Spring Security 提供的异步任务代理,就可以在异步任务中从 SecurityContextHolder 里边获取当前登陆用户的信息:
1 |
|
获取登陆用户
登陆用户获取主要分为两种方式:
- 从 SecurityContextHolder 中获取
- 从当前请求对象中获取
从 SecurityContextHolder 中获取
Authentication
登陆用户信息使用 Authentication 对象表示,Authentication 对象主要有两方面的功能:
- 作为 AuthenticationManager 的输入参数,提供用户认证的凭证,当它作为一个输入参数时,它的 isAuthenticated 方法返回 false,表示用户还未认证。
- 代表已经经过身份认证的用户,此时的 Authentication 可以从 SecurityContext 中获取。
一个 Authentication 对象主要包含三个方面的信息:
- principal:定义认证的用户,如果用户使用用户名/密码的方式登陆,principal 通常就是一个 UserDetails 对象;
- credentials:登陆凭证,一般就是指密码。当用户登陆成功之后,登陆凭证会被自动擦除,以防止泄漏;
- authorities:用户被授予的权限信息。
常见的 Authentication 实现类:
类名 | 作用 |
---|---|
AbstractAuthenticationToken | 实现了 Authentication、CredentialsContainer 两个接口,其中 CredentialsContainer 提供了登陆凭证擦除方法。一般登陆成功后,为了防止用户信息泄漏,可以将登陆凭证擦除。 |
RememberMeAuthenticationToken | 使用 RememberMe 登陆时,存储登陆信息 |
TestingAuthenticationToken | 单元测试时封装的用户对象 |
AnonymousAuthenticationToken | 匿名登录时封装用户对象 |
RunAsUserToken | 替换验证身份时封装的用户对象 |
UsernamePasswordAuthenticationToken | 表单登陆时封装的用户对象 |
JaasAuthenticationToken | JAAS 认证时封装的用户对象 |
PreAuthenticatedAuthenticationToken | Pre-Authentication 场景下封装的用户对象 |
SecurityContextHolder
SecurityContextHolder 中存储的是 SecurityContext,SecurityContext 中存储的是 Authentication。SecurityContext 的存储方式由 SecurityContextHolderStrategy 决定,目前存在三种类型的 SecurityContextHolderStrategy,分别为:
- ThreadLocalSecurityContextHolderStrategy (默认)
- InheritableThreadLocalSecurityContextHolderStrategy
- GlobalSecurityContextHolderStrategy
还有一个 与SecurityContext 相关的重要过滤器 SecurityContextPersistenceFilter
,它的作用是为了存储 SecurityContext 而设计的,主要做两件事情:
- 请求来的时候,从 HttpSession 中获取 SecurityContext 并存例入 SecurityContextHolder 中,这样在同一个请求的后续处理过程中,开发者始终可以通过 SecurityContextHolder 获取当前登陆用户信息。
- 当一个请求处理完毕时,从 SecurityContextHolder 中获取 SecurityContext 并存入 HttpSession 中(主要针对异步 Servlet),方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登陆用户信息。
上述两部操作均有 SecurityContextRepository
接口完成,它的三个实现类为:
- NullSecurityContextRepository(什么都没做)
- TestSecurityContextRepository(为单元测试提供支持)
- HttpSessionSecurityContextRepository(默认)
从当前请求对象中获取
直接在 Controller 的请求参数中放入 Authentication 或 Principal 对象获取,本质上,这些对象都是 HttpServletRequest 带来的(通过 ServletRequestMethodArgumentResolver
),在使用了 Spring Security 框架时,HttpServletRequest 的实现类为 Servlet3SecurityContextHolderAwareRequestWrapper
。
Spring Security 使用 SecurityContextHolderAwareRequestFilter
过滤器将默认的请求对象转化为 Servlet3SecurityContextHolderAwareRequestWrapper。
定义登陆用户的其它方式
Spring Security 支持多种用户定义的方式,其抽象接口为 UserDetailsService
,其不同的实现类表示不同的用户定义方式,将配置好的 UserDetailsService 提供给 AuthenticationManagerBuilder,系统再将 UserDetailsService 提供给 AuthenticationProvider 使用。
基于内存
我们使用配置文件配置用户时,本质上就是基于内存,对应的实现类为 InMemoryUserDetailsManager
。
基于 JdbcUserDetailsManager
JdbcUserDetailsManager 支持将用户数据持久化到数据库,同时它还封装类一系列操作用户的方法,例如用户的添加、更新、查找等。
Spring Security 中为 JdbcUserDetailsManager
提供了数据库脚本(无法自定义用户表、角色表等,这意味着这种方式的局限性很大),位置在 org/springframework/security/core/userdetails/jdbc/users.ddl。
基于 MyBatis
使用 MyBatis 做数据持久化是目前大多数企业应用采用的方案,Spring Security 中结合 MyBatis 可以灵活地定制用户表以及角色表。