一般的,企业的用户数据不可能直接写在文件中,我们需要设计数据库用来存储这些重要的数据,假设现在有权限(permission),角色(role),用户(user)三个重要的主题,那么如何将这三者联系起来呢?我们看下图:
图中告诉我们:
permission和role的关系是多对多的关系
role和user的关系也是多对多的关系
一、数据库表构建
其中permission、role、tb_user、permission_role、role_user
- tb_user用户表
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`userId` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`nikeName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '无名氏',
`name` char(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`email` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`photo` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '',
`sex` char(3) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '0',
`detail` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '此人比较懒,没有任何描述',
`previledge` int(2) NULL DEFAULT 0,
PRIMARY KEY (`userId`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE,
UNIQUE INDEX `username_password_index`(`username`, `password`) USING BTREE,
UNIQUE INDEX `email`(`email`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- role角色表
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(11) NOT NULL,
`role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`sn` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '管理员', 'Manager');
INSERT INTO `role` VALUES (2, '学生', 'Student');
INSERT INTO `role` VALUES (3, '作者', 'Author');
- permission权限表
-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`permission_id` int(11) NOT NULL,
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '资源表达式,资源名:资源操作:资源实例',
PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- role_permission角色权限表
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`role_id` int(40) NULL DEFAULT NULL,
`permission_id` int(40) NULL DEFAULT NULL,
INDEX `role_id`(`role_id`) USING BTREE,
INDEX `permission_id`(`permission_id`) USING BTREE,
CONSTRAINT `role_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `role_permission_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`permission_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- user_role用户角色表
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_id` int(11) NOT NULL,
INDEX `user_id`(`user_id`) USING BTREE,
INDEX `role_id`(`role_id`) USING BTREE,
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`userId`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
二、类的编码
2.1、有了上面几张表后,我们需要建立对应的bean
- UserInfo映射到tb_user表 存放用户信息
/**
* 用户信息
*/
public class UserInfo {
//用户编号(唯一标志符)
private String userId;
//用户名
@Size(min = 3,max = 40,message = "用户名长度请保持在{min}到{max}之间")
private String username;
//账户密码
@NotBlank(message = "密码不得为空!" )
private String password;
//昵称
private String nikeName;
//姓名
private String name;
//邮箱
private String email;
//头像
private String photo;
//个人描述
private String detail;
//性别 0是男,1是女
private String sex;
- Role映射到role表
public class Role {
//角色id
private int roleId;
//角色名称(中文)
private String roleName;
//角色code(英文)
private String sn;
//一个角色role存在多个资源映射关系
private List<RolePermission> rolePermissions;
- Permission映射到permission表
public class Permission {
//权限id
private int permissionId;
//权限名称
private String name;
//shiro的权限资源表达式,比如 user:create,user:delete
private String resource;
- 中间表:映射tb_user拥有的不同的role: user_role
public class UserRole {
//用户uid
private String userId;
//用户对应的角色列表
private List<Role> roles;
- 中间表:映射role拥有的不同的Permission:role_permission
public class RolePermission {
//角色id
private int roleId;
//角色对应的资源列表
private List<Permission> permissionList;
2.3. 使用mybatis编写他们的Mapper文件
- 管理User的数据库操作的Dao接口
public interface ShiroUserDao {
//根据用户输入的用户名查询用户信息
public UserInfo searchUserByUsername(String username);
}
对应的ShiroUserMapper文件
<?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.onlineStudySystem.dao.shiroDao.ShiroUserDao">
<resultMap id="userInfoMap" type="UserInfo">
<id property="userId" column="userId"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="nikeName" property="nikeName"></result>
<result column="name" property="name"></result>
<result column="email" property="email"></result>
<result column="photo" property="photo"></result>
<result column="detail" property="detail"></result>
<result column="sex" property="sex"></result>
</resultMap>
<select id="searchUserByUsername" resultMap="userInfoMap" parameterType="java.lang.String">
select * from tb_user where username = #{username,jdbcType=VARCHAR}
</select>
</mapper>
管理Role获取用户角色以及角色对应的资源的接口
public interface RoleDao {
//查询所有的角色
public List<Role> searchAllRole();
//根据用户传进的角色id查询指定角色
public List<Role> searchByRoleId(int roleId);
//根据角色id查询资源角色映射表
public List<RolePermission> searchPermissionIdByRoleId(int roleId);
//根据用户id查询角色
public List<UserRole> searchRoleByUserId(String userId);
}
对应的ShiroRoleMapper文件
<?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.onlineStudySystem.dao.shiroDao.RoleDao">
<resultMap id="roleMap" type="com.onlineStudySystem.bean.shiro.Role">
<id property="roleId" column="role_id"></id>
<result property="roleName" column="role_name" jdbcType="VARCHAR"></result>
<result property="sn" column="sn" jdbcType="VARCHAR"></result>
<collection fetchType="lazy" property="rolePermissions" column="role_id" select="com.onlineStudySystem.dao.shiroDao.RoleDao.searchPermissionIdByRoleId"></collection>
</resultMap>
<resultMap id="userRoleMap" type="com.onlineStudySystem.bean.shiro.UserRole">
<result column="user_id" property="userId"></result>
<collection property="roles" column="role_id" select="searchByRoleId"></collection>
</resultMap>
<resultMap id="rolePermissionMap" type="com.onlineStudySystem.bean.shiro.RolePermission">
<result property="roleId" column="role_id"></result>
<collection fetchType="lazy" property="permissionList" column="permission_id" select="com.onlineStudySystem.dao.shiroDao.ShiroPermissionDao.searchPermissionById">
</collection>
</resultMap>
<select id="searchAllRole" resultMap="roleMap">
select * from role
</select>
<select id="searchByRoleId" resultMap="roleMap">
select * from role where role_id = #{role_id}
</select>
<select id="searchPermissionIdByRoleId" resultMap="rolePermissionMap">
SELECT * from role_permission where role_id =#{roleId}
</select>
<select id="searchRoleByUserId" resultMap="userRoleMap">
select * from user_role where user_id = #{userId}
</select>
</mapper>
- 用于获取对资源权限(Permission)表达式的接口
public interface ShiroPermissionDao {
//根据PermissionId查询某条Permission
public Permission searchPermissionById(int permissionId);
//填充permission数据
public int insertPermissionInfo(@Param("permissions") List<Permission> permissions);
//搜索现有的所有权限
public List<Permission> searchAllPermission();
}
对应的Mapper文件
<mapper namespace="com.onlineStudySystem.dao.shiroDao.ShiroPermissionDao">
<resultMap id="resMap" type="com.onlineStudySystem.bean.shiro.Permission">
<id property="permissionId" column="permission_id"></id>
<result property="name" column="name"></result>
<result property="resource" column="resource"></result>
</resultMap>
<select id="searchPermissionById" resultMap="resMap">
select * from permission where permission_id = #{permissionId}
</select>
<insert id="insertPermissionInfo" parameterType="com.onlineStudySystem.bean.shiro.Permission">
insert into permission values
<foreach collection="permissions" item="permission" open="(" close=")" separator=",">
#{permission.permissionId},#{permission.name},#{permission.resource}
</foreach>
</insert>
<select id="searchAllPermission" resultMap="resMap">
select * from permission
</select>
</mapper>
2.4 在mybatis-config全局配置文件中开启懒加载模式
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 将积极加载改为消息加载即按需加载 -->
<setting name="aggressiveLazyLoading" value="false" />
<!-- 目的是排除不相干的方法导致立刻加载的情况-->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
</configuration>
这个Mapper文件中,我使用了懒加载模式,如果你懒加载的对象中,有toString方法的话,一定要加上
<setting name="lazyLoadTriggerMethods" value="toString()"/>
2.5 小总结:
上面这几个Mapper文件中的关联关系图如下:
三、加载权限表达式
我们在创建完表和mapper文件之后,要求进行保存权限的表达式以及对应的名称,也就是向Permission表中填充数据。我们就从Controller接口介绍,当我们请求uri重新加载数据,比如/reload
,这个接口将会去扫描所有的controller的requestMapping方法,然后检查是否标注@RequiresPermissions
注解,如果标注了,则取出注解内的值,去数据库查看是否存在这个资源,如果不存在,则自动的插入资源到数据中,注意这里还提到了一个注解@PermissionsName
,它描述资源的中文含义,应该要和@RequiresPermissions注解共同出现,标注一个权限。 /reload接口能够帮我们动态的实现将不存在的资源重新加载到db中看下面的具体代码:
@Controller
public class PermissionContrller {
//请求映射处理器
//springMvc在启动的时候将所有贴有请求映射标签;RequestMapper方法收集起来看封装到该对象中。
@Autowired
RequestMappingHandlerMapping rmhm;
@Autowired
PermissionService permissionService;
@RequestMapping("/reload")
@ResponseBody
public String reload() throws Exception{
//获取所有的controller中请求方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = rmhm.getHandlerMethods();
//请求方法集合
Collection<HandlerMethod> values = handlerMethods.values();
//在开始重新刷新所有的权限集合之前,我们查询数据库并记录数据库中已有的权限
List<Permission> permissions = permissionService.searchAllPermission();
HashSet<String> set = new HashSet<>();
for(Permission p : permissions){
set.add(p.getResource());
}
//存放数据库没有的权限
List<Permission> noExistPermList = new ArrayList<>();
//通过请求方法中,遍历标签
for(HandlerMethod method : values){
if(method!=null){
RequiresPermissions methodAnnotation = method.getMethodAnnotation(RequiresPermissions.class);
//获取权限
String resource = methodAnnotation.value()[0];
//查看权限值是否不为空
if(!StringUtils.isEmpty(resource)){
System.out.println(resource);
//获取资源名字
String name = method.getMethodAnnotation(PermissionsName.class).value();
System.out.println(name);
//检查资源是否存在数据库中
if(!set.contains(resource)) {
//记录到一个数组,加载到数据库
Permission p = new Permission();
p.setName(name);
p.setResource(resource);
noExistPermList.add(p);
}
}
}
}
// 更新数据库
int res = permissionService.insertPermissionInfo(noExistPermList);
return res>0?"更新成功":"更新失败";
}
}
对上面reload接口的代码再进一步讲解:
- RequestMappingHandlerMapping 可以获取所有Controller中的方法。通过这些方法,我们可以拿到他们的元信息,比如方法标注的注解,以及注解的值等。
2.RequiresPermissions
这是一个注解类,它的使用方式如下
@RequestMapping("/shiro/deletecomment")
//必须要求拥有comment:up权限才可以进行提交comment
@RequiresPermissions("comment:delete")
@PermissionsName("删除评论")
@ResponseBody
public String deleteComment(){
return "删除评论成功!";
}
在shiro环境中,用于标注用户必须要具备什么样的权限才可以访问目标方法,在方法被调前进行判断。
- PermissionsName 这也是一个注解类,不过这是我们自定义的,为权限操作起一个中文名称,可有可无。配合@RequirePermissions注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionsName {
String value();
}
整体的逻辑
首先进来先获取所有@Controller下的方法(标注@xxxMapping的) 。依次遍历得到的所有方法,获取标注RequirePermissions注解的方法。然后再获取@RequirePermissions注解中设置的值(values),它返回一个数组,因为我们只设置一个权限值,故此只用取数据的第一个即可。然后获取我们自定义的PermissionsName中的value值。将他们和之前数据库存有的权限依次比较,如果发现不存在于数据库中就将他们进行添加到数据库即可。
四、自定义Realm,进行数据库方式授权和登陆认证
在我们自定义的reaml中,对通过登录认证的不同用户进行权限的查询和分配。
代码:
import com.onlineStudySystem.bean.UserInfo;
import com.onlineStudySystem.bean.shiro.Permission;
import com.onlineStudySystem.bean.shiro.Role;
import com.onlineStudySystem.bean.shiro.RolePermission;
import com.onlineStudySystem.bean.shiro.UserRole;
import com.onlineStudySystem.service.shiro.RoleService;
import com.onlineStudySystem.service.shiro.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 主要根据用户参数去数据库获取对应的角色、资源、账号信息,并封装返回给Authorticator调度判断。
*/
public class UserReaml extends AuthorizingRealm {
@Autowired
RoleService roleService;
@Autowired
UserService userService;
@Override
public String getName() {
return "UserReaml";
}
/**
* 获取权限信息
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username =(String) principals.getPrimaryPrincipal();
//获取到username之后,我们对它进行查看权限
UserInfo userInfo = userService.searchUserByUsername(username);
//获取用户对应角色信息
List<UserRole> userRoles = roleService.searchRoleByUserId(userInfo.getUserId());
List<String> permissionList = new ArrayList<>();
List<String> roles = new ArrayList<>();
//遍历并查看它所拥有的全部角色,注意这里获取的是用户和角色映射表存放的信息
for(UserRole user_role : userRoles){
//抽取出所有的角色列表
List<Role> roleList = user_role.getRole();
for(Role role : roleList){
//获取当前角色对应的权限
List<RolePermission> rolePermissions = role.getRolePermissions();
for(RolePermission p : rolePermissions){
List<Permission> permissionList1 = p.getPermissionList();
for(Permission permission:permissionList1){
permissionList.add(permission.getResource());
}
}
//添加role
roles.add(role.getSn());
}
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissionList);
simpleAuthorizationInfo.addRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 获取登陆信息
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//我们在配置文件中使用了md5加密的方式处理
//获取登录的用户名
String username= (String)token.getPrincipal();
System.out.println("authnc:"+username);
//去数据库中查询它的账号密码
UserInfo userInfo = userService.searchUserByUsername(username);
if(userInfo==null){
//直接返回null给判断器,他会判断抛出用户不存在的异常
System.out.println("authc:"+null);
return null;
}
//我们采用md5加密的方式,首先获取数据库中存放的加密后的密码
String password = userInfo.getPassword();
System.out.println("authc:pass:"+password);
//设置用户名,密码,盐,realm名字返回给判断器和真实用户输入的密码进行对比
SimpleAuthenticationInfo simpleAuthenticationInfo =
new SimpleAuthenticationInfo(username,password, ByteSource.Util.bytes(username+"666"),getName());
return simpleAuthenticationInfo;
}
}