idea查询类_Spring Security入门(三): 基于自定义数据库查询的认证实战

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自定义用户类必须实现UserDetailsUserDetails的源码如下:

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 回车即可进入登录页面a898a31b3f97de0e296d5f3fe07280b1.png

右键->检查  在下方在弹出的元素审查窗口中选中Elements标签查看表单的html源码,我们可以看到登录表单中实际还包含了一个隐藏了_csrf输入框,其值为622251f2-f7f3-4b78-88a0-52451771deaf,是一个UUID字符串,它的用处是为了保护web请求,防止跨站请求伪造(简称CSRF)

63f55614a03745b21f66cb39cc233755.png

输入数据库中存在的用户账号x_zhangsan己密码zhangsan123,点击Sign in即可登录成功,登录成功后浏览器中会返回如下信息:

"msg":

登录成功后的返回信息中包含了用户的基本属性和角色及权限信息。

使用postman登录需要带上_csrftoken值:

//localhost:8088/apiBoot/login?username=x_zhangsan&password=zhangsan123&_csrf=66dff592-bc63-488f-ab34-929258a55db6

在postman 中响应信息得到了json格式的美化,看起来非常清晰

5  存储用户认证信息类的源码解读

5.1 认识SecurityContextHolderSecurityContext

用户登录成功后的认证信息最终能会作为一个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前端的记技术文干货文章。

658a10b0313eaeee8d2e5bace63bed86.png

读者对本文有任何疑问可在下面的留言板中留言,我看到后会及时回复                      本文留言讨论区  

                      ---END---

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值