Shiro 安全框架(四)
前面的认证以及授权是写死的数据,实际开发中必然需要将相关数据存在数据库中保存,从数据库中读取相关数据并进行认证和权限加载。
采用 RBAC 来设计数据库表:
shiro.sql
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`url` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_role_pers
-- ----------------------------
DROP TABLE IF EXISTS `t_role_pers`;
CREATE TABLE `t_role_pers` (
`id` int NOT NULL AUTO_INCREMENT,
`r_id` int NULL DEFAULT NULL,
`p_id` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_role_user
-- ----------------------------
DROP TABLE IF EXISTS `t_role_user`;
CREATE TABLE `t_role_user` (
`id` int NOT NULL AUTO_INCREMENT,
`u_id` int NULL DEFAULT NULL,
`r_id` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
设计 Mapper 和 Service 层接口及实现
Dao 层
dao 接口
dao 层使用的是 mybatis 框架,在此项目中使用一个接口 UserDao:
主要功能是:
- 用户查询根据根据用户名查询用户是否存在。
- 根据用户名结合角色表多表查询将结果封装返回。
- 根据角色 ID 查询对应的权限集合。
public interface UserDao {
// 保存用户,用户注册
void save(User user);
// 用户查询
User getUserByName(String username);
// 根据角色名结合用户角色表查询用户对应的角色
User findRolesByName(String username);
// 根据角色id 查询对应的权限集合
List<Perms> findPermsByRid(int id);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cnda.dao.UserDao">
<!-- useGeneratedKeys="true" keyProperty="id"
在插入数据是实现主键自增?
useGeneratedKeys设置为 true 时,表示如果插入的表id以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。
useGeneratedKeys 默认为 false,只对 insert 语句有效
-->
<insert id="save" parameterType="user" useGeneratedKeys="true" keyProperty="id">
insert into t_user values(#{id},#{username},#{password},#{salt})
</insert>
<!-- 用户查询 -->
<select id="getUserByName" resultType="user">
select * from t_user where username = #{username}
</select>
<!-- 根据用户查询对应的角色信息,将其封装到 User 中,User 中有 List<Role> 属性 -->
<resultMap id="rm1" type="user">
<id column="uid" property="id"/>
<result column="username" property="username"/>
<collection property="roles" javaType="list" ofType="role">
<result column="rid" property="id"/>
<result column="role" property="name"/>
</collection>
</resultMap>
<select id="findRolesByName" parameterType="string" resultMap="rm1">
select u.id uid,username,r.id rid,r.name role
from t_user u
left join t_role_user ru
on u.id = ru.u_id
left join t_role r
on r.id = ru.r_id
where username = #{username}
</select>
<!-- 根据角色 id 查询对应的权限信息,并返回 -->
<select id="findPermsByRid" parameterType="int" resultType="perms">
select p.id, p.name,p.url,r.name
from t_pers p
left join t_role_pers rp
on p.id = rp.p_id
left join t_role r
on rp.r_id = r.id
where r.id = #{id}
</select>
</mapper>
Service 层
UserService
// 注册用户方法
void register(User user);
// 查询用户名
User findUserByName(String username);
// 查询用户所具有的角色和权限
User findRoleAndPermsByName(String username);
UserServiceImpl
@Service("userService")
@Transactional // 实现声明式事务管理
public class UserServiceImpl implements UserService {
@Autowired
private UserDao dao;
// 实现用户注册
@Override
public void register(User user) {
// 将用户的明文密码进行md5加密和加盐处理以及hash散列次数
// 生产随机盐,长度为 10
String salt = SaltUtils.getSalt(10);
Md5Hash hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(hash.toString());
user.setSalt(salt);
System.out.println(user);
dao.save(user);
}
// 查询用户名
@Override
public User findUserByName(String username) {
return dao.getUserByName(username);
}
// 根据用户名返回对应的角色以及权限信息
@Override
public User findRoleAndPermsByName(String username) {
User rolesByName = dao.findRolesByName(username);
rolesByName.getRoles().forEach(role -> {
// 根据角色名查询对应的权限
role.setPerms(dao.findPermsByRid(role.getId()));
});
return rolesByName;
}
}
自定义 Realm 中,调用 UserServiceImpl 服务获取数据库中的数据进行认证和授权
public class CustomerRealm extends AuthorizingRealm {
// 授权 Authorization
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
// 通过定义的 SpringBeanFactoryUtils,获取对应的 Service 对象
UserService userService = (UserService) SpringBeanFactoryUtils.getBean("userService");
// 根据用户名查询对应的角色以及权限信息
User user = userService.findRoleAndPermsByName(primaryPrincipal);
List<Role> roles = user.getRoles(); // 用户的所有角色
System.out.println(user);
if (!CollectionUtils.isEmpty(roles)){ // 不为空
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
roles.forEach(role -> {
info.addRole(role.getName());
// 根据角色赋予资源权限
role.getPerms().forEach(perms -> {
info.addStringPermission(perms.getName());
});
});
return info;
}
return null;
}
// 认证 Authentication
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证触发");
String principal = (String) authenticationToken.getPrincipal();
// 默认是类名首字母小写,也可以指定name
//UserService userService = (UserService) SpringBeanFactoryUtils.getBean("userService");
UserService service = SpringBeanFactoryUtils.getBean(UserService.class);
User user = service.findUserByName(principal);
// 从数据库查询用户进行认证
if (!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),ByteSource.Util.bytes(user.getSalt()),this.getName());
}
return null;
}
}
运行程序查看结果
Root 用户
User(id=1, username=root, password=null, salt=null,
roles=[Role(id=1, name=admin,
perms=[ Perms(id=1, name=admin:*:*, url=null),
Perms(id=2, name=user:*:*, url=null),
Perms(id=3, name=order:*:*, url=null)
])])
Tom 用户
User(id=2, username=tom, password=null, salt=null,
roles=[ Role(id=3, name=tmp,
perms=[Perms(id=3, name=order:*:*, url=null)
]),
Role(id=2, name=user,
perms=[Perms(id=2, name=user:*:*, url=null)
])
])
认证和权限对应正确,在实际开发中,可能还需要加上验证码安全验证,以及更完善的认证、权限匹配规则和 RBAC 数据库设计。
Shiro 的简单使用就到此结束,后面可能还有 Shiro 和 Themleaf 整合,以及验证码的加入。
小结
学习了 Shiro 这一安全框架在 Spring 以及 Spring Boot 中的基本使用,主要的是两个方法:
- 认证:
doGetAuthenticationInfo - 授权:
doGetAuthorizationInfo
两个都是自定义 Realm 实现 AuthorizingRealm 类必须实现的两个方法。
Shiro 的配置类:Spring Boot 集成时,一般来说这种配置类非常的常见,同时也是一个模板代码量变化不大。
一个加密实现:MD5 + salt 加密方式,需要两端实现:
- 登陆时:
- 在 ShiroConfig 创建自定义 Realm 实例时:添加凭证匹配器,进行加密,其中可以定义认证时加密方式,以及 hash 散列次数。
- 自定义 Realm 中的认证方法中:返回
SimpleAuthenticationInfo()需要在此处加盐(数据库读出的盐)处理,实现登陆时认证。
- 注册时:
- 在注册用户时,存入数据库时,需要对用户密码进行 MD5 + salt 处理,可使用 Shiro 自带的
Md5Hash这个类实现,数据库中需要保存用户加密时的盐。
- 在注册用户时,存入数据库时,需要对用户密码进行 MD5 + salt 处理,可使用 Shiro 自带的
JSP 中使用 Shiro 标签,实现权限控制。