Shiro 安全框架(四)

103 阅读5分钟

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:

主要功能是:

  1. 用户查询根据根据用户名查询用户是否存在。
  2. 根据用户名结合角色表多表查询将结果封装返回。
  3. 根据角色 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)
])])

image-20221201154626375

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)
])
])

image-20221201154650338

认证和权限对应正确,在实际开发中,可能还需要加上验证码安全验证,以及更完善的认证、权限匹配规则和 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 这个类实现,数据库中需要保存用户加密时的盐。

JSP 中使用 Shiro 标签,实现权限控制。