Spring Security框架确实好用,在简单的集成使用之后,还是决定写一下原理的总结,从源头理解框架的实现。有些内容摘自前人博客。权限认证是几乎每个系统都会用到的技术,特别总结。在项目中的实现参考了mall项目。
SpringSecurity
先贴参考:
核心类
Authentication 是一个接口,用来表示用户认证信息的。在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息和代表当前用户相关信息的 Authentication 的引用。默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext。
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口,它的默认实现是 ProviderManager。通过 Authentication.getPrincipal()
可以获取到代表当前用户的信息,这个对象通常是 UserDetails 的实例。UserDetails 是 Spring Security 中一个核心的接口。其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法。UserDetails 是通过 UserDetailsService 的 loadUserByUsername()
方法进行加载的。
Authentication 的 getAuthorities()
可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails 的。
看完是不是一堆名词云里雾里,没关系,多看几次就理顺了。
核心流程
用户输入用户名和密码登录,SpringSecurity将获取到的用户名和密码封装成一个实现了Authentication接口的UsernamePasswordAuthenticationToken
,将上述产生的token传递给AuthenticationManager
进行登录验证,验证成功后会返回一个封装了用户权限信息的Authentication对象,通过调用SecurityContextHolder.getContext().setAuthentication()
将Authentication对象赋予当前的SecurityContext
.
UserDetailsService
我们可以通过自己实现UserDetails来定义自己获取用户其他信息的方法。也可以实现UserDetailsService来加载自定义的UserDetails信息。
1 | public interface UserDetails extends Serializable { |
BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.
Spring Security-安全管理框架配合源码给出了自定义登录逻辑修改实例。
其他参考:
Spring Security的GrantedAuthority(已授予的权限)
加入JWT验证
在配置类中加入自定义拦截器
1 | // 添加JWT filter |
传统的Session认证方式
http协议本身是一种无状态的协议,也就是每发起一次请求就要进行一次用户密码验证。因此我们在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送应用,应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
总的来说登录验证就一下几步:
- 获取用户信息
- 调用service查询user
- 判断用户是否存在:有(存到Session,并跳转首页) 否(跳转到登录页面)
1 | //吐槽一下老旧的代码,甚至逻辑也不太清晰 |
通常认证的记录被保存在内存中,传统的方式存在服务器开销大,扩展性差等问题。
基于token控制
也是无状态的,但是它不需要在服务端去保留用户的认证信息或者会话信息。流程上是这样的:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *
。
以上分析参考自文章什么是JWT
JWT构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
payload就是存放有效信息的地方,标注中注册的声明,公有的声明和私有的声明。
signature这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
Spring Security权限控制 + JWT Token认证中token验证和权限控制阶段流程也可以参考,收藏一下。
动态权限控制
最后回忆一下mall项目的权限控制。最初采用了@PerAuthorize注解定义好需要的权限,将该权限存入权限表,当用户登录时将所有的权限查询出来。然后Spring Security将用户所有的权限值和接口注解定义的权限值进行比对。这样做无法批量控制接口的权限。
在后来的版本中使用Spring Security实现基于路径的动态权限。
在配置类中加入DynamicSecurityFilter
1 | //有动态权限配置时添加动态权限校验过滤器 |
动态权限调用过程:
在DynamicSecurityFilter
中调用super.beforeInvocation(fi)
方法时会调用AccessDecisionManager
中的decide方法用于鉴权操作,而decide中的configAttributes
参数会通过SecurityMetadataSource
中的getAttributes
方法来获取。
后台资源规则被缓存在了一个Map对象中,当后台资源发生变化时,我们需要清空缓存的数据,然后在下次查询的时候重新加载进来。Mall项目中修改UmsResourceController类,当修改后台资源的时候,需要调用clearDataSource来清空缓存的数据。
获取权限列表过程:
UserDetailsService
====>调用LoadUserByUsername(String username)
====>调用getResourceList(id)
====>adminCacheService.getResourceList(id)
=====>adminRoleRelationDao.getResourceList(id)
;获取到权限列表和UserInfo