Shiro-Springboot授权认证图解(扫盲)2020全代码流程分析---看不会可骂我

写在前面

官方文档:http://shiro.apache.org/introduction.html
别看我很牛逼,其实下面这些理论都是我从官网抄录下来的

前半段是理论,不敢兴趣的人,直接跳过。。。看后面的实际操作,所谓程序员先做后学,才记忆深刻。

本文较长,希望各位同学耐心看完,最好跟我一起制作demo

本文重在讲授权、认证流程,其他功能,我们在学会了基础之后,再慢慢深入了解

一、Shrio

1.1. 什么是Apache Shiro?

Apache Shiro是一个功能强大且灵活的开源安全框架,可以干净地处理身份验证,授权,企业会话管理和加密。

1.2. Shiro有哪些功能

在这里插入图片描述
Shiro的目标是Shiro开发团队所称的“应用程序安全的四个基石”——认证、授权、会话管理和加密:

  • (一)Authentication 身份验证:有时被称为“登录”,这是一种证明用户是他们所说的那个人的行为。
  • (二)Authorization 授权:访问控制的过程,即决定“谁”可以访问“什么”。
  • (三)Session Management 会话管理:管理特定于用户的会话,甚至在非web或EJB应用程序中也是如此。
  • (四)Cryptography 密码学:保持数据安全使用加密算法,同时仍然易于使用。

在不同的应用程序环境中,还有一些额外的特性来支持和加强这些关注,特别是:

  • (一)、Web Support Web支持:Shiro的Web支持api帮助轻松地保护Web应用程序。
  • (二)、Caching 缓存:在Apache Shiro的API中,缓存是第一层公民,以确保安全操作保持快速和高效。
  • (三)Concurrency 并发性:Apache Shiro支持具有并发性特性的多线程应用程序。
  • (四)Testing 测试:存在测试支持来帮助您编写单元和集成测试,并确保您的代码将如预期的那样得到保护。
  • (五)“Run As” “以用户身份运行”:允许用户假设另一个用户的身份(如果允许的话)的特性,有时在管理场景中非常有用。
  • (六)“Remember Me” “记住我”:在会话中记住用户的身份,这样他们只需要在强制登录时登录。

1.3 主要概念

以上说完Shiro的主要功能之后,我们再来看看Shiro有那些概念

在最高的概念层次上,Shiro的架构有3个主要概念:主题、安全管理器和领域。下图是这些组件如何交互的高级概述,我们将介绍下面的每个概念:
在这里插入图片描述

Subject

Subject实例都绑定到(并且需要)一个SecurityManager。当您与一个Subject交互时,这些交互转换为与SecurityManager的特定于Subject的交互。

SecurityManager

SecurityManager是Shiro架构的核心,充当一种“保护伞”对象,协调其内部安全组件,这些组件一起形成一个对象图。但是,一旦为应用程序配置了SecurityManager及其内部对象图,它通常就会被搁置,应用程序开发人员几乎把所有时间都花在Subject API上。

Realms

从这个意义上说,领域本质上是一个特定于安全性的DAO:它封装了数据源的连接细节并使之具有asso特性根据需要向Shiro提供关联数据。在配置Shiro时,必须指定至少一个用于身份验证和/或授权的领域。SecurityManager可以配置多个域,但至少需要一个域。

1.4. Shiro的核心体系结构概念

在这里插入图片描述
涉及的关键词有:

  • Subject 主题(org.apache.shiro.subject.Subject)
  • SecurityManager (org.apache.shiro.mgt.SecurityManager)
  • Authenticator 身份验证(org.apache.shiro.authc.Authenticator)
  • Authentication Strategy 身份验证策略(org.apache.shiro.authc.pam.AuthenticationStrategy)
  • Authorizer 授权人(org.apache.shiro.authz.Authorizer)
  • SessionManager (org.apache.shiro.session.mgt.SessionManager)
  • SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO)
  • CacheManager 缓存管理器(org.apache.shiro.cache.CacheManager)
  • Cryptography 加密(org.apache.shiro.crypto。*)
  • Realms领域(org.apache.shiro.realm.Realm)

上面的概念图还是东西有点多,我来简化一下
在这里插入图片描述

二、Shiro-Springboot代码准备工作

在这里插入图片描述

2.1 准备测试使用的SQL语句

