Springboot 使用 Shiro 实现多个自定义 Realm 的认证与授权

chan 作者
阅读 13354 喜欢 6

场景:
假设在我们的系统中 有两个角色用户管理员
目前用户有100W,但管理员只有2个
假设是让你设计这个表 你会怎么设计?
如果使用经典的 RBAC 五张表会产生一个问题,管理员登陆通过用户名查询需要检索用户数据100W+
如果是我们能够将这两个角色的数据设计到两张表里面:管理员表(2个人), 用户表(100W)
管理员的表,用户的表假设我们都使用shiro来进行认证 ? 怎么办?

设计思路:
用户登陆时候设置一个登陆类型,重写 UserNameAndPasswordToken 在执行正式的登陆的时候,根据登陆的类型选择 我们要使用的 Realm 最终去进行认证那么这个功能就搞定了
存在两个问题
1:在哪里去做选择 执行哪一个realm?
ModularRealmAuthenticator.java类进行拓展(直接和realm打交道的)
2:我们的登陆类型如何来定

一、创建 RBAC 权限角色表并初始化(用户与管理员分开两张表)

CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
  `name` varchar(64) NOT NULL COMMENT '权限名称',
  `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
  `url` varchar(255) NOT NULL COMMENT '授权路径',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';

insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values 
(21,0,'查看用户','UserView','',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'),
(22,0,'新增用户','UserInsert','',NULL,'2019-04-04 15:30:31','2019-04-04 15:30:44'),
(23,0,'编辑用户','UserUpdate','',NULL,'2019-04-04 15:30:32','2019-04-04 15:30:45'),
(24,0,'删除用户','UserDelete','',NULL,'2019-04-04 15:30:48','2019-04-04 15:30:45');

CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
  `name` varchar(64) NOT NULL COMMENT '角色名称',
  `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';

insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values 
(10,0,'超级管理员','admin',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05'), 
(11,0,'用户','user',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05'
);

CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';

insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values 
(1,10,21),
(2,10,22),
(3,10,23),
(4,10,24),
(5,11,21),
(6,11,23);


CREATE TABLE `tb_admin` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';

insert  into `tb_admin`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values 
(1,'chan','asdf','15888888838','a2@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';

insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values 
(2,'ajax','asdf','15888888838','a2@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29'),
(3,'axios','asdf','15888888888','a1@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');

CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';

insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values 
(1,1,10),
(2,2,11),
(3,3,11);

二、导包

<!--导入thymeleaf对shiro的支持包-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

<!--导入shiro的功能包-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

三、创建前端页面

登录

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<span th:text="${msg}" style="color: red"></span><br>
普通用户登录窗口<br>
<form action="/userLogin" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit" value="用户登陆">
</form>

<hr>

管理员登录窗口<br>
<form action="/adminLogin" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit" value="用户登陆">
</form>

</body>
</html>

登录成功

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title th:text="${user.username}"></title>
</head>
<body>
账号 <span th:text="${user.username}"></span> 登入成功 <br>

<!-- 拥有某权限才会显示 -->
<shiro:hasPermission name="UserInsert">
    <a href="/create">添加用户</a>
</shiro:hasPermission>
<shiro:hasPermission name="UserUpdate">
    <a href="/update">修改用户</a>
</shiro:hasPermission>
<shiro:hasPermission name="UserDelete">
    <a href="/delete">删除用户</a>
</shiro:hasPermission>
<shiro:hasPermission name="UserView">
    <a href="/read">查看用户</a>
</shiro:hasPermission>
</body>
</html>

四、创建 UserRealm 和 AdminRealm

UserRealm

/**
 * @author CHAN
 * @date 2020/02/28
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private TbUserService tbUserService;

    @Override
    public String getName() {
        return "UserRealm";
    }

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取用户对象
        TbUser user = (TbUser) principalCollection.getPrimaryPrincipal();

        // 根据用户名获取用户权限
        Set<String> perms = tbUserService.findPermissionByUsername(user.getUsername());

        // 根据用户名获取用户角色(假设查出来的角色为 user )
        Set<String> roles = new HashSet<>();
        roles.add("user");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(roles);
        authorizationInfo.setStringPermissions(perms);
        return authorizationInfo;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();

        // 根据用户名查出用户对象
        TbUser user = tbUserService.findUserByUsername(username);
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }
}

AdminRealm

/**
 * @author CHAN
 * @date 2020/02/29
 */
public class AdminRealm extends AuthorizingRealm {

    @Autowired
    private TbAdminService tbAdminService;

    @Override
    public String getName() {
        return "AdminRealm";
    }

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取用户对象
        TbAdmin admin = (TbAdmin) principalCollection.getPrimaryPrincipal();

        // 根据用户名查询用户权限
        Set<String> perms = tbAdminService.findPermissionByUsername(admin.getUsername());

        // 根据用户名查询用户角色
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(roles);
        authorizationInfo.setStringPermissions(perms);
        return authorizationInfo;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        TbAdmin admin = tbAdminService.findAdminByUsername(username);
        return new SimpleAuthenticationInfo(admin, admin.getPassword(), getName());
    }
}

五、自定义认证和授权

认证

继承ModularRealmAuthenticator 重写 doAuthenticate() 方法

/**
 * @author CHAN
 * @date 2020/02/28
 */
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // realm 校验
        assertRealmsConfigured();

        // 获取前端传递过来的 token
        CustomToken customToken = (CustomToken) authenticationToken;

        // 获取登录类型
        String loginType = customToken.getLoginType();

        // 获取所有的 realms()
        Collection<Realm> realms = getRealms();

        // 把与登录类型一致的 realms 存放在集合中
        Collection<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }
        return typeRealms.size() == 1 ? doSingleRealmAuthentication(typeRealms.iterator().next(), customToken) : doMultiRealmAuthentication(typeRealms, customToken);
    }
}

