SpringSecurity(三)——自定义数据源(数据库)
前言
上一篇笔记中我们说明了自定义数据源的一些操作,但是依旧是讲用户认证的信息存放于内存中,然而在系统开发中,登录等认证都是通过与数据库中存放的数据源进行比对,所以本篇笔记记录使用数据库作为数据源的配置与实现。
文章目录
数据库设计
事先声明,这个数据库表的设计是通过视频学习的时候获取到的资源,这边直接搬运过来(视频免费,资源付费)。
数据表设计
在设计数据表的时候我们需要考虑这么几点。首先,我们需要有用户认证的信息;其次需要用户权限信息。在一个系统中权限有多种,但大体会归为这么几类,所以按照标准的数据库设计的思想我们应该有一个中间表(关系表)用来关联用户与权限表之间的关系。所以,最终确定关于认证这一方面至少需要三张表:用户信息表、权限表、用户权限关系表。
数据表创建
-- 用户表
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`name_zh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用户角色关系表
CREATE TABLE `user_role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
上面的三个表创建之后不会存在物理关系,如果需要表与表之间的链表查询,可以使用表关联,也可以执行多条SQL语句来查询用户的权限信息。
表数据插入
-- 插入用户数据
BEGIN;
INSERT INTO `user`
VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user`
VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user`
VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
COMMIT;
-- 插入角色数据
BEGIN;
INSERT INTO `role`
VALUES (1, 'ROLE_product', '商品管理员');
INSERT INTO `role`
VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role`
VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;
-- 插入用户角色数据
BEGIN;
INSERT INTO `user_role`
VALUES (1, 1, 1);
INSERT INTO `user_role`
VALUES (2, 1, 2);
INSERT INTO `user_role`
VALUES (3, 2, 2);
INSERT INTO `user_role`
VALUES (4, 3, 3);
COMMIT;
表数据插入完毕后,我们可以准备Java项目了。
Java代码
用户表
// 自定义的用户对象
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
// 账户是否启用
private Boolean enabled;
// 账户是否过期
private Boolean accountNonExpired;
// 账户是否被锁定
private Boolean accountNonLocked;
// 密码是否过期
private Boolean credentialsNonExpired;
// 关系属性,用于存储当前用户的所有角色
private List<Role> roles = new ArrayList<>();
// 获取当前用户的所有角色
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
// 将角色信息遍历后加入到权限信息的集合中
roles.forEach(role->{
SimpleGrantedAuthority simpleGrantedAuthority =
new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
}
自定义用户类的时候,要将成员父类的所有属性
都继承重写,同时记得查看return
语句的返回值是否有问题。
自定义用户类中的角色是由数据库中查到的权限来决定的,所以针对角色信息的属性,我们在其中通过当前用户的权限信息来动态获取角色信息。
权限类
// 在这里我使用了lombok工具,在之前的笔记中有说明过,应该是SpringBoot系列中,具体记不清了。
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
// 角色类
public class Role implements Serializable {
// 权限表主键
private Integer id;
// 权限名(标识)
private String name;
// 权限含义(中文名)
private String nameZh;
}
在权限类中,该类实现了Serializable接口,这个借口在很早的笔记中有过说明。该接口是一个标记接口,接口中并没有执行任何操作(在Java代码中没有执行任何操作,该接口的标识直接对标虚拟机),它的作用是将实现该接口的类中的成员变量都能准确的对应注入属性,如果我们不实现该接口的话很有可能会出现异常,所以建议大家把所有实体类都实现这个接口。
上面的User
类只是实现了UserDetails
接口,因为UserDetails
接口已经实现了Serializable
接口,所以不必重复实现。
其他类文件
其他的类文件其实就是MyBatis的接口和映射文件以及一些SQL语句,这些自己创建,一会仅会把SQL语句放出来。
自定义用户数据源
前一篇笔记中说明过自定义数据源的流程,其实没什么区别,仅仅是将数据通过数据库中查询出来后,将该对象放在了认证器中。
@Configuration
public class MyUserDetailService implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Autowired
public MyUserDetailService(UserMapper userMapper, RoleMapper roleMapper) {
this.userMapper = userMapper;
this.roleMapper = roleMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
// 判断用户是否为空
if (ObjectUtils.isEmpty(user))throw new UsernameNotFoundException("用户名不正确");
// 查询权限信息
List<Role> role = roleMapper.getRoleByUid(user.getId());
user.setRoles(role);
System.out.println(user);
return user;
}
}
该类中通过构造注入的方式将Mybatis
的持久层接口对象注入进来, 然后执行查询数据库的操作即可。在这里,我们通过用户传递给后端的用户名去查询数据库中对应的用户名,如果存在则会进入下一步对照密码,如果没有则会证明不存在。
当用户名存在后,我们会继续执行认证器中的一些操作,这就需要将该对象放入我们自定义的认证器中。
自定义认证器
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
private final MyUserDetailService myuserDetailService;
@Autowired
public WebSecurityConfig(MyUserDetailService myuserDetailService) {
this.myuserDetailService = myuserDetailService;
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 自定义认证管理器时,需要将外部的数据源对象手动注入到对应的自定义的管理其中
builder.userDetailsService(myuserDetailService);
}
// 用来将自定义的AuthenticationManager对象在工厂中进行暴露,使得该对象在项目中的任何地方都可以注入
@Override
// 在工厂中实例一个对象
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
SQL语句
我们查询用户信息的时候可以采用联表查询的方式,但是在本次的设计中,有一个流程是必须存在的:查询用户是否存在。
查询用户是否存在
<select id="loadUserByUsername" resultType="com.lee.entity.User">
select * from `user`
<if test="username!=null and username!=''">
<where>
username=#{username}
</where>
limit 1
</if>
</select>
查询用户权限信息
<select id="getRoleByUid" resultType="com.lee.entity.Role">
select
r.id,
r.name,
r.name_zh
from
role r,user_role `ur`
where
r.id = `ur`.rid
and
ur.uid = #{uid}
</select>
同样,我们可以将查询用户权限信息的SQL语句拆分开执行两次,用业务逻辑代码来执行筛选。但是个人觉得没必要。虽然我一直坚持标准库表设计,建议在业务支持的情况下使用数据字典,但是这个明显字需要查询一个用户,并不是大量数据的查询,所以拆分与否没什么纠结的。
总结
本篇笔记中记录了自定义数据源的操作,下一篇笔记中记录自定义用户登录拦截器。