在这里插入图片描述
在这里插入图片描述
直接复制到数据库软件运行建表就可以。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  `admin` bit(1) NULL DEFAULT NULL,
  `dept_id` bigint(20) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `employee` VALUES (1, 'admin', 'e00cf25ad42683b3df678c61f42c6bda', '[email protected]', 20, b'1', 2);
INSERT INTO `employee` VALUES (2, '鲁班', 'd3b284c242a1c1fa0615e8d0bdf234bb', '[email protected]', 35, b'0', 1);
INSERT INTO `employee` VALUES (3, '赵云', 'a46fbb88724b575acc9bf5e164ae4ac3', '[email protected]', 25, b'0', 1);

DROP TABLE IF EXISTS `employee_role`;
CREATE TABLE `employee_role`  (
  `employee_id` bigint(20) NULL DEFAULT NULL,
  `role_id` bigint(20) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `employee_role` VALUES (1, 1);
INSERT INTO `employee_role` VALUES (2, 2);
INSERT INTO `employee_role` VALUES (3, 3);
INSERT INTO `employee_role` VALUES (3, 4);

DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `expression` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `permission` VALUES (1, '远程攻击', 'department:list');
INSERT INTO `permission` VALUES (2, '近身攻击', 'department:');

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `sn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `role` VALUES (1, '麻花藤', 'BOSS');
INSERT INTO `role` VALUES (2, '射手', 'SS');
INSERT INTO `role` VALUES (3, '战士', 'ZS');
INSERT INTO `role` VALUES (4, '刺客', 'CK');

DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission`  (
  `role_id` bigint(20) NULL DEFAULT NULL,
  `permission_id` bigint(20) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `role_permission` VALUES (1, 1);
INSERT INTO `role_permission` VALUES (1, 2);
INSERT INTO `role_permission` VALUES (2, 1);
INSERT INTO `role_permission` VALUES (3, 2);
INSERT INTO `role_permission` VALUES (4, 2);

SET FOREIGN_KEY_CHECKS = 1;

2.2 项目需要的依赖

  • 2.2.1 创建一个springboot的项目
    注意:为了节省大家的时间,请严格按照的我包路径进行配置,下面有自动生成代码的方式,如果一样的包路径,就不需要进行多余的修改。
    包路径为:com.cancan.shirospringboot
    在这里插入图片描述
  • 2.2.2 pom.xml
        <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>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>

        <!--mybatis-plus自动的维护了mybatis以及mybatis-spring的依赖,
        在springboot中这三者不能同时的出现,避免版本的冲突,表示:跳进过这个坑-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.1</version>
        </dependency>

        <!-- 引入Druid依赖,阿里巴巴所提供的数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.29</version>
        </dependency>

        <!-- 提供mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.1-android</version>
        </dependency>
        <!--fastjson-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.56</version>
		</dependency>

2.3 mybatis-plus快速生成代码

在这里插入图片描述

/**
 *  演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
 *
 * @author cancan
 * @since 2020-03-23
 */
public class CodeGenerator {

    /**
     * 配置模块与表
     */
    private static Map<String,List<String>> moduleTableMap = Maps.newHashMap();

    static{
        //key为当前所有文件放置的包名,如果为空指的就是packageConfig父类目录下,value为在该包名下生成的表,可以多张表,存放在list
        moduleTableMap.put("", Arrays.asList("employee","permission","role"));
    }

    public static void main(String[] args) {
        autoGenerator();
    }

    private static void autoGenerator(){
        if(MapUtils.isNotEmpty(moduleTableMap)){
            for(Map.Entry<String,List<String>> entry : moduleTableMap.entrySet()){
                List<String> tables = entry.getValue();
                if(CollectionUtils.isNotEmpty(tables)){
                    tables.stream().forEach(e->{
                        // 代码生成器
                        AutoGenerator mpg = new AutoGenerator();
                        //全局配置
                        String projectPath = globalConfig(mpg);
                        //数据库配置
                        dataSourceConfig(mpg);
                        //包配置
                        PackageConfig pc =packageConfig(mpg,entry.getKey());
                        injectionConfig(mpg,pc,projectPath);
                        strategyConfig(mpg,pc,e);
                        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
                        mpg.execute();
                    });
                }
            }
        }else{
            throw new RuntimeException("请配置模块");
        }
    }

    /**
     * 设置全局配置.
     *
     * @param mpg 自动代码生成对象
     * @return 设置全局配置
     */
    private static String globalConfig(AutoGenerator mpg){
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("cancan");
        gc.setOpen(false);
        gc.setDateType(DateType.ONLY_DATE);
        gc.setIdType(IdType.UUID);
        mpg.setGlobalConfig(gc);
        return projectPath;
    }

    /**
     * 数据源配置
     * @param mpg 自动代码生成对象
     */
    private static void dataSourceConfig(AutoGenerator mpg){
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=utf-8&serverTimezone=UTC");
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("admin");
        mpg.setDataSource(dsc);
    }

    /**
     * 设置包配置.
     *
     * @param mpg 自动代码生成对象
     * @param moduleName 当前包的名字
     * @return 包的配置
     */
    private static PackageConfig packageConfig(AutoGenerator mpg, String moduleName){
        PackageConfig pc = new PackageConfig();
        //模块名
        pc.setModuleName(moduleName);
        pc.setParent("com.cancan.shirospringboot");
        mpg.setPackageInfo(pc);
        return pc;
    }

    /**
     * 自定义配置
     *
     * @param mpg 自动代码生成对象
     * @param pc 包配置
     * @param projectPath 设置全局配置
     */
    private static void injectionConfig(AutoGenerator mpg, PackageConfig pc, String projectPath){
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };
        List<FileOutConfig> focList = new ArrayList<>();
        focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
            @Override
            public String outputFile(TableInfo tableInfo) {
                String outPath = projectPath+"/src/main/java/"+pc.getParent().replaceAll("\\.","/")
                        +"/mapper/"+tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
                return outPath;
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);
        mpg.setTemplate(new TemplateConfig().setXml(null));
    }

    /**
     * 策略配置
     *
     * @param mpg 自动代码生成对象
     * @param pc 包配置
     * @param tableName 表格名字
     */
    private static void strategyConfig(AutoGenerator mpg, PackageConfig pc, String tableName){
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(tableName);
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);
    }
}