授权

继承ModularRealmAuthorizer,分别重写:
isPermitted(PrincipalCollection principals, String permission);
isPermitted(PrincipalCollection principals, Permission permission);
hasRole(PrincipalCollection principals, String roleIdentifier);

/**
 * @author CHAN
 * @date 2020/02/29
 */
public class CustomModularRealmAuthorizer extends ModularRealmAuthorizer {

    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        // 校验 realm
        assertRealmsConfigured();
        // 获取 realmName
        Set<String> realmNames = principals.getRealmNames();
        String realmName = realmNames.iterator().next();
        // 获取所有 realm
        Collection<Realm> realms = getRealms();
        for (Realm  realm : realms) {
            if (!(realm instanceof Authorizer)) {
                continue;
            }
            if ("AdminRealm".equals(realmName)) {
                if (realm instanceof AdminRealm) {
                    return ((AdminRealm) realm).isPermitted(principals, permission);
                }
            }
            if("UserRealm".equals(realmName)) {
                if (realm instanceof UserRealm) {
                    return ((UserRealm) realm).isPermitted(principals, permission);
                }
            }
        }
        return false;
    }

    @Override
    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        assertRealmsConfigured();
        Set<String> realmNames = principals.getRealmNames();
        String realmName = realmNames.iterator().next();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) {
                continue;
            }
            //匹配名字
            if("AdminRealm".equals(realmName)) {
                if (realm instanceof AdminRealm) {
                    return ((AdminRealm) realm).isPermitted(principals, permission);
                }
            }
            //匹配名字
            if("UserRealm".equals(realmName)) {
                if (realm instanceof UserRealm) {
                    return ((UserRealm) realm).isPermitted(principals, permission);
                }
            }
        }
        return super.isPermitted(principals, permission);
    }

    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        assertRealmsConfigured();
        Set<String> realmNames = principals.getRealmNames();
        //获取realm的名字
        String realmName = realmNames.iterator().next();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)){
                continue;
            }
            //匹配名字
            if("AdminRealm".equals(realmName)) {
                if (realm instanceof AdminRealm) {
                    return ((AdminRealm) realm).hasRole(principals, roleIdentifier);
                }
            }
            //匹配名字
            if("UserRealm".equals(realmName)) {
                if (realm instanceof UserRealm) {
                    return ((UserRealm) realm).hasRole(principals, roleIdentifier);
                }
            }
        }
        return false;
    }
}

