SpringBoot 整合 Shiro 安全框架完成用户认证和授权功能

1. Apache Shiro

1.1 Shiro 简介

Apache Shiro 是一个强大且灵活的开源的安全框架,它可以清晰地处理身份验证、授权、企业会话管理和加密。Shiro 开发团队开发出这个框架最主要的目的就是使得安全框架可以易用、易理解。

Shiro 的主要功能是管理应用程序中与安全相关的全部操作,并且尽可能地支持多种实现方法。Shiro 是建立在完善的接口驱动设计和面向对象原则之上的,并支持各种自定义行为。Shiro 提供的默认实现,使得它可以实现与其他的安全框架同样的功能。

Apache Shiro 相当简单,对比 Spring Security, 它可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就已经足够。根据自己的业务需求去选择对应的安全框架,如果并不需要太过于复杂的需求,Shiro 足以满足我们的需求。

1.2 Shiro 功能结构图

Apache Shiro 是一个具有许多功能的综合应用安全框架,下图展示了 Apache Shiro 的强大功能的组织结构:

在这里插入图片描述
下面的表格中,分别对其组织结构的各个部分进行了介绍:

名 称解 释
Authentication身份认证/登录,即验证用户是拥有相应的身份
Authorization授权,即权限验证,验证某个已登录的用户是否拥有某个权限,即判断用户是否能做某个事情。例如:验证某个用户是否拥有某个角色,或者从更细粒度的角度来讲,验证某个用户对某个资源是否具有某个权限
Session Manager会话管理,即用户登录后就是一次会话,在没有退出登录之前,它的所有信息都在会话中;会话可以是在普通 JavaSE 环境中,也可以在 Web 环境中
Cryptography加密,即保护数据的安全,如将密码加密存储到数据库,而不是明文存储
Web SupportWeb 支持,可以非常容易的集成到 Web 环境中
Caching缓存。例如,用户登录后,他的用户信息、拥有的角色/权限不必每次都去查询数据库,直接读取缓存中的信息,这样可以提高效率
Concurrencyshiro 支持多线程应用的并发验证。例如,在一个线程中开启另一个线程,能把权限自动传播过去
Testing提供测试支持
Run As允许一个用户伪装成另一个用户(如果他们允许)的身份进行访问
Remember Me记住我,一次登录后,下次再访问的话就不用登录了

值得注意的是,Shiro 不会去维护用户角色以及角色拥有的权限。这些需要我们自己去设计、提供,然后通过相应的接口注入到 Shiro.

1.3 Shiro 外部结构

了解了 Shiro 强大的功能之后,接下来就要了解 Apache Shiro 的外部架构,也就是说从应用程序的角度观察,如何去使用 Shiro. 下图展示了 Apache Shiro 的外部架构:
在这里插入图片描述
从上图中,可以看到,和应用代码直接交互的是 Subject 对象,也就是 Shiro 对外的 API 核心是 Subject. 下面对架构图中每个对象的含义进行了解释:

1. Subject: 主体,代表了当前“用户”。与当前应用交互的任何对象都是一个 Subject, 譬如网络爬虫,机器人等。它是一个抽象的概念,所有的 Subject 都需要绑定到 SecurityManager 中, SecurityManager 管理着与 Subject 所有的交互操作。Subject 相当于一个门面,实际的执行者还是 SecurityManager.
2. SecurityManager: 安全管理器,所有与安全相关的操作都会与 SecurityManager 交互,同时它还管理所有 Subject。它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,可以把它看成 SpringMVC 框架中的前端控制器 DispatcherServlet.
3. Realm: 域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),也就是说 SecurityManager 要验证用户身份,它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法。同时还需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作,可以把 Realm 看成 DataSource, 即安全数据源。

总之,对于使用者而言,它的外部结构大致就是:

  1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager.
  2. 我们需要给 Shiro 的 SecurityManager 注入 Realm, 从而让 SecurityManager 能得到合法的用户及其权限进行判断。

1.4 Shiro 内部结构