在这里插入图片描述

2.4 在Shiro中继承了MD5的算法,所以可以直接使用它来实现对密码进行加密测试

在这里插入图片描述

2.4.1 配置配置信息

将 application.properties 修改为 application.yml
由于 2.3 添加了数据库信息,所以需要配置数据库的连接池和连接账号密码

server:
  port: 8081

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: admin

2.4.2 使用Shrio自带的MD5加密,生成密码

直接在springboot项目的test运行即可

@SpringBootTest
class ShiroSpringbootApplicationTests {

    private final String password = "1";
    //盐,默认是用账号来当盐
    private final String salt = "admin";

    @Test
    void testMD5() throws Exception{
        //直接加密
        Md5Hash hash= new Md5Hash(password);
        System.out.println("账号为:"+salt+", 密码为:"+password+", 直接加密:"+hash);
        //带着盐加密,本演示项目只是加密一次
        Md5Hash hash1 = new Md5Hash(password, salt);
        System.out.println("账号为:"+salt+", 密码为:"+password+",带着盐加密:"+hash1);
        //带着盐加密,而且加密多次
        //Md5Hash hash2 = new Md5Hash(password, salt, 2);
        //System.out.println("带着盐加密两次"+hash2);
    }
}

测试结果:

账号为:admin, 密码为:1, 直接加密:c4ca4238a0b923820dcc509a6f75849b
账号为:admin, 密码为:1,带着盐加密:e00cf25ad42683b3df678c61f42c6bda

2.5 自定义Realm做准备

自定义Realm需要认证/授权, 认证需要查找用户的信息出来比对,授权需要查找用户关联的角色和权限。下面是这几个功能的sql语句。
在这里插入图片描述

2.5.1 创建根据用户名字查找用户

EmployeeMapper.java

public interface EmployeeMapper extends BaseMapper<Employee> {
    Employee selectEmployeeByUsername(String username);
}

EmployeeMapper.xml

    <select id="selectEmployeeByUsername" resultType="com.cancan.shirospringboot.entity.Employee">
        SELECT
            id,
            name,
            password,
            email,
            age,
            admin,
            dept_id
        FROM employee
        WHERE name = #{username}
    </select>

