0 引言
在笔者的上一篇文章中Spring Security入门(二):基于内存的认证
一文中有提到过Spring Security
实现自定义数据库查询需要你实现UserDetailsService
接口,并实现loadUserByUsername(String username)
抽象方法。我们可以在UserDetailsService
接口的实现类中注入数据库访问对象Dao
,从而实现自定义数据库查询认证用户信息。下面在笔者的boot-demo
实战项目中我们结合spring data jpa
作为持久层技术来一步一步实现自定义数据库认证。
1 表结构设计与实体类
1.1 新建用户表
tbl_user
与对应实体类
笔者使用的数据库为mysql5.6
, 在IDEA中新建一个客户端连接,并在就控制台窗口中执行如下新建tbl_user
表的脚本:
use mysql;
然后执行插入两条数据:
#张三原始密码为zhangsan123
为了维护用户敏感信息的安全,数据库里用户的登录密码或支付密码等安全性要求较高的字段一律采用加密存储的方式存储。
添加用户的sql
脚本中用户的加密密文均在是IDEA中的命令控制台执行spring-boot-cli
命令spring encodepassword ${password}
的方式获得,其实质是使用BCryptPasswordEncoder
编码原始密码所得
tbl_user
表的建表依据为:在spring security
自定义用户类必须实现UserDetails
,UserDetails
的源码如下:
public
于是我们创建一个实现UserDetails
接口的实现类并使之与tbl_user
表中的字段一一对应
user.java
"tbl_user")
1.2 新建角色表
roels
及其对应的实体类
roles
表的建表sql
脚本如下:
use mysql;
执行难添加用户sql
脚本:
insert
新建roles
表对应是实体类Role.java
:
@Entity(name=
1.3 新建用户-角色关系关系表
tbl_user_role
及其对应的实体类
tbl_user_role
表的建表sql
脚本如下:
use mysql;
执行往tbl_user_role
表添加数据sql
脚本:
--插入数据
新建tbl_user_role
表对应的实体类UserRole.java
:
@Entity(name=
2 用于认证用户信息的数据库访问层
2.1 新建与用户表对应的Repository接口
public
TblUserRepository
接口继承JpaRepository
接口,自动拥有了基本的CRUD、分页查询方法及根据字段和关键字查找表对应实体类信息的功能。在TblUserRepository
接口中我们自定义了一个根据username
字段查找用户信息的方法,继承自JpaRepository
接口的数据库访问接口无需开发人员手动实现其中 2.2 新建与角色表对应的Repository
接口
public
在RoleRepository
接口中笔者自定义了根据角色id列表查询角色列表的抽象方法,方便给用户查询角色列表
2.3 新建与用户角色关系表对应的
Repository
接口
public
在 UserRoleRepository
接口中,笔者定义了根据角色id
查询用户角色列表的抽象方法。
3 定义
UserDetailsService
接口的实现类
@Service
以上为了从数据库中查出登录用户的用户名、加密密文及角色列表从数据库中查了3次。
第1次通过
TblUserRepository#findUserByUsername
传入username
参数查出不包含角色信息的User
对象,如果用户不存在则直接返回null;第2次通过
UserRoleRepository#findByUserId
传入用户id
查出用户-角色关系列表第3步通过第2部中得到了
角色id列表
作为入参传入到RoleRepository#findRolesByRoleIdIn
方法得到完整的角色信息列表
由于使用spring-data-jpa
实现关联查询笔者暂时还没有掌握,因而以上认证用户信息访问了三次数据库,确实容易影响效率;在实际的商用生产环境可以参照spring-data-jpa
的连接查询改为连接查询,对于用户登录认证信息等热点数据首次你从数据库查询出来后最好缓存在redis
缓存中,并设置过期时间。另外如果是使用mybatis
作为数据库持久层框架,可以借助resultMap
集合association
属性通过一条sql
将包含角色列表的用户信息一次性查出来
4
WebSecurityConfigurerAdapter
实现类中配置userDetailsService
4.1 配置
userDetailsService
@Configuration
在上文《Spring Security入门(二) 基于内存存储的表单登录实战》的基础上对所有用户进入登录页面和登录接口放开权限,而对/index/*
路径下的接口允许访问角色改为数据库中存在的Admin,SystemAdmin,Developer
等角色。登录成功处理器和失败处理器配置用沿用上文中的逻辑。
4.2 测试用户登录认证效果
在浏览器中输入 http://localhost:8088/apiBoot/login 回车即可进入登录页面
右键->检查 在下方在弹出的元素审查窗口中选中Elements
标签查看表单的html
源码,我们可以看到登录表单中实际还包含了一个隐藏了_csrf
输入框,其值为622251f2-f7f3-4b78-88a0-52451771deaf
,是一个UUID
字符串,它的用处是为了保护web请求,防止跨站请求伪造(简称CSRF
)
输入数据库中存在的用户账号x_zhangsan
己密码zhangsan123
,点击Sign in
即可登录成功,登录成功后浏览器中会返回如下信息:
"msg":
登录成功后的返回信息中包含了用户的基本属性和角色及权限信息。
使用postman登录需要带上_csrf
的token
值:
//localhost:8088/apiBoot/login?username=x_zhangsan&password=zhangsan123&_csrf=66dff592-bc63-488f-ab34-929258a55db6
在postman 中响应信息得到了json
格式的美化,看起来非常清晰
5 存储用户认证信息类的源码解读
5.1 认识
SecurityContextHolder
和SecurityContext
用户登录成功后的认证信息最终能会作为一个Authentication
实现类对象(表单登录通常对应的是一个UsernamePasswordAuthenticationToken
对象)对象被认证过滤器保存在SecurityContextHolder
类的SecurityContext
(安全上下文)中,之后就可以通过SecurityContextHolder
这个类直接去获取当前登录用户的认证信息了,SecurityContextHolder
其实就是一个存放用户具体认证信息的工具类。
通过查看这两个类的相关源码可以对Spring Security
安全框架是如何保存用户的认证信息的原理会有一个更全面的认识,相关源码如下:
SecurityContextHolder.java
public
SecurityContext.java
public
SecurityContext
类是一个接口,它的实现类是SecurityContextImpl
(1)利用SecurityContextHolder
保存用户认证信息的示例源码:
//第1步创建一个空的SecurityContext对象实例
采用
SecurityContextHolder.createEmptyContext()
方法,而不是使用SecurityContextHolder.getContext().setAuthentication(authentication)
获取一个SecurityContext
对象实例的目的是为了避免多线程场景下的跨线程竞争Spring Security
不关心是何种Authentication
的实现类实例被设置到SecurityContext
实例中,测试场景下使用TestingAuthenticationToken
实现类是为了方便,大多数生产场景下一般选用UsernamePasswordAuthenticationToken(userDetails, password, authorities)
构造Authentication
实现类对象实例最后
SecurityContext
实例被保存到SecurityContextHolder
类后,Spring Security
会使用这些信息来进行后面当前认证用户在每一个限权操作的权限鉴定,简称鉴权(authorization
)
(2)利用SecurityContextHolder
获取用户的认证信息和权限的代码实例如下:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection extends GrantedAuthority> authorities =
authentication.getAuthorities();
默认情况下,SecurityContextHolder
使用一个ThreadLocal
对象用于存储用户的详细认证信息,这也就意味着即时当前SecurityContext
对象没有作为一个参数传递到具体的方法里去,同一个线程中的任意方法都能拿到SecurityContext
对象,进而拿到用户的认证信息。如果在当前主体的请求被处理后清除线程程,以这种方式使用ThreadLocal
是非常安全的。Spring Security
的 FilterChainProxy
(过滤链代理)确保了 SecurityContext
永远是干净的。
5.2 认识
SecurityContextHolder
类中的SecurityContextHolderStrategy
由于一些应用与线程特殊的工作方式,并非所有的应用都完全适合使用ThreadLocal
对象来存储安全上下文。例如对于一个Swing
客户端应用就要求虚拟机种所有线程共享一个安全上下文对象,这种情况修啊需要选择全局策略。
SecurityContextHolder
一共由三种方式存储SecurityContext
对象,可以通过在应用启动前调用方法SecurityContextHolder.setStrategyName(String strategyName)
方法进行设置。三种策略模式分别是
SecurityContextHolder.MODE_GLOBAL
: 全局模式,适用于单个应用要求虚拟机中所有线程要求共享一个安全上下文的场景SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
:继承本地线程模式SecurityContextHolder.MODE_THREADLOCAL
: 本地线程模式
SecurityContextHolder.java
public
通过阅读SecurityContextHolder
类中的关键源码,可以看出SecurityContextHolder
类首先通过系统变量名spring.security.strategy
从系统属性中获取strategyName
,并在初始化方法中根据strategyName
去实例化strategy
属性。在初始化方法中,首先判断strategyName
变量是否为空,为空的化就使用MODE_THREADLOCAL
模式,然后根据strategyName
的值去构建不同的SecurityContextHolderStrategy
实现类实例。MODE_THREADLOCAL
模式对应ThreadLocalSecurityContextHolderStrategy
类实例;MODE_INHERITABLETHREADLOCAL
模式对应InheritableThreadLocalSecurityContextHolderStrategy
类实例;MODE_GLOBAL
对应GlobalSecurityContextHolderStrategy
类实例。而SecurityContextHolder
类的三个重要的静态方法getContext、setContext和createEmptyContext
其实都是委托给strategy
来操作的。
通过阅读ThreadLocalSecurityContextHolderStrategy
类的源码,我们也可以看到SecurityContext
确实是保存在了一个ThreadLocal
对象中的泛型变量中。
final
大多数应用场景下,我们无需改变默认的strategyName
的值,默认使用ThreadLocal
存储当前登录用户的认证信息即可。
本文代码晚点我会提交到gitee 个人仓库,地址:https://gitee.com/heshengfu1211/boot-demo.git
感兴趣的小伙伴可以克隆下来参考完整的代码
6 参考文章
[1] https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#servlet-authentication
7 推荐阅读
[1] Spring Security 入门(一)Spring Security中的认证与密码编码器
[2] Spring Security入门(二) 基于内存存储的表单登录实战
[3] SpringBoot之路(二)使用用Spring-Data-JPA访问数据库进行基本的CRUD操作
[4] SpringBoot之路(四)Spring-Data-Jpa中的高级应用
初次阅读作者文章的读者欢迎点击文章标题下方的蓝色字体“码农的进阶之路2020”或者扫描下方二维码加个关注,笔者会定期更新java后端与web前端的记技术文干货文章。
读者对本文有任何疑问可在下面的留言板中留言,我看到后会及时回复 本文留言讨论区
---END---