一、本篇概述

    前面两篇中有讲解Spring boot整合shiro如何通过读取ini文件,数据库用户数据实现登录和权限认证授权等,那么就有人问了,我如果通过数据库中存储的当前用户可以访问哪些rul,来实现过滤当前用户可以访问哪接口或者是可以访问哪些页面,那么该怎么做呢?

例如有一个接口,比如获取所有用户信息的接口,我要实现admin用户可以访问,但是其他用户不可以访问。对于接口而言自然就会有暴露给外部调用的接口url,而本篇主要就是讲解如何通过url来实现用户的权限过滤。

与上篇博客一样,因篇幅原因粘贴部分重要的代码,全部的代码下载demo案例

二、pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com</groupId>
    <artifactId>shiro3</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro3</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 添加mybatis-plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1.tmp</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.1.tmp</version>
        </dependency>

        <!-- 添加shrio依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.1</version>
        </dependency>

        <!-- 添加mysql依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- 添加阿里数据源依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- servlet依赖. -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <!-- jsp支持-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

三、代码生成器

与数据交互的dao层是用的mybatis plus,所以我们可以直接使用它提供的代码生成器修改一番生成我们需要的文件

package com.shiro3.util;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @Package: com.tp.mybatisplusstudy
 * @ClassName: CodeGenerator
 * @Author: tanp
 * @Description: 代码生成器,根据数据库中的表自动生成mapper和表格对应实体类等
 * @Date: 2020/4/21 20:26
 */
public class CodeGenerator {
    private static final String URL = "jdbc:mysql://192.169.1.167:3306/shiro2?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=true";

    private static final String USER_NAME = "root";

    private static final String PASSWORD = "123456";

    private static final String AUTHOR = "tanp";

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor(AUTHOR);
        gc.setOpen(false);
        //实体属性 Swagger2 注解
        gc.setSwagger2(false);
        gc.setDateType(DateType.ONLY_DATE);
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(URL);
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername(USER_NAME);
        dsc.setPassword(PASSWORD);
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.shiro3");
        pc.setMapper("dao");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/"
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        //控制 不生成 controller
        templateConfig.setController("");

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);

        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

三、shiro配置

package com.shiro3.shiro;

import com.shiro3.filter.URLPathMatchingFilter;
import com.shiro3.relam.AuthRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Package: com.shior2
 * @ClassName: ShiroConfig
 * @Author: tanp
 * @Description: ${description}
 * @Date: 2020/7/3 14:11
 */

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("mySecurityManager") SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(manager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        //自定义拦截器
        Map<String, Filter> customisedFilter = new HashMap<>(6);
        customisedFilter.put("url", getURLPathMatchingFilter());

        //拦截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        //配置映射关系, 顺序判断
        // 配置不会被拦截的链接
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/index", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/config*/**", "anon");
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        filterChainDefinitionMap.put("/doLogout", "logout");
        //其余接口一律拦截
        filterChainDefinitionMap.put("/**", "url");
        shiroFilterFactoryBean.setFilters(customisedFilter);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    // * URLPathMatchingFilter 不可以用@Bean管理起来。 原因是Shiro的bug, 这个也是过滤器,
    // * ShiroFilterFactoryBean 也是过滤器,当他们都出现的时候,默认的什么anno,authc,logout过滤器就失效了。所以不能把他声明为@Bean
    public URLPathMatchingFilter getURLPathMatchingFilter() {
        return new URLPathMatchingFilter();
    }

    @Bean("mySecurityManager")
    public SessionsSecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        return securityManager;
    }

    @Bean("authRealm")
    public AuthRealm getAuthRealm() {
        AuthRealm authRealm = new AuthRealm();
        return authRealm;
    }
}

   本篇博客的shiro配置与上篇博客相比,增加了一些配置,这里也就讲增加的配置讲解一下。增加的代码在ShiroFilterFactoryBean对象配置时添加,可以看到在配置该对象时,设置了登录页面、首页、未授权页面还有拦截器,可以看到对于不同的访问请求的url,分别设置了不同的权限。还设置了一个过滤器,实现url过滤,下面来看下过滤器的代码

四、URLPathMatchingFilter过滤器

package com.shiro3.filter;

import com.shiro3.service.IPermissionService;
import com.shiro3.util.SpringContextUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.Set;

/**
 * @Description URLPathMatchingFilter 不可以用@Bean管理起来。 原因是Shiro的bug, 这个也是过滤器,
 * ShiroFilterFactoryBean 也是过滤器,当他们都出现的时候,默认的什么anno,authc,logout过滤器就失效了。所以不能把他声明为@Bean。
 * @Date 2020/7/10 16:32
 * @Author tanp
 */
public class URLPathMatchingFilter extends PathMatchingFilter {
    @Autowired
    IPermissionService iPermissionService;

    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue)
            throws Exception {

        //因为URLPathMatchingFilter 并没有用@Bean管理起来,所以在该类中就无法就注解注入被Spring管理的IPermissionService类,需要手动获取
        if(null==iPermissionService){
            iPermissionService = SpringContextUtils.getContext().getBean(IPermissionService.class);
        }


        String requestURI = getPathWithinApplication(request);
        System.out.println("requestURI:" + requestURI);

        Subject subject = SecurityUtils.getSubject();
        // 如果没有登录,就跳转到登录页面
        if (!subject.isAuthenticated()) {
            WebUtils.issueRedirect(request, response, "/login");
            return false;
        }

        // 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行)
        boolean needInterceptor = iPermissionService.needInterceptor(requestURI);
        if (!needInterceptor) {
            return true;
        } else {
            boolean hasPermission = false;
            String userName = subject.getPrincipal().toString();
            Set<String> permissionUrls = iPermissionService.listPermissionURLs(userName);
            for (String url : permissionUrls) {
                // 这就表示当前用户有这个权限
                if (url.equals(requestURI)) {
                    hasPermission = true;
                    break;
                }
            }

            if (hasPermission) {
                return true;
            } else {
                UnauthorizedException ex = new UnauthorizedException("当前用户没有访问路径 " + requestURI + " 的权限");
                subject.getSession().setAttribute("ex", ex);
                WebUtils.issueRedirect(request, response, "/unauthorized");
                return false;
            }

        }
    }
}

    在过滤器中,我们获取请求的url路径,然后通过用户名获取该用户可以访问的请求url路径,然后进行匹配判断是否有权限访问当前访问的请求url。