2.5.2 根据登录用户的id查询到其拥有的所有角色的编码

RoleMapper.java

public interface RoleMapper extends BaseMapper<Role> {
    List<String> selectRoleSnsByEmpId(Long id);
}

RoleMapper.xml

    <select id="selectRoleSnsByEmpId" resultType="java.lang.String">
        SELECT r.sn
        FROM role r
                 LEFT JOIN employee_role er
                           ON r.id = er.role_id
        WHERE er.employee_id = #{employeeId}
    </select>

2.5.3 根据登录用户的id查询到其拥有的所有权限表达式

PermissionMapper.java

public interface PermissionMapper extends BaseMapper<Permission> {
    List<String> selectExpressionByEmpId(Long id);
}

PermissionMapper.xml

    <select id="selectExpressionByEmpId" resultType="java.lang.String">
        SELECT p.expression
        from permission p
                 LEFT JOIN role_permission rp ON  p.id = rp.permission_id
                 LEFT JOIN employee_role er ON  rp.role_id = er.role_id
        WHERE er.employee_id = #{employeeId}
    </select>

2.5.4 打包遇到的问题,.xml放置在java目录下,而不是resource目录下

pom.xml配置的maven插件

		<resources>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/*</include>
				</includes>
				<filtering>false</filtering>
			</resource>
			<!--把放置在java目录下的.xml文件放置在合适的路径下-->
			<resource>
				<directory>src/main/java</directory>
				<includes>
					<include>**/*.xml</include>
				</includes>
				<filtering>true</filtering>
			</resource>
		</resources>

2.6 自定义Realm

在这里插入图片描述
在这里插入图片描述

//将自定义的realm 由shiroconfig类 交给Spring容器管理
public class MyRealm extends AuthorizingRealm {
    //根据用户名字在数据库查对应的角色出来
    @Autowired
    private EmployeeMapper mapper;
    //员工id查角色
    @Autowired
    private RoleMapper roleMapper;
    //员工id查权限
    @Autowired
    private PermissionMapper permissionMapper;

    //com.pci.shiro01.shiro.MyRealm.doGetAuthenticationInfo()
    //如果上面的方法没有加盐, 就不能配置盐
    //将容器中的配置的凭证匹配器注入给当前的Realm对象
    // 在该Realm中使用指定的凭证匹配器来完成密码匹配的操作
    @Override
    @Autowired
    public  void setCredentialsMatcher(CredentialsMatcher credentialsMatcher){
        super.setCredentialsMatcher(credentialsMatcher);
    }


    //获取认证信息
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户的用户名
        //有的同学该位置有点疑问,灿灿老师说明一下,AuthenticationToken为接口,真正由spring管理的类为 UsernamePasswordToken
        //其中getPrincipal()方法获取的值为用户传进来的 username。
        String username = token.getPrincipal().toString();
        //根据用户的账号去数据库中查询用户的信息
        Employee employee = mapper.selectEmployeeByUsername(username);
        if(employee != null){
            //身份: 类似我们之前在session中共享用户对象
            //凭证: 从数据库中查询出来的密码
            return new SimpleAuthenticationInfo(
                    employee, //身份信息
                    employee.getPassword(), //凭证
                    ByteSource.Util.bytes(employee.getName()), //盐
                    "crmRealm"); //Realm名称
        }
        return null;
    }
    //获取授权信息
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获取到当前登录的用户
        Employee employee = (Employee) principals.getPrimaryPrincipal();
        //如果是超级管理员, 授予其admin的角色和所有权限
        if(employee.getAdmin()){
            info.addRole("admin");
            info.addStringPermission("*:*");
        }else{
            //根据登录用户的id查询到其拥有的所有角色的编码
            List<String> roleSns =  roleMapper.selectRoleSnsByEmpId(employee.getId());
            //根据登录用户的id查询到其拥有的所有权限表达式
            List<String> expressions = permissionMapper.selectExpressionByEmpId(employee.getId());
            //将用户拥有的角色和权限i俺家到授权信息对象中, 供shiro授权校验时使用
            info.addRoles(roleSns);
            info.addStringPermissions(expressions);
        }
        return info;
    }
}

2.7 自定义 FormAuthenticationFilter修改对认证结果的处理

在这里插入图片描述

2,7,1 Json结果封装类, 主要用于返回给前端数据