看完 Shiro 的外部结构之后,再来了解 Shiro 的内部结构是怎么样的,下图展示了 Shiro 的内部结构:
在这里插入图片描述
通过上图可以发现,内部结构图中的组件也是 Shiro 功能图中的组件,只是内部结构组件更加的完善,下面的表格中给出了各个组件对应的解释,与功能结构图中重复的地方就当是再复习巩固一下。

名 称解 释
Subject主体,即与应用进行交互的任何”用户“
SecurityManager相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher, 是 Shiro 的心脏。所有具体的交互都是通过 SecurityManager 进行控制,它管理着所有 Subject 且负责进行认证和授权以及会话和缓存的管理。
Authenticator认证器,负责主体认证,它是可扩展的。如果用户觉得 Shiro 默认的不能满足需求,可以自定义实现,它需要认证策略 (Authentication Strategy), 即在何种情况下算用户认证通过
Authrizer授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能
Realm可以有 1 个或多个 Realm, 可以认为是安全实体数据源,即用于获取安全实体。可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等。值得注意的是,它需要由用户提供,Shiro 不知道你的用户/权限存储在何处及以何种格式存储,因此一般在应用中都需要实现自己的 Realm
SessionManager会话管理器。由于 Shiro 不仅可以用在 Web 环境中,还可以用在普通的 JavaSE 环境、EJB等环境中。因此,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据。如我们在 Web 环境中,刚开始是一台 Web 服务器,接着又上了台 EJB 服务器,如果想把两台服务器的会话数据放到一个地方,这时就需要实现自己的分布式会话(例如可以把数据放到 Memcached 服务器)
SessionDAO会话的数据访问对象,用于会话的 CRUD。当我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO, 可以通过 JDBC 写到数据库。此外 SessionDAO 中可以使用 Cache 进行缓存,以此来提高性能
CacheManager缓存控制器,来管理用户、角色、权限等的缓存,由于这些数据基本上很少去改变,因此放到缓存中,可以提高访问的性能
Cryptography密码模块,Shiro 提供了一些常见的加密组件,用于密码加密/解密

2. 案例实现

看完了上面关于 Shiro 的介绍,下面动手去实现一个案例,在 SpringBoot 中 整合 Shiro 安全框架来完成用户认证和授权功能。

2.1 sql 文件代码