六、自定义 token 支持登录类型

继承UsernamePassowrdToken

public class CustomToken extends UsernamePasswordToken {

    //定义登陆的类型是为了在后面的校验中 去选择使用哪一个realm
    private String loginType;

    public CustomToken(String userName,String password,String loginType){
          super(userName,password);
          this.loginType=loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }
}

七、配置 ShiroConfig 类

/**
 * @author CHAN
 * @date 2020/02/28
 */
@SpringBootConfiguration
public class ShiroConfig {

    /**
     * shiro 拦截器配置
     */
    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置如果认证失败的登录的跳转地址
        shiroFilterFactoryBean.setLoginUrl("/toLogin");

        Map<String, String> maps = new HashMap<>();
        // 无需认证可以访问
        maps.put("/toLogin","anon");
        maps.put("/userLogin","anon");
        maps.put("/adminLogin","anon");
        // 必须拥有某权限才能访问
        maps.put("/read", "perms[UserView]");
        maps.put("/delete", "perms[UserDelete]");
        maps.put("/update", "perms[UserUpdate]");
        maps.put("/create", "perms[UserInsert]");
        // 需要认证才能访问
        maps.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(maps);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

    /**
     * 配置 securityManager
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义认证
        securityManager.setAuthenticator(authenticator());
        // 自定义授权
        securityManager.setAuthorizer(authorizer());
        // 自定义 Realm
        List<Realm> realms = new ArrayList<>();
        realms.add(userRealm());
        realms.add(adminRealm());
        // 设置 realm
        securityManager.setRealms(realms);
        return securityManager;
    }

    /**
     * 配置 shiro-dialect 这个方言(模板引擎支持 shiro)
     */
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

    /**
     * 配置 UserRealm
     */
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    /**
     * 配置 AdminRealm
     */
    @Bean
    public AdminRealm adminRealm() {
        return new AdminRealm();
    }

    /**
     * 配置认证
     */
    @Bean
    public CustomModularRealmAuthenticator authenticator() {
        return new CustomModularRealmAuthenticator();
    }

    /**
     * 配置授权
     */
    @Bean
    public CustomModularRealmAuthorizer authorizer() {
        return new CustomModularRealmAuthorizer();
    }

}

八、Controller

AdminController

/**
 * @author CHAN
 * @date 2020/02/28
 */
@Controller
public class AdminController {

    /**
     * 使用枚举设置登录类型为 admin
     */
    private static final String LOGIN_TYPE = LoginType.ADMIN.getType();

    @PostMapping(value = "adminLogin")
    public String toAdmin(TbAdmin admin, Model model) {
        CustomToken customToken = new CustomToken(admin.getUsername(), admin.getPassword(), LOGIN_TYPE);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(customToken);
            if (subject.isAuthenticated()) {
                model.addAttribute("user", admin);
                return "success";
            }
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "账号不存在");
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return "login";
    }

}

UserController

/**
 * @author CHAN
 * @date 2020/02/28
 */
@Controller
public class UserController {

    /**
     * 使用枚举设定登录类型为 user
     */
    private static final String LOGIN_TYPE = LoginType.USER.getType();


    @PostMapping(value = "userLogin")
    public String userLogin(TbUser user, Model model) {
        CustomToken customToken = new CustomToken(user.getUsername(), user.getPassword(), LOGIN_TYPE);
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.login(customToken);
            if (subject.isAuthenticated()) {
                model.addAttribute("user", user);
                return "success";
            }
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "账号不存在");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误");
            return "login";
        } catch (Exception e) {
            System.out.println(e.getMessage());
            model.addAttribute("msg", "异常");
            return "login";
        }
        return "login";
    }

}

九、测试

全部评论0