/**
 *  返回给前端的json结果封装类
 * @author cancan
 * @since 2020-05-17
 */
@Data
public class JsonResult {
    private boolean success = true;
    private String msg;

    public JsonResult() {
    }

    //如果失败的使用封装错误信息
    public void mark(String msg){
        this.success = false;
        this.msg = msg;
    }

    public JsonResult(boolean success, String msg) {
        this.success = success;
        this.msg = msg;
    }
}

2.7.2 修改默认认证过滤器中对认证结果的处理

在这里插入图片描述
该图为认证的过程
在这里插入图片描述

//修改默认认证过滤器中对认证结果的处理
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        response.setContentType("application/json;charset=UTF-8");
        //响应页面中需要的数据
        JsonResult result = new JsonResult();
        response.getWriter().print(JSON.toJSONString(result));
        HttpServletResponse response1 = (HttpServletResponse) response;
        response1.sendRedirect("/login");
        return false;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        response.setContentType("application/json;charset=UTF-8");
        //响应页面中需要的数据
        JsonResult result = new JsonResult();

        String msg = "";
        if (e instanceof UnknownAccountException) {
            msg = "账号错误";
        } else if (e instanceof IncorrectCredentialsException) {
            msg = "密码错误";
        } else {
            msg = "未知错误";
        }
        e.printStackTrace();

        try {
            result.mark(msg);
            response.getWriter().print(JSON.toJSONString(result));
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        return false;
    }
}

2.8 Shiro的spring配置类

2.8.1 Shiro配置类

在这里插入图片描述
前置知识: 默认的自动可用过滤器实例由DefaultFilter enum定义,enum的name字段是可用于配置的名称。它们是:

过滤器名称
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

当运行一个web应用程序时,Shiro会创建一些有用的默认过滤器实例,并使它们在[main]部分自动可用。您可以像配置任何其他bean一样在main中配置它们,并在链定义中引用它们。

/**
 * shiro配置文件
 * @author cancan
 * @since 2020-05-16
 */
@Configuration
public class ShiroConfig {

    //通过配置,设置session过期时间,没有配置默认10分钟过期
    @Bean
    public DefaultWebSessionManager sessionManager(@Value("${server.servlet.session.timeout:10}") long globalSessionTimeout, SessionDAO sessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setSessionValidationInterval(globalSessionTimeout * 60 * 1000);
        sessionManager.setGlobalSessionTimeout(globalSessionTimeout * 60 * 1000);
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        return new MemorySessionDAO();
    }

    //定义ShiroFilter
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //修改默认认证过滤器中对认证结果的处理
        Map<String, javax.servlet.Filter> filters = shiroFilterFactoryBean.getFilters();
        MyFormAuthenticationFilter filter = new MyFormAuthenticationFilter();
        //将默认的登录账户username修改为account
        filter.setUsernameParam("account");
        filters.put("authc", filter);
        shiroFilterFactoryBean.setFilters(filters);
        // 默认跳转到登陆页面,com.cancan.shirospringboot.controller.LoginController.index
        shiroFilterFactoryBean.setLoginUrl("/index");
        // 登陆成功后的页面,com.cancan.shirospringboot.controller.LoginController.login
        shiroFilterFactoryBean.setSuccessUrl("/login");
        // 权限控制map
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不会被拦截的链接 顺序判断
        /*
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须用有了 记住我 功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         */
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/style/", "anon");
        filterChainDefinitionMap.put("/logout.do", "logout");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    //修改默认认证过滤器中对认证结果的处理
    //注入bean, 为了交给shiroFilterFactoryBean 进行管理
    @Bean
    public MyFormAuthenticationFilter myFormAuthenticationFilter(){
        MyFormAuthenticationFilter myFormAuthenticationFilter = new MyFormAuthenticationFilter();
        return myFormAuthenticationFilter;
    }

    //指定使用哪一个安全管理器, 将安全管理器交给Sdpring容器管理器
    //将定义类CRMRealm extends AuthorizingRealm 交给管理
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(MyRealm myRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager);
        securityManager.setRememberMeManager(null);
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    //添加自己的Realm
    @Bean
    public MyRealm crmRealm(){
        MyRealm myRealm = new MyRealm();
        //如果没有添加盐, 就不要加盐验证
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myRealm;
    }