五、shiro调用

package com.shiro3.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * @Description 登录
 * @Date 2020/7/8 14:45
 * @Author tanp
 */
@Controller
@RequestMapping("")
public class LoginController {

    /**
     * @Description 登陆, 页面post提交才进入此方法
     * @Date 2020/7/8 14:45
     * @Author tanp
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(Model model, String name, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(name, password);
        try {
            subject.login(token);
            Session session = subject.getSession();
            session.setAttribute("subject", subject);
            return "redirect:index";
        } catch (AuthenticationException e) {
            model.addAttribute("error", "验证失败");
            return "login";
        }
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login1() {
        return "login";
    }

}

可以看到,无论配置如何变化,最终用户做登录对象做交互的都是subject对象

六、代码整体架构

statefulset 对外暴露_shiro自定义relam

statefulset 对外暴露_shiro自定义过滤器_02

七、数据库脚本

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
    
drop table if exists user;
drop table if exists role;
drop table if exists permission;
drop table if exists user_role;
drop table if exists role_permission;
    
create table user (
  id bigint auto_increment,
  name varchar(100),
  password varchar(100),
  salt varchar(100),
  constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
    
create table role (
  id bigint auto_increment,
  name varchar(100),
  desc_ varchar(100),
  constraint pk_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
    
create table permission (
  id bigint auto_increment,
  name varchar(100),
  desc_ varchar(100),
  url varchar(100),
  constraint pk_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
    
create table user_role (
  id bigint auto_increment,
  uid bigint,
  rid bigint,
  constraint pk_users_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
    
create table role_permission (
  id bigint auto_increment,
  rid bigint,
  pid bigint,
  constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
 
INSERT INTO `permission` VALUES (1,'addProduct','增加产品','/addProduct');
INSERT INTO `permission` VALUES (2,'deleteProduct','删除产品','/deleteProduct');
INSERT INTO `permission` VALUES (3,'editeProduct','编辑产品','/editeProduct');
INSERT INTO `permission` VALUES (4,'updateProduct','修改产品','/updateProduct');
INSERT INTO `permission` VALUES (5,'listProduct','查看产品','/listProduct');
INSERT INTO `permission` VALUES (6,'addOrder','增加订单','/addOrder');
INSERT INTO `permission` VALUES (7,'deleteOrder','删除订单','/deleteOrder');
INSERT INTO `permission` VALUES (8,'editeOrder','编辑订单','/editeOrder');
INSERT INTO `permission` VALUES (9,'updateOrder','修改订单','/updateOrder');
INSERT INTO `permission` VALUES (10,'listOrder','查看订单','/listOrder');
INSERT INTO `role` VALUES (1,'admin','超级管理员');
INSERT INTO `role` VALUES (2,'productManager','产品管理员');
INSERT INTO `role` VALUES (3,'orderManager','订单管理员');
INSERT INTO `role_permission` VALUES (1,1,1);
INSERT INTO `role_permission` VALUES (2,1,2);
INSERT INTO `role_permission` VALUES (3,1,3);
INSERT INTO `role_permission` VALUES (4,1,4);
INSERT INTO `role_permission` VALUES (5,1,5);
INSERT INTO `role_permission` VALUES (6,1,6);
INSERT INTO `role_permission` VALUES (7,1,7);
INSERT INTO `role_permission` VALUES (8,1,8);
INSERT INTO `role_permission` VALUES (9,1,9);
INSERT INTO `role_permission` VALUES (10,1,10);
INSERT INTO `role_permission` VALUES (11,2,1);
INSERT INTO `role_permission` VALUES (12,2,2);
INSERT INTO `role_permission` VALUES (13,2,3);
INSERT INTO `role_permission` VALUES (14,2,4);
INSERT INTO `role_permission` VALUES (15,2,5);
INSERT INTO `role_permission` VALUES (50,3,10);
INSERT INTO `role_permission` VALUES (51,3,9);
INSERT INTO `role_permission` VALUES (52,3,8);
INSERT INTO `role_permission` VALUES (53,3,7);
INSERT INTO `role_permission` VALUES (54,3,6);
INSERT INTO `role_permission` VALUES (55,3,1);
INSERT INTO `role_permission` VALUES (56,5,11);
INSERT INTO `user` VALUES (1,'zhang3','12345','e5ykFiNwShfCXvBRPr3wXg==');
INSERT INTO `user` VALUES (2,'li4','abcde','jPz19y7arvYIGhuUjsb6sQ==');
INSERT INTO `user_role` VALUES (43,2,2);
INSERT INTO `user_role` VALUES (45,1,1);

八、总结

   与前面一篇不同,本篇博客中的案例是真实的从前端页面获取用户信息的请求url,然后对用户进行登录判断,和url的过滤。