首先,给出数据库文件的代码。在数据库中,有三个主要表,分别为用户表、角色表、权限表, 即 pe_user、pe_role、pe_pemission. 这三个表的关系分别为,用户与角色多对多关联、角色与权限多对多关联。因此,又产生了两个关联表,在数据库中分别为 pe_user_role 和 pe_role_permission.

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 80022
 Source Host           : localhost:3306
 Source Schema         : shiro_db

 Target Server Type    : MySQL
 Target Server Version : 80022
 File Encoding         : 65001

 Date: 19/12/2021 20:38:54
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for pe_permission
-- ----------------------------
DROP TABLE IF EXISTS `pe_permission`;
CREATE TABLE `pe_permission`  (
  `id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限名称',
  `code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '权限描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of pe_permission
-- ----------------------------
INSERT INTO `pe_permission` VALUES ('1', '添加用户', 'user-add', NULL);
INSERT INTO `pe_permission` VALUES ('2', '查询用户', 'user-find', NULL);
INSERT INTO `pe_permission` VALUES ('3', '更新用户', 'user-update', NULL);
INSERT INTO `pe_permission` VALUES ('4', '删除用户', 'user-delete', NULL);
INSERT INTO `pe_permission` VALUES ('5', '访问主页', 'user-home', NULL);

-- ----------------------------
-- Table structure for pe_role
-- ----------------------------
DROP TABLE IF EXISTS `pe_role`;
CREATE TABLE `pe_role`  (
  `id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '主键ID',
  `name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限名称',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '说明',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `UK_k3beff7qglfn58qsf2yvbg41i`(`name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of pe_role
-- ----------------------------
INSERT INTO `pe_role` VALUES ('1', '系统管理员', '系统日常维护');
INSERT INTO `pe_role` VALUES ('2', '普通员工', '普通操作权限');

-- ----------------------------
-- Table structure for pe_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `pe_role_permission`;
CREATE TABLE `pe_role_permission`  (
  `role_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色ID',
  `permission_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`role_id`, `permission_id`) USING BTREE,
  INDEX `FK74qx7rkbtq2wqms78gljv87a0`(`permission_id`) USING BTREE,
  INDEX `FKee9dk0vg99shvsytflym6egxd`(`role_id`) USING BTREE,
  CONSTRAINT `fk-p-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `fk-pid` FOREIGN KEY (`permission_id`) REFERENCES `pe_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of pe_role_permission
-- ----------------------------
INSERT INTO `pe_role_permission` VALUES ('1', '1');
INSERT INTO `pe_role_permission` VALUES ('1', '2');
INSERT INTO `pe_role_permission` VALUES ('2', '2');
INSERT INTO `pe_role_permission` VALUES ('1', '3');
INSERT INTO `pe_role_permission` VALUES ('1', '4');
INSERT INTO `pe_role_permission` VALUES ('1', '5');

-- ----------------------------
-- Table structure for pe_user
-- ----------------------------
DROP TABLE IF EXISTS `pe_user`;
CREATE TABLE `pe_user`  (
  `id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'ID',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名称',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of pe_user
-- ----------------------------
INSERT INTO `pe_user` VALUES ('1', 'hzz', '702650a13cfda40e6cdc3aa1a587f1ed');
INSERT INTO `pe_user` VALUES ('2', 'huazaizai', '8680322d3f0d1a1a5ff2059346aa2c5b');

-- ----------------------------
-- Table structure for pe_user_role
-- ----------------------------
DROP TABLE IF EXISTS `pe_user_role`;
CREATE TABLE `pe_user_role`  (
  `role_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色ID',
  `user_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`role_id`, `user_id`) USING BTREE,
  INDEX `FK74qx7rkbtq2wqms78gljv87a1`(`role_id`) USING BTREE,
  INDEX `FKee9dk0vg99shvsytflym6egx1`(`user_id`) USING BTREE,
  CONSTRAINT `fk-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `fk-uid` FOREIGN KEY (`user_id`) REFERENCES `pe_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of pe_user_role
-- ----------------------------
INSERT INTO `pe_user_role` VALUES ('1', '1');

SET FOREIGN_KEY_CHECKS = 1;

2.2 maven 工程 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hzz</groupId>
    <artifactId>shiro_springboot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <fastjson.version>1.2.47</fastjson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- shiro 与 spring 整合 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
    <build>
        <plugins>
            <!--编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>

            <!--单元测试插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2.3 domain 包下的实体对象

2.3.1 编写用户对象 User

package com.hzz.shiro.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

/**
 * 用户实体类
 */
@Entity
@Table(name = "pe_user")
@Getter
@Setter
public class User implements Serializable {
    private static final long serialVersionUID = 4297464181093070302L;
    /**
     * ID
     */
    @Id
    private String id;
    private String username;
    private String password;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name="pe_user_role",joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")},
            inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
    )
    private Set<Role> roles = new HashSet<Role>();//用户与角色   多对多
}

2.3.2 编写角色对象 Role

package com.hzz.shiro.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "pe_role")
@Getter
@Setter
public class Role implements Serializable {
    private static final long serialVersionUID = 594829320797158219L;
    @Id
    private String id;
    private String name;
    private String description;

    //角色与用户   多对多
    @ManyToMany(mappedBy="roles")
    private Set<User> users = new HashSet<User>(0);

    //角色与权限  多对多
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name="pe_role_permission",
            joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
            inverseJoinColumns={@JoinColumn(name="permission_id",referencedColumnName="id")})
    private Set<Permission> permissions = new HashSet<Permission>(0);
}

2.3.3 编写权限对象 Permission

package com.hzz.shiro.domain;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name = "pe_permission")
@Getter
@Setter
@NoArgsConstructor
public class Permission implements Serializable {
    private static final long serialVersionUID = -4990810027542971546L;
    /**
     * 主键
     */
    @Id
    private String id;
    private String name;
    private String code;
    private String description;
}

2.4 dao 包下的接口

2.4.1 编写用户接口 UserDao

因为使用的 SpringData JPA 作为数据访问层框架,因此直接继承 JpaRepository 类,可以让框架去编写 sql 语句。

package com.hzz.shiro.dao;

import com.hzz.shiro.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
  * 用户数据访问接口
  */
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
    //根据手机号获取用户信息
    User findByUsername(String name);
}

2.5 service 包下的服务类

2.5.1 编写服务对象 UserService

package com.hzz.shiro.service;

import com.hzz.shiro.dao.UserDao;
import com.hzz.shiro.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public User findByName(String name) {
        return this.userDao.findByUsername(name);
    }

    public List<User> findAll() {
        return userDao.findAll();
    }
}

2.6 controller 包下控制类

2.6.1 编写控制对象 UserController

package com.hzz.shiro.controller;

import com.hzz.shiro.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    // 个人主页
    // 使用shiro注解鉴权
    // @RequiresPermissions()  -- 访问此方法必须具备的权限
    // @RequiresRoles()   --- 访问此方法必须具备的角色

    /**
     * 1. 过滤器,如果权限信息不匹配 setUnauthorizedUrl 地址
     * 2. 注解,如果权限信息不匹配,抛出异常
     * @return
     */

    @RequiresPermissions("user-home")
    @RequestMapping(value = "/user/home")
    public String home() {
        return "访问个人主页成功";
    }

    //添加
    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String add() {
        return "添加用户成功";
    }


    //查询
    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String find() {
        return "查询用户成功";
    }
	
    //更新
    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    public String update(String id) {
        return "更新用户成功";
    }
	
    //删除
    @RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
    public String delete() {
        return "删除用户成功";
    }

    /**
     * 1. 传统登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 程序员手动控制
     * 2. shiro 登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 通过 subject, login => realm 域的认证方法 =>
     * @param username
     * @param password
     * @return
     */
	// 用户登录
	@RequestMapping(value="/login")
    public String login(String username,String password) {
	    // 构造登录令牌
        try {
            /**
             * 密码加密
             *      shiro 提供的 md5 加密
             *      Md5Hash
             *          参数一:加密的内容
             *                  111111   ---- abcd
             *          参数二:盐(加密的混淆字符串) (用户登录的用户名)
             *                  111111 + 混淆字符串
             *          参数三:加密次数
             */
            password = new Md5Hash(password, username, 3).toString();
            UsernamePasswordToken upToken = new UsernamePasswordToken(username, password);
            // 1. 获取 subject
            Subject subject = SecurityUtils.getSubject();
            // 2. 调用 subject 进行登录
            subject.login(upToken);
            return "登录成功";
        } catch (Exception e) {
            return "用户名或密码错误";
        }
    }

    @RequestMapping(value = "/autherror")
    public String autherror(int code) {
	    return code == 1 ? "未登录":"未授权";
    }
}

2.7 realm 包下对象

2.7.1 编写自定义域 CustomRealm

package com.hzz.shiro.realm;

import com.hzz.shiro.domain.Permission;
import com.hzz.shiro.domain.Role;
import com.hzz.shiro.domain.User;
import com.hzz.shiro.service.UserService;
import org.apache.shiro.authc.*;
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.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Set;

public class CustomRealm extends AuthorizingRealm {

    public void setName() {
        super.setName("customRealm");
    }

    @Autowired
    private UserService userService;

    /**
     * 授权方法
     *      操作的时候,判断用户是否具有响应的权限
     *          先认证   --- 安全数据
     *          再授权   --- 根据安全数据获取用户具有的所有操作权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1. 获取已认证的用户数据
        User user = (User) principalCollection.getPrimaryPrincipal();      // 得到唯一的安全数据
        // 2. 根据用户数据获取用户的权限信息(所有角色,所有权限)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet<>();       // 所有角色
        Set<String> perms = new HashSet<>();       // 所有权限
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
            for (Permission perm : role.getPermissions()) {
                perms.add(perm.getCode());
            }
        }
        info.setStringPermissions(perms);
        info.setRoles(roles);
        return info;
    }

    /**
     * 认证方法
     *      参数:传递的用户名密码
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. 获取登录的用户名密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        // 2. 根据用户名查询数据库
        User user = userService.findByName(username);
        // 3. 判断用户是否存在一致或者密码是否一致
        if (user != null && user.getPassword().equals(password)) {
            // 4. 如果一致返回安全数据
            // 构造方法,安全数据,密码,realm 域名
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
            return info;
        } else {
            // 5. 不一致,返回 null (抛出异常)
            return null;
        }
    }
}

2.8 exception 包下的异常处理类

2.8.1 编写异常类 BaseExceptionHandler

package com.hzz.shiro.exception;

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义的公共异常处理
 *      1. 声明异常处理器
 *      2. 对异常统一处理
 */
@ControllerAdvice
public class BaseExceptionHandler {

    @ExceptionHandler(value = AuthorizationException.class)
    @ResponseBody
    public String error(HttpServletRequest request, HttpServletResponse response, AuthorizationException authorization) {
        return "未授权";
    }
}

2.9 config 包下对象

2.9.1 编写 Shiro 配置类 ShiroConfiguration

package com.hzz.shiro.config;

import com.hzz.shiro.realm.CustomRealm;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfiguration {
    // 1. 创建 realm
    @Bean
    public CustomRealm getRealm() {
        return new CustomRealm();
    }
    // 2. 创建安全管理器
    @Bean
    public SecurityManager getSecurityManager(CustomRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
        securityManager.setRealm(realm);
        return securityManager;
    }
    // 3. 配置 shiro 的过滤器工厂
    /**
     * 在 web 工程中,shiro 进行权限控制都是通过一组过滤器集合进行控制
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        // 1. 创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        // 2. 设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        // 3. 通用配置(跳转登录页面,未授权跳转的页面)
        filterFactory.setLoginUrl("/autherror?code=1"); // 跳转到url地址
        filterFactory.setUnauthorizedUrl("/autherror?code=2");  // 未授权跳转的页面
        // 4. 设置过滤器集合
        /**
         * 设置所有过滤器, 有顺序map
         *      key = 拦截的 url 地址
         *      value = 过滤器类型
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
//        filterMap.put("/user/home", "anon");  // 当前请求地址可以匿名访问

        filterMap.put("/user/**", "authc");   // 当前请求地址必须认证之后才可以访问
        filterFactory.setFilterChainDefinitionMap(filterMap);
        return filterFactory;
    }

    // 4. 开启对 shiro 注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

2.10 resources 文件夹下资源文件

2.10.1 application.yml

server:
  port: 8081
spring:
  application:
    name: shiro-test # 指定服务名
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro_db?useUnicode=true&characterEncoding=utf8
    username: root
    password: 123123
  jpa:
    database: MySQL
    show-sql: true
    open-in-view: true

2.11 项目启动类

2.11.1 项目启动对象 ShiroApplication

package com.hzz.shiro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;

@SpringBootApplication(scanBasePackages = "com.hzz")
@EntityScan("com.hzz.shiro.domain")
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
    @Bean
    public OpenEntityManagerInViewFilter openEntityManagerInViewFilter() {
        return new OpenEntityManagerInViewFilter();
    }
}

3. 测试

搭建完工程之后,就可以着手去测试 Shiro 框架的用户认证和授权功能了。运行 SprigBoot 的启动类。

3.1 认证测试

要对某个接口进行测试,必然涉及到后端的控制类里面的方法进行处理,对于测试登录功能来说,它的请求是发送给 UserController 里面的方法进行处理的,它的代码段如下:

 /**
     * 1. 传统登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 程序员手动控制
     * 2. shiro 登录
     *      前端发送登录请求 => 接口部分获取用户名密码 => 通过 subject, login => realm 域的认证方法 =>
     * @param username
     * @param password
     * @return
     */
	// 用户登录
	@RequestMapping(value="/login")
    public String login(String username,String password) {
	    // 构造登录令牌
        try {
            /**
             * 密码加密
             *      shiro 提供的 md5 加密
             *      Md5Hash
             *          参数一:加密的内容
             *                  111111   ---- abcd
             *          参数二:盐(加密的混淆字符串) (用户登录的用户名)
             *                  111111 + 混淆字符串
             *          参数三:加密次数
             */
            password = new Md5Hash(password, username, 3).toString();
            UsernamePasswordToken upToken = new UsernamePasswordToken(username, password);
            // 1. 获取 subject
            Subject subject = SecurityUtils.getSubject();
            // 2. 调用 subject 进行登录
            subject.login(upToken);
            return "登录成功";
        } catch (Exception e) {
            return "用户名或密码错误";
        }
    }

在该方法中,用到了 Subject 对象,调用了 login 方法。这个时候就交给了 SecurityManager 安全管理器,管理 Subject, 所以在 ShiroConfiguration 类中创建一个对象

 // 创建安全管理器
    @Bean
    public SecurityManager getSecurityManager(CustomRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
        securityManager.setRealm(realm);
        return securityManager;
    }

之后,进入到自定义的 CustomRealm, 对登录认证进行处理

 /**
     * 认证方法
     *      参数:传递的用户名密码
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1. 获取登录的用户名密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        // 2. 根据用户名查询数据库
        User user = userService.findByName(username);
        // 3. 判断用户是否存在一致或者密码是否一致
        if (user != null && user.getPassword().equals(password)) {
            // 4. 如果一致返回安全数据
            // 构造方法,安全数据,密码,realm 域名
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
            return info;
        } else {
            // 5. 不一致,返回 null (抛出异常)
            return null;
        }
    }

之后,再使用测试工具进行测试。
首先,测试一下认证功能。打开 Postman 测试工具,对登录接口进行测试。
输入请求链接 localhost:8081/login?username=hzz&password=123456,用户名和密码都是预存到数据库中的,用户名为 hzz, 密码的明文为 123456.
在这里插入图片描述
可以看到,输入正确的用户名和密码之后,发送请求成功后会返回一个 ”登录成功“ 提示。之后,再使用错误的密码进行测试,例如密码可以是 12345, 再次发送请求。

在这里插入图片描述
可以看到,密码错误时,返回一个 ”用户名或密码“ 错误提示。这便是测试使用 Shiro 安全框架提供的认证功能。

3.2 授权测试

对认证功能进行测试之后,再来测试一下授权功能。在用户控制类中的 home 方法上,加上注解 @RequiresPermissions("user-home"), 也就说调用这个方法需要用户角色具有 user-home 权限,没有这个权限,将不能调用。

 // 个人主页
    // 使用shiro注解鉴权
    // @RequiresPermissions()  -- 访问此方法必须具备的权限
    // @RequiresRoles()   --- 访问此方法必须具备的角色

    /**
     * 1. 过滤器,如果权限信息不匹配 setUnauthorizedUrl 地址
     * 2. 注解,如果权限信息不匹配,抛出异常
     * @return
     */
    @RequiresPermissions("user-home")
    @RequestMapping(value = "/user/home")
    public String home() {
        return "访问个人主页成功";
    }

在访问该接口之前,需要先认证登录成功。这样 Session 才会有相应的用户角色、权限信息。我们先使用用户名为 hzz, 密码为 123456 进行测试,因为该用户具有访问主页的权限,具体的权限可以查看数据记录。
在这里插入图片描述
可以看到,返回一个提示 ”访问个人主页成功“,表示授权访问个人主页。
之后,再使用另一个账号进行测试,用户名为 huazaizai, 密码为 123456, 该用户不具有 user-home 权限,预期的结果应该是提示 ”未授权“。
在这里插入图片描述
可以看到当使用未分配访问主页权限的用户进行测试时,Shiro 框架能够帮我们成功拦截,并进行授权验证。

4. 总结

总的来说,Shiro 是一个简单易用的安全框架,相比于 Spring Security 它的配置更加容易理解。Shiro 对很多其他的框架兼容性更好,号称是无缝集成。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReadThroughLife

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值