    /**
     *  开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    //开启shiro注解授权
    //使用代理方式;所以需要开启代码支持;
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    //指定当前需要使用的凭证匹配器
    //在CRMRealm进行autowired使用
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //指定加密算法
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(1);
        return hashedCredentialsMatcher;
    }

    //Shiro生命周期处理器
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}

2.8.2 设置登录Controller和登录成功Controller

package com.cancan.shirospringboot.controller;

/**
 * @author cancan
 * @version 1.0
 * @date 2020/5/17 20:06
 */
@RestController
public class LoginController {

    //登录界面
    @RequestMapping("/index")
    public String index(){
        return "请登录";
    }
    
    //登录成功的界面
    @RequestMapping("/login")
    public String login(){
        return "登录成功";
    }
}

2.8.3 认证登录测试

post请求: localhost:8081/index?account=admin&password=1
在这里插入图片描述
退出登录:
post请求: localhost:8081/logout.do
在这里插入图片描述
登录失败,账号错误

localhost:8081/index?account=admin1&password=1
{"msg":"账号错误","success":false}

登录失败,密码错误

localhost:8081/index?account=admin&password=2
{"msg":"密码错误","success":false}

2.9 授权

然后在控制器类上使用shiro提供的种注解来做控制:

注解 功能
@RequiresGuest 只有游客可以访问
@RequiresAuthentication 需要登录才能访问
@RequiresUser 已登录的用户或“记住我”的用户能访问
@RequiresRoles 已登录的用户需具有指定的角色才能访问
@RequiresPermissions 已登录的用户需具有指定的权限才能访问

最开始的sql,我定义了3个用户,每个角色分配角色和权限
在这里插入图片描述

2.10 访问的controller

在这里插入图片描述
添加上认证的注解

注意:Shiro @RequiresRoles注解相关参数说明
@RequiresRoles(value={“admin”,“user”},logical = Logical.OR)
@RequiresPermissions(value={“add”,“update”},logical = Logical.AND)
如果有多个权限/角色验证的时候中间用“,”隔开,默认是所有列出的权限/角色必须同时满足才生效。但是在注解中有logical = Logical.OR这块。这里可以让权限控制更灵活些。

如果将这里设置成OR,表示所列出的条件只要满足其中一个就可以,如果不写或者设置成logical = Logical.AND,表示所有列出的都必须满足才能进入方法。

/**
 * @author cancan
 * @version 1.0
 * @date 2020/5/17 20:06
 */
@RestController
public class LoginController {

    //登录界面
    @RequestMapping("/index")
    public String index(){
        return "请登录";
    }


    //登录成功的界面
    @RequestMapping("/login")
    @ResponseBody
    public String login(){
        return "登录成功";
    }

    @RequiresGuest
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        return "游客你好";
    }

    @RequiresRoles(value = {"admin","SS"},logical = Logical.OR)
    @RequestMapping("/get")
    @ResponseBody
    public String get(){
        return "拥有角色权限登录";
    }

    @RequiresPermissions(value = "js")
    @RequestMapping("/getdist")
    @ResponseBody
    public String getDist(){
        return "拥有近身攻击权限";
    }

}

2.11 统一异常处理

在这里插入图片描述

/**
 * 统一异常处理
 * @author cancan
 * @since 2020-05-16
 */
@ControllerAdvice
public class UnauthorizedExceptionUtil {

    @ExceptionHandler(UnauthorizedException.class)
    public void handler(HttpServletResponse response,
                        HandlerMethod method, UnauthorizedException e)
            throws IOException {
        if(method.getMethod().isAnnotationPresent(ResponseBody.class)){
            response.setContentType("text/json;charset=UTF-8");
            JsonResult result = new JsonResult();
            result.mark("对不起, 您没有权限执行该操作");
            response.getWriter().print(JSON.toJSONString(result));
        }else{
            throw e;
        }
    }
}

2.12 完整流程测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

写在最后

Shrio的用法本身就不是很难,但是还需要注意一些细节。
本文虽然简单,但是对于刚刚接触Shiro的人,还是很有帮助的,如果可以帮助你,请点赞,谢谢

本人长期从事java开发,如果有疑问,请留言

猜你喜欢

转载自blog.csdn.net/qq_34168515/article/details/106151163