美文网首页三个JAVA臭皮匠权限安全
SpringBoot整合Shiro实现权限管理与登陆注册

SpringBoot整合Shiro实现权限管理与登陆注册

作者: 后厂村老司机 | 来源:发表于2018-08-13 15:29 被阅读3次

    前言

    Shiro解决了什么问题?
    互联网无非就是一些用户C想要使用一些服务S的资源去完成某件事,S的资源不能说给谁用就给谁用,因此产生了权限的概念,即C必须有权限才能操作S的资源。S如何确定C就是C呢?因此又产生了身份验证的概念。一个Authorization一个Authentication就是Shiro解决的最重要的两个问题,其他的功能都是给Shiro打辅助的,比如Session管理,加密处理,记住我等。
    Shiro是什么?
    把Shiro想象成一家安全公司
    公司给服务端提供的服务是:服务端把自己维护的权限啊、用户啊、角色啊什么的信息通过接口提供给Shiro,shiro就可以帮服务端处理用户权限角色等的安全认证和授权等工作
    公司给客户提供的服务是:客户可以是任何外来的东西,但是想要访问服务端提供的要求权限验证等资源,就必须先经过shiro这层把关,shiro会对客户进行安全认证和授权等工作

    image.png

    Shiro重要概念有哪些?

    image.png
    • Subject:可以理解为与shiro打交道的对象,该对象封装了一些对方的信息,shiro可以通过subject拿到这些信息
    • SecurityManager:Shiro的总经理,通过指使Authorizer和Authenticator等对subject进行授权和身份验证等工作
    • Realm:管理着一些如用户、角色、权限等重要信息,Shiro中所需的这些重要信息都是从Realm这里获取的,Realm本质上就是一个重要信息的数据源
    • Authenticator:认证器,负责Subject的认证操作,认证过程就是根据Subject提供的信息通过Realm查询到相关信息,然后做对比,支持扩展
    • Authorizer:授权器,控制着Subject对服务资源的访问权限
    • SessionManager:用于管理Session,这个Session可以是web的也可以不是web的。
    • SessionDao:把Session的 CRUD和存储介质联系起来的工具,存储介质可以是数据库,也可以是缓存,比如把session放到redis里面
    • CacheManager:缓存控制器,Realm管理的数据(用户、角色、权限)可以放到缓存里由CacheManager管理,提高认证授权等的速度
    • Cryptography:加密组件,Shiro提供了很多加解密算法的组件
      我们需要操作哪些组件呢?
      作为一个成熟的跟Apache混的组件,用起来肯定越简单越符合用户习惯。我们使用的时候只需要把用户、角色、权限信息存储好(一般放到数据库里)并提供接口可以把这些信息注入到Shiro中就可以了。然后再对Shiro进行一些简单的配置即可!

    1、SpringBoot整合Shiro

    step1
    创建5张表,分别记录用户、角色、权限、用户角色关系、角色权限关系。
    建表很随意的,字段随便起名字,哪些字段也很随意,甚至有些表可以不存在的(比如权限表)。比如你可以简单的给user表一个id一个name,给role表一个id一个name。

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
      `id` int(32) NOT NULL,
      `name` varchar(60) DEFAULT NULL,
      `mob` varchar(60) DEFAULT NULL,
      `email` varchar(150) DEFAULT NULL,
      `valid` int(2) DEFAULT NULL,
      `pticket` varchar(200) DEFAULT NULL,
      `role_id` int(32) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role` (
      `uid` int(32) NOT NULL,
      `role_id` int(32) NOT NULL,
      PRIMARY KEY (`uid`,`role_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role` (
      `id` int(32) NOT NULL,
      `description` varchar(255) DEFAULT NULL,
      `role` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    DROP TABLE IF EXISTS `role_permission`;
    CREATE TABLE `role_permission` (
      `permission_id` int(32) NOT NULL,
      `role_id` int(32) NOT NULL,
      PRIMARY KEY (`permission_id`,`role_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    DROP TABLE IF EXISTS `permission`;
    CREATE TABLE `permission` (
      `id` int(32) NOT NULL,
      `name` varchar(255) DEFAULT NULL,
      `description` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    step2
    设计上面表的dao层和service层,只要提供根据user的id或者某个属性查询到所有的角色及权限信息就足够了
    比如我写的接口,dao用mybatisGenerator生成,service自己完成两个根据用户id获取角色权限信息的接口。

    @Service
    public class UserService {
        @Autowired
        private UserRoleDAO userRoleDAO;
        @Autowired
        private UserDAO userDAO;
        @Autowired
        private RoleDAO roleDAO;
        @Autowired
        private PermissionDAO permissionDAO;
        @Autowired
        private RolePermissionDAO rolePermissionDAO;
        //根据用户id查询所有的角色信息
        public List<Role> findRoles(Integer id) {
            UserRoleExample example = new UserRoleExample();
            example.createCriteria().andUidEqualTo(id);
            List<UserRoleKey> keyList = userRoleDAO.selectByExample(example);
            List<Integer> roleIdList = new ArrayList<>(keyList.size());
            for (UserRoleKey userRoleKey : keyList) {
                roleIdList.add(userRoleKey.getRoleId());
            }
            RoleExample roleExample = new RoleExample();
            roleExample.createCriteria().andIdIn(roleIdList);
            return roleDAO.selectByExample(roleExample);
        }
        //根据用户的id查询所有权限信息
        public List<Permission> findPermissions(Integer id) {
            List<Role> roles = findRoles(id);
            List<Integer> roleIds = new ArrayList<>(roles.size());
            for (Role role : roles) {
                roleIds.add(role.getId());
            }
            RolePermissionExample example = new RolePermissionExample();
            example.createCriteria().andRoleIdIn(roleIds);
            List<RolePermissionKey> keyList = rolePermissionDAO.selectByExample(example);
            List<Integer> permissionIdList = new ArrayList<>(keyList.size());
            for (RolePermissionKey rolePermissionKey : keyList) {
                permissionIdList.add(rolePermissionKey.getPermissionId());
            }
            PermissionExample permissionExample = new PermissionExample();
            if (permissionIdList.size() != 0) {
                permissionExample.createCriteria().andIdIn(permissionIdList);
                return permissionDAO.selectByExample(permissionExample);
            }
            return new ArrayList<>();
        }
    
        public User findUserById(String uId) {
            return userDAO.selectByPrimaryKey(Integer.valueOf(uId));
        }
    
        @Transactional
        public int assignDefaultUserRolePermission(User user) {
            int success1 = userDAO.insert(user);
            UserRoleKey userRoleKey = new UserRoleKey();
            userRoleKey.setUid(user.getId());
            userRoleKey.setRoleId(2);
            int success2 = userRoleDAO.insert(userRoleKey);
            return success1 + success2;
        }
    
    }
    
    

    step3
    把获取角色和权限信息的userService的两个接口提供给Shiro,让Shiro有法可依。

    • 首先,UserRealm这个类继承了AuthorizingRealm,这个类的作用是两处获取信息,一处是Subject即用户传过来的信息;一处是我们提供给shiro的userService接口以获取权限信息和角色信息。拿这两个信息之后AuthorizingRealm会自动进行比较,判断用户名密码,用户权限等等。
    • 然后,拿用户凭证信息的是doGetAuthenticationInfo接口,拿角色权限信息的是doGetAuthorizationInfo接口
    • 然后,两个重要参数,AuthenticationToken是我们可以自己实现的用户凭证/密钥信息,PrincipalCollection是用户凭证信息集合。注意Principals表示凭证(比如用户名、手机号、邮箱等)
    • 最后,配置完成对比的两方之后Subject.login(token)的时候就会调用doGetAuthenticationInfo方法;涉及到Subject.hasRole或者Subject.hasPermission的时候就会调用doGetAuthorizationInfo方法;
    public class UserRealm extends AuthorizingRealm {
        @Autowired
        private UserService userService;
    
        //shiro的权限信息配置
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            String uid = (String) principals.getPrimaryPrincipal();
            List<Role> roles = userService.findRoles(Integer.valueOf(uid));
            Set<String> roleNames = new HashSet<>(roles.size());
            for (Role role : roles) {
                roleNames.add(role.getRole());
            }
            //此处把当前subject对应的所有角色信息交给shiro,调用hasRole的时候就根据这些role信息判断
            authorizationInfo.setRoles(roleNames);
            List<Permission> permissions = userService.findPermissions(Integer.valueOf(uid));
            Set<String> permissionNames = new HashSet<>(permissions.size());
            for (Permission permission : permissions) {
                permissionNames.add(permission.getName());
            }
            //此处把当前subject对应的权限信息交给shiro,当调用hasPermission的时候就会根据这些信息判断
            authorizationInfo.setStringPermissions(permissionNames);
            return authorizationInfo;
        }
    
        //根据token获取认证信息authenticationInfo
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            /**这里为什么是String类型呢?其实要根据Subject.login(token)时候的token来的,你token定义成的pricipal是啥,这里get的时候就是啥。比如我
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken idEmail = new UsernamePasswordToken(String.valueOf(user.getId()), user.getEmail());
            try {
                idEmail.setRememberMe(true);
                subject.login(idEmail);
            }
            **/
            String uId = (String) token.getPrincipal();
            User user = userService.findUserById(uId);
            if (user == null) {
                return null;
            }
            //SimpleAuthenticationInfo还有其他构造方法,比如密码加密算法等,感兴趣可以自己看
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    uId,                      //表示凭证,可以随便设,跟token里的一致就行
                    user.getEmail(),   //表示密钥如密码,你可以自己随便设,跟token里的一致就行
                    getName()
            );
            //authenticationInfo信息交个shiro,调用login的时候会自动比较这里的token和authenticationInfo
            return authenticationInfo;
        }
    
    }
    

    step4
    对shiro进行一些配置,如登陆路径、权限验证、密码匹配等等.

    @Configuration
    public class ShiroConfig {
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            //拦截器.
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
            filterChainDefinitionMap.put("/logout", "anon");
            filterChainDefinitionMap.put("/afterlogout", "anon");
            //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
            //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
            filterChainDefinitionMap.put("/static/**", "anon");
    //        filterChainDefinitionMap.put("/html/**","anon");
            filterChainDefinitionMap.put("/afterlogin", "anon");
            filterChainDefinitionMap.put("/**", "authc");
            shiroFilterFactoryBean.setLoginUrl("/login");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
        @Bean
        public Realm myShiroRealm() {
            UserRealm myShiroRealm = new UserRealm();
            return myShiroRealm;
        }
    
    
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myShiroRealm());
            return securityManager;
        }
    
        @Bean(name="lifecycleBeanPostProcessor")
        public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
         *
         * @return
         */
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
            return authorizationAttributeSourceAdvisor;
        }
    }
    

    step5
    完成以上配置就可以正常些登录登出接口,以及权限验证接口了。我这里边是利用了类似于openid的统一登录认证接口,然后写了几个登录接口。注意这里的Session,一定要用SecurityUtils.getSubject.getSession,不然会有坑。

    • 对于需要验证权限的接口,比如要求角色直接在接口上加@RequiresRoles("admin"),@RequiresPermission("xxx")就可以了,注解里还有稍微高阶点的用法,自己Ctrl点进去看源码就行。
    • @RequireRoles有个坑,就是shiroFilter里配置的无权跳转路径是不跳转的,因为ajax获取与url直接获取是有区别的,我这边用的ajax获取。所以写一个统一的异常处理接口捕获无权异常就可以了。
    @RestController
    public class LoginController {
        @Autowired
        private LoginService loginService;
    
        @RequestMapping("/login")
        public void login(HttpServletResponse response) {
            response.setStatus(302);
            try {
                response.sendRedirect(OPSConstants.OPS_URL_WITH_RETURN);
            } catch (IOException e) {
    
            }
        }
    
        @RequestMapping("/afterlogin")
        public void recTicket(String ticket, HttpServletRequest request, HttpServletResponse response) {
            Map<String, String> paramMap = new HashMap<>(1);
            paramMap.put("ticket", ticket);
            String result = HttpHandler.getInstance().usingGetMethod(OPSConstants.OPS_URL_WITH_TICKET, paramMap, null);
            User user = JSON.parseObject(result, User.class);
            loginService.registerOrLogin(user);
            HttpSession session = request.getSession();
            session.setMaxInactiveInterval(1000 * 60 * 60);
            session.setAttribute(session.getId(), user);
            try {
                response.sendRedirect("/html/index.html");
            } catch (IOException e) {
    
            }
        }
    
        @RequestMapping(value = "/logout")
        public void logout(HttpServletRequest request, HttpServletResponse response) {
            HttpSession session = request.getSession();
            User user = (User) session.getAttribute(session.getId());
            String pticket = user.getPticket();
            String url = OPSConstants.OPS_URL_LOGOUT + "&pticket=" + pticket;
            response.setStatus(302);
            try {
                response.sendRedirect(url);
            } catch (IOException e) {
    
            }
        }
    
        @RequestMapping(value = "/afterlogout")
        public void afterLogout(HttpServletResponse response) {
            //这里一定要使用shiro退出方式,否则session失效
            SecurityUtils.getSubject().logout();
            try {
                response.sendRedirect(OPSConstants.OPS_URL_WITH_RETURN);
            } catch (IOException e) {
    
            }
        }
    
    }
    

    无权处理:

    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = UnauthorizedException.class)
        @ResponseBody
        public ResponseBO jsonExceptionHandler(HttpServletRequest req, Exception e) {
            return new ResponseBO(403, "权限不足!");
        }
    //    @ExceptionHandler(value = UnauthorizedException.class)
    //    public ModelAndView businessExceptionHandler(){
    //        ModelAndView mav = new ModelAndView();
    //        mav.setStatus(HttpStatus.UNAUTHORIZED);
    //        mav.addObject("message", e.getMessage());
    //        mav.setViewName("403");
    //        return mav;
    //    }
    }
    

    总结

    shiro虽然轻量,但是坑还是很多的,官方文档和网上的博客对初学者并不友好。学习的方法是git上找一个能通的项目,然后直接ctrl+左键查看源码就行,或者debug的时候打断点,查看那些配置的方法及传的参数。


    image.png

    相关文章

      网友评论

      • TJJ:推荐一个能跑通的项目呗??
        后厂村老司机:我都是自己搭的,因为我依赖的的是openid第三方登录,所以我的项目不是很通用。你可以按照我的教程搭一下

      本文标题:SpringBoot整合Shiro实现权限管理与登陆注册

      本文链接:https://www.haomeiwen.com/subject/yajdbftx.html