前后端分离简单项目--蚂蚁博客--后端部分

原文网址:前后端分离简单项目--蚂蚁博客--后端部分_IT利刃出鞘的博客-CSDN博客

简介

说明

        本文介绍我从0开发的前后端分离的简单项目--蚂蚁博客。本博文介绍后端部分。

        本项目是一个全栈项目,使用主流、前沿的技术栈开发,项目虽小,五脏俱全。

        后期我会出一个视频,详细讲解本项目。视频录完后会将链接贴到本文。

项目介绍

见:前后端分离简单项目--蚂蚁博客--简介_IT利刃出鞘的博客-CSDN博客

项目源码

gitee地址:https://gitee.com/knifeedge/ant_blog

ant_backend目录是后端部分的代码。

技术栈

  1. SpringBoot(spring-boot-starter-parent:2.4.2)
  2. MyBatis-Plus(mybatis-plus-boot-starter:3.4.3.1)
  3. MySQL(8.0.21)
  4. Shiro(shiro-redis-spring-boot-starter:3.3.1)
  5. JWT(java-jwt:3.18.1)
  6. knife4j(swagger的升级版)(knife4j-spring-boot-starter:3.0.3)
  7. hutool(hutool-all:5.5.7)
  8. lombok(lombok(版本由spring-boot-starter-parent指定))

技术栈简介

  1. MyBatis-Plus
    1. 主流、开发效率最高的持久层框架。秒杀Mybatis、JPA
    2. 相关教程:
      1. MyBatis-Plus--使用_IT利刃出鞘的博客-CSDN博客
      2. MyBatis-Plus--分页--方法/教程/实例_IT利刃出鞘的博客-CSDN博客
  2. MySQL
  3. Shiro
    1. 主流、便利的权限管理框架
    2. 相关教程。 shiro用法我写了一个系列,有如下文章:
      1. Shiro--用Session控制权限--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      2. Shiro--整合shiro-redis--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      3. Shiro--整合jwt--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
      4. Shiro--整合jwt--通过url路径控制权限--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
  4. JWT
    1. 主流的token生成、校验工具
    2. 相关教程:
      1. JWT--使用/教程/实例_IT利刃出鞘的博客-CSDN博客_jwt使用教程
  5. knife4j(swagger的升级版)
    1. 主流的接口管理工具
    2. 相关教程:
      1. Knife4j--使用/教程/实例/配置_IT利刃出鞘的博客-CSDN博客_knife4j
  6. hutool
    1. 主流的工具包
  7. lombok

项目结构

概述

        项目结构清晰。

  1. 业务部分(business包):按模块进行划分;
    1. 这样的结构是最好的,单个模块的代码都在一处,便于查找,也便于模块化。
    2. 不推荐的做法:所有业务的controller放一个包,所有业务的service放一个包...。原因:不利于模块化;而且一旦模块很多,开发过程中我只关注某个模块,但显示时会很长,影响开发效率
  2. 公共部分(common包):全局处理、常量、工具类
  3. 配置部分(config包):配置类

结构概览

建库建表

DROP DATABASE IF EXISTS blog;
CREATE DATABASE blog DEFAULT CHARACTER SET utf8;
USE blog;

DROP TABLE IF EXISTS `t_blog`;
DROP TABLE IF EXISTS `t_user`;
SET NAMES utf8mb4;

CREATE TABLE `t_blog`
(
    `id`           BIGINT(0) NOT NULL AUTO_INCREMENT,
    `user_id`      BIGINT(0) NOT NULL,
    `user_name`    VARCHAR(64) NOT NULL,
    `title`        VARCHAR(256) CHARACTER SET utf8mb4 NOT NULL COMMENT '标题',
    `description`  VARCHAR(256) CHARACTER SET utf8mb4 NOT NULL COMMENT '摘要',
    `content`      LONGTEXT CHARACTER SET utf8mb4 NOT NULL COMMENT '内容',
    `status`       INT(0) NOT NULL DEFAULT 0 COMMENT '0:正常。 1:正在审核。2:已删除',
    `create_time`  DATETIME(0),
    `update_time`  DATETIME(0),
    `deleted_flag` BIGINT(0) NOT NULL DEFAULT 0 COMMENT '0:未删除 其他:已删除',
    PRIMARY KEY (`id`) USING BTREE,
    KEY `index_user_id`(`user_id`),
    KEY `index_user_name`(`user_name`),
    KEY `index_create_time`(`create_time`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '博客';

CREATE TABLE `t_user`
(
    `id`              BIGINT(0) NOT NULL AUTO_INCREMENT,
    `user_name`       VARCHAR(64) NOT NULL,
    `password`        VARCHAR(64) NOT NULL,
    `salt`            VARCHAR(64) NOT NULL,
    `nick_name`       VARCHAR(64) NOT NULL,
    `avatar_url`      VARCHAR(256) NOT NULL,
    `email`           VARCHAR(64),
    `status`          INT(0) NOT NULL DEFAULT 0 COMMENT '0:正常 1:被锁定',
    `last_login_time` DATETIME(0),
    `create_time`     DATETIME(0) NOT NULL,
    `update_time`     DATETIME(0) NOT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    KEY `index_user_name`(`user_name`),
    KEY `index_create_time`(`create_time`)
) ENGINE = InnoDB COMMENT = '用户';

-- 账号:knife,密码:222333   密码会加盐,两次md5之后进行保存
INSERT INTO `t_user` VALUES (1, 'knife', 'e4b8c8e43f8fabbe08d5aa67d58068ac', 'sCPKiMOoEl2ecPsfFhClcg==',
                             '刀刃', 'https://i.postimg.cc/T2Cn6r8y/IOHWn0.png',
                             NULL, 0, NULL, '2021-01-23 09:33:36', '2021-01-23 09:33:36');

-- 账号:sky,密码:123456    密码会加盐,两次md5之后进行保存
INSERT INTO `t_user` VALUES (2, 'sky', '49b3146badc6479f5d6c306994a4a33e','+SyMx8kT2CnKf6K2l3IH8g==',
                             '天蓝', 'https://i.postimg.cc/Hn91nMcj/image.jpg',
                             NULL, 0, NULL, '2021-01-10 22:01:05', '2021-01-10 22:01:05');

INSERT INTO `t_blog` VALUES (1, 1, 'knife', 'Java中枚举的用法',
                             '本文介绍Java的枚举类的使用,枚举一般用于定义一些常量。枚举类完全单例、线程安全;性能高,常量值地址唯一,可以用==直接对比;不需重新编译引用类。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类。',
                             '本文介绍Java的枚举类的使用,枚举一般用于定义一些常量。枚举类完全单例、线程安全;性能高,常量值地址唯一,可以用==直接对比;不需重新编译引用类。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类。',
                             0, '2021-01-23 11:33:36', '2021-01-23 11:33:36', 0);
INSERT INTO `t_blog` VALUES (2, 1, 'knife', 'Java中泛型的用法',
                             '本文介绍Java的泛型的使用。一些框架的源码中经常看到泛型,学习泛型可以帮助我们更好的阅读框架源码、理解框架,也可以提高编程水平',
                             '本文介绍Java的泛型的使用。一些框架的源码中经常看到泛型,学习泛型可以帮助我们更好的阅读框架源码、理解框架,也可以提高编程水平',
                             0, '2021-01-28 23:37:37', '2021-01-28 23:37:37', 0);
INSERT INTO `t_blog` VALUES (3, 1, 'knife', 'Java的HashMap的原理',
                             '本文介绍Java的HashMap的原理。HashMap实际上是一个大的数组,key是数组的下标,value是数组的值。如果产生了哈希冲突,HashMap使用链地址法解决。JDK8中引入了红黑树,当同一个key上边大于8个元素时,链表会转化为红黑树,提高性能',
                             '本文介绍Java的HashMap的原理。HashMap实际上是一个大的数组,key是数组的下标,value是数组的值。如果产生了哈希冲突,HashMap使用链地址法解决。JDK8中引入了红黑树,当同一个key上边大于8个元素时,链表会转化为红黑树,提高性能',
                             0, '2021-05-28 09:06:06', '2021-05-28 09:06:06', 0);
INSERT INTO `t_blog` VALUES (4, 1, 'knife', 'Java中BigDecimal的用法',
                             '本文介绍Java的BigDecimal的使用。BigDecimal处理数字很精确,而且可以表示大于16位的数。相比而言,double只能处理16位以内的数而且计算时不精确。比较经典的使用场景是金额',
                             '本文介绍Java的BigDecimal的使用。BigDecimal处理数字很精确,而且可以表示大于16位的数。相比而言,double只能处理16位以内的数而且计算时不精确。比较经典的使用场景是金额',
                             0, '2021-06-24 20:36:54', '2021-06-24 20:36:54', 0);
INSERT INTO `t_blog` VALUES (5, 1, 'knife', 'Java中反射的用法',
                             '本文介绍Java的反射的使用。反射一般用于通过Class获得实例、调用方法等',
                             '本文介绍Java的反射的使用。反射一般用于通过Class获得实例、调用方法等',
                             0, '2021-10-28 22:24:18', '2021-10-28 22:24:18', 0);
INSERT INTO `t_blog` VALUES (6, 1, 'knife', 'Java的ArrayList保证线程安全的方法',
                             'ArrayList不是线程安全的,也就是说:多个线程操作同一个ArrayList的时候会出现问题。有多种方法可以保证线程安全:Collections.synchronizedList(List)、JUC中的CopyOnWriteArrayList、Vector。推荐使用前两种,第三种性能很差,不推荐使用',
                             'ArrayList不是线程安全的,也就是说:多个线程操作同一个ArrayList的时候会出现问题。有多种方法可以保证线程安全:Collections.synchronizedList(List)、JUC中的CopyOnWriteArrayList、Vector。推荐使用前两种,第三种性能很差,不推荐使用',
                             0, '2021-08-28 21:31:20', '2021-08-28 21:31:20', 0);
INSERT INTO `t_blog` VALUES (7, 1, 'knife', 'SpringBoot启动的流程',
                             '本文介绍SpringBoot启动的流程。分析Spring的启动流程有多种方法:1.构造一个AnnotationConfigApplicationContext对象,调用它的getBean(xxx.class)方法; 2.直接分析SpringBoot的启动流程。本文直接分析SpringBoot的启动流程。本文分析的版本:SpringBoot版本:2.3.0.RELEASE(其对应Spring:5.2.6.RELEASE)。',
                             '本文介绍SpringBoot启动的流程。分析Spring的启动流程有多种方法:1.构造一个AnnotationConfigApplicationContext对象,调用它的getBean(xxx.class)方法; 2.直接分析SpringBoot的启动流程。本文直接分析SpringBoot的启动流程。本文分析的版本:SpringBoot版本:2.3.0.RELEASE(其对应Spring:5.2.6.RELEASE)。',
                             0, '2021-09-25 19:02:55', '2021-09-25 19:02:55', 0);
INSERT INTO `t_blog` VALUES (8, 1, 'knife', 'ArrayList扩容原理',
                             '本文介绍Java的ArrayList扩容的原理。直接new 一个ArrayList对象时(未指定初始容量大小)是一个空的数组,容量大小为零。当第一次调用ArrayList对象的add方法时,分配容量大小',
                             '本文介绍Java的ArrayList扩容的原理。直接new 一个ArrayList对象时(未指定初始容量大小)是一个空的数组,容量大小为零。当第一次调用ArrayList对象的add方法时,分配容量大小',
                             0, '2021-10-29 22:30:32',
                             '2021-10-29 22:30:32', 0);
INSERT INTO `t_blog` VALUES (9, 1, 'knife', 'Java的类加载流程',
                             '本文介绍Java的类加载流程。Java的类加载流程为:加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载。加载:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存(不一定在堆,对于HotSpot在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。',
                             '本文介绍Java的类加载流程。Java的类加载流程为:加载=> 链接(验证+准备+解析)=> 初始化=> 使用=> 卸载。加载:通过一个类的全限定名获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存(不一定在堆,对于HotSpot在方法区)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。',
                             0, '2021-10-29 23:03:05', '2021-10-29 23:03:05', 0);
INSERT INTO `t_blog` VALUES (10, 1, 'knife', 'SpringBoot整合RabbitMQ',
                             'SpringBoot引入RabbitMQ依赖,生产者调用发送方法,消费者进行订阅',
                             'SpringBoot引入RabbitMQ依赖,生产者调用发送方法,消费者进行订阅',
                             0, '2021-10-15 21:38:59', '2021-10-15 21:38:59', 0);
INSERT INTO `t_blog` VALUES (11, 1, 'knife', 'ElasticSearch复杂查询的方法',
                             '本文介绍如何使用ElasticSearch进行复杂查询。ElasticSearch使用布尔查询进行复杂查询。布尔查询的所有子查询之间的逻辑关系是与(and):只有当一个文档满足布尔查询中的所有子查询条件时,ElasticSearch引擎才认为该文档满足查询条件。',
                             '本文介绍如何使用ElasticSearch进行复杂查询。ElasticSearch使用布尔查询进行复杂查询。布尔查询的所有子查询之间的逻辑关系是与(and):只有当一个文档满足布尔查询中的所有子查询条件时,ElasticSearch引擎才认为该文档满足查询条件。对于单个子句,只要一个文档满足该子句的查询条件,返回的逻辑结果就是true。对于should子句,它一般包含多个子查询条件,参数 minimum_should_match 控制文档必须满足should子句中的子查询条件的数量,只有当文档满足 minimum_should_match 时,should子句返回的逻辑结果才是true。',
                             0, '2021-03-28 18:55:01', '2021-03-28 18:55:01', 0);
INSERT INTO `t_blog` VALUES (12, 1, 'knife', 'Kafka如何保证消息不丢失',
                             '本文介绍保证Kafka消息不丢失的方案。Kafka在生产者、服务器、消费者三个地方都可能导致消息丢失。',
                             '本文介绍保证Kafka消息不丢失的方案。Kafka在生产者、服务器、消费者三个地方都可能导致消息丢失。',
                             0, '2021-10-18 23:00:08', '2021-10-18 23:00:08', 0);
INSERT INTO `t_blog` VALUES (13, 1, 'knife', 'Java的CAS的原理',
                             '本文介绍Java的CAS的原理。CAS是多线程的基础,含义是:Compare And SetCAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作,可以说CAS是整个JUC(java.util.concurrent)的基石。CAS性能很高,适合于高并发场景。',
                             '本文介绍Java的CAS的原理。CAS是多线程的基础,含义是:Compare And SetCAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作,可以说CAS是整个JUC(java.util.concurrent)的基石。CAS性能很高,适合于高并发场景。',
                             0, '2021-07-26 19:59:10', '2021-07-26 19:59:10', 0);
INSERT INTO `t_blog` VALUES (14, 1, 'knife', 'Spring的AOP的原理',
                             '本文介绍Spring的AOP的原理。Spring的AOP是通过动态代理来实现的。',
                             '本文介绍Spring的AOP的原理。Spring的AOP是通过动态代理来实现的。',
                             0, '2021-08-28 20:59:58', '2021-08-28 20:59:58', 0);


INSERT INTO `t_blog` VALUES (15, 2, 'sky', 'Vue-cli的使用',
                             'Vue-cli是Vue的一个脚手架工具',
                             'Vue-cli可以用来创建vue项目',
                             0, '2021-02-23 11:34:36', '2021-02-25 14:33:36', 0);
INSERT INTO `t_blog` VALUES (16, 2, 'sky', 'Vuex的用法',
                             'Vuex是vue用于共享变量的插件',
                             '一般使用vuex来共享变量',
                             0, '2021-03-28 23:37:37', '2021-03-28 23:37:37', 0);

依赖

pom.xml

<?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.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>blog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>blog</name>
    <description>Blog project for Spring Boot</description>

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

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

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

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.1</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

application.yml

server:
  port: 9000

spring:
  application:
    name: blog
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/blog?useUnicode=true&characterEncoding=utf8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 222333

#mybatis-plus配置控制台打印完整带参数SQL语句
#mybatis-plus:
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

shiro:
  enabled: true       # 开启 shiro,默认为 true
  web:
    enabled: true     # 开启 shiro Web,默认为 true

custom:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效时长,7天。单位:秒
    expire: 604800

  user:
    defaultAvatarUrl: https://i.postimg.cc/m2wGXNXk/image.png

业务部分

本处展示博客模块、登录模块、登出模块。其余模块省略,详见文章开头的gitee地址。

博客模块

controller

package com.example.demo.business.blog.controller;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.business.blog.entity.Blog;
import com.example.demo.business.blog.service.BlogService;
import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.ShiroUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@Api(tags = "博客")
@RestController
@RequestMapping("blog")
public class BlogController {

    @Autowired
    BlogService blogService;

    @ApiOperation("创建博客")
    @RequiresAuthentication
    @PostMapping("/add")
    public Result<Blog> add(@RequestBody Blog blog) {
        Assert.hasText(blog.getTitle(), "标题不能为空");
        Assert.hasText(blog.getDescription(), "摘要不能为空");
        Assert.hasText(blog.getContent(), "内容不能为空");

        Blog temp = new Blog();
        temp.setUserId(Long.parseLong(ShiroUtil.getProfile().getUserId()));
        temp.setUpdateTime(LocalDateTime.now());
        temp.setStatus(0);

        blogService.save(temp);

        return new Result<Blog>().data(temp);
    }

    @ApiOperation("编辑博客")
    @RequiresAuthentication
    @PostMapping("/edit")
    public Result<Blog> edit(@RequestBody Blog blog) {
        Assert.isTrue(blog.getId() != null, "id不能为空");
        Assert.hasText(blog.getTitle(), "标题不能为空");
        Assert.hasText(blog.getDescription(), "摘要不能为空");
        Assert.hasText(blog.getContent(), "内容不能为空");

        Blog temp = blogService.getById(blog.getId());

        // 只能编辑自己的文章
        Assert.isTrue(temp.getUserId().equals(Long.parseLong(ShiroUtil.getProfile().getUserId())), "没有权限编辑");

        BeanUtil.copyProperties(blog, temp, "id", "userId", "updateTime", "status");
        blogService.updateById(temp);

        return new Result<Blog>().data(temp);
    }

    @ApiOperation("博客分页")
    @GetMapping("/page")
    public Result<IPage<Blog>> list(Page<Blog> page, @RequestParam String userName) {
        if (!StringUtils.hasText(userName)) {
            throw new BusinessException("用户名不能为空");
        }

        IPage<Blog> pageData = blogService.lambdaQuery()
                .eq(Blog::getUserName, userName)
                .orderByDesc(Blog::getCreateTime)
                .page(page);

        return new Result<IPage<Blog>>().data(pageData);
    }

    @ApiOperation("查看博客")
    @GetMapping("/getThis")
    public Result<Blog> detail(@RequestParam Long id) {
        Blog blog = blogService.getById(id);
        Assert.notNull(blog, "该博客已被删除");

        return new Result<Blog>().data(blog);
    }

    @ApiOperation("删除博客")
    @RequiresAuthentication
    @PostMapping("/delete")
    public Result delete(@RequestParam Long[] ids) {
        Assert.notNull(ids, "博客id不能为空");

        List<Blog> blogList = blogService.lambdaQuery()
                .in(Blog::getId, Arrays.asList(ids))
                .list();

        // 只能删除自己的文章
        for (Blog blog : blogList) {
            if (!ShiroUtil.getProfile().getUserId().equals(blog.getUserId().toString())) {
                throw new BusinessException("您无权删除其他人的文章");
            }
        }

        blogService.deleteBlog(Arrays.asList(ids));
        return new Result();
    }
}

service

接口

package com.example.demo.business.blog.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.business.blog.entity.Blog;

import java.sql.Wrapper;
import java.util.List;

public interface BlogService extends IService<Blog> {
    int deleteBlog(List<Long> blogIds);

    Integer blogCount(Long userId);
}

实现类

package com.example.demo.business.blog.service.impl;

import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.business.blog.entity.Blog;
import com.example.demo.business.blog.mapper.BlogMapper;
import com.example.demo.business.blog.service.BlogService;
import com.example.demo.common.exception.BusinessException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements BlogService {

    @Override
    public int deleteBlog(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new BusinessException("博客id不能为空");
        }

        LambdaQueryChainWrapper<Blog> wrapper = lambdaQuery().in(Blog::getId, ids);
        return this.getBaseMapper().deleteBlog(wrapper);
    }

    @Override
    public Integer blogCount(Long userId) {
        return lambdaQuery()
                .eq(Blog::getUserId, userId)
                .count();
    }
}

mapper

package com.example.demo.business.blog.mapper;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.business.blog.entity.Blog;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;


@Repository
public interface BlogMapper extends BaseMapper<Blog> {

    @Update("UPDATE t_blog SET deleted_flag = id ${ew.customSqlSegment}")
    int deleteBlog(@Param("ew") Wrapper<Blog> wrapper);
}

entity

package com.example.demo.business.blog.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_blog")
public class Blog {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private Long userId;

    private String userName;

    private String title;

    // 摘要
    private String description;

    private String content;

    private Integer status;

    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private LocalDateTime updateTime;

    @TableLogic
    private Long deletedFlag;

}

登录模块

package com.example.demo.business.login;

import com.example.demo.business.user.entity.User;
import com.example.demo.business.user.entity.UserVO;
import com.example.demo.business.user.service.UserService;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.properties.UserProperty;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@Api(tags = "登录/注册")
@RestController
@RequestMapping("/login")
public class LoginController {
    @Autowired
    private UserService userService;

    @Autowired
    private UserProperty userProperty;

    /**
     * 若未注册则自动注册,若已注册则登录
     */
    @ApiOperation("登录/注册")
    @PostMapping
    public Result<UserVO> login(@RequestBody User user, HttpServletResponse response) {
        Assert.hasLength(user.getUserName(), "用户名不能为空");
        Assert.hasLength(user.getPassword(), "密码不能为空");

        User userFromDB = userService.lambdaQuery().eq(User::getUserName, user.getUserName()).one();
        if (userFromDB == null) {
            userFromDB = register(user.getUserName(), user.getPassword());
        } else {
            String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,
                    user.getPassword(), userFromDB.getSalt(), AuthConstant.HASH_ITERATIONS).toString();

            if (!userFromDB.getPassword().equals(calculatedPassword)) {
                throw new BusinessException("用户名或密码不正确");
            }
        }

        String jwt = JwtUtil.createToken(userFromDB.getId().toString());

        response.setHeader(AuthConstant.AUTHENTICATION_HEADER, jwt);
        // response.setHeader("Access-Control-Expose-Headers", TOKEN_HEADER);   // 后端配置跨域时使用

        UserVO userVO = new UserVO();
        userVO.setId(userFromDB.getId());
        userVO.setUserName(userFromDB.getUserName());
        userVO.setNickName(userFromDB.getNickName());
        userVO.setAvatarUrl(userFromDB.getAvatarUrl());
        userVO.setEmail(userFromDB.getEmail());

        return new Result<UserVO>().success().data(userVO);
    }

    private User register(String userName, String password) {
        User user = new User();
        user.setUserName(userName);
        user.setNickName(userName);

        String salt = new SecureRandomNumberGenerator().nextBytes().toString();
        String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,
                password, salt, AuthConstant.HASH_ITERATIONS).toString();
        user.setPassword(calculatedPassword);
        user.setSalt(salt);
        user.setAvatarUrl(userProperty.getDefaultAvatarUrl());
        userService.save(user);
        return user;
    }
}

登出模块

package com.example.demo.business.logout;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "退出")
@RestController
@RequestMapping("/logout")
public class LogoutController {
    @ApiOperation("退出登录")
    @RequiresAuthentication
    @PostMapping
    public Result logout() {
        SecurityUtils.getSubject().logout();
        return new Result();
    }
}

公共部分

全局处理

全局异常处理

package com.example.demo.common.advice;

import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler(Exception.class)
    public Result<Object> handleException(Exception e) throws Exception {
        log.error(e.getMessage(), e);

        // 如果某个自定义异常有@ResponseStatus注解,就继续抛出
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        // 实际项目中应该这样写,防止用户看到详细的异常信息
        // return new Result().failure().message.message("操作失败");
        return new Result<>().failure().message(e.getMessage());
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthenticatedException.class)
    public Result<Object> handleUnauthenticatedException(Exception e) {
        log.error(e.getMessage(), e);
        return new Result<>().failure().message(e.getMessage());
    }

    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthorizedException.class)
    public Result<Object> handleUnauthorizedException(Exception e) {
        log.error(e.getMessage(), e);
        return new Result<>().failure().message(e.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    public Result<Object> handleBusinessException(Exception e) throws Exception {
        log.error(e.getMessage(), e);

        // 如果某个自定义异常有@ResponseStatus注解,就继续抛出
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        // 实际项目中应该这样写,防止用户看到详细的异常信息
        // return new Result<>().failure().message("操作失败");
        return new Result<>().failure().message(e.getMessage());
    }
}

全局响应处理

package com.example.demo.common.advice;

import com.example.demo.common.constant.WhiteList;
import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@Slf4j
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 若接口返回的类型本身就是ResultWrapper,则无需操作,返回false
        // return !returnType.getParameterType().equals(ResultWrapper.class);
        return true;
    }

    @Override
    @ResponseBody
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            // 若返回值为String类型,需要包装为String类型返回。否则会报错
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                Result<Object> result = new Result<>().data(body);
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("序列化String错误");
            }
        } else if (body instanceof Result) {
            return body;
        } else if (isKnife4jUrl(request.getURI().getPath())) {
            // 如果是接口文档uri,直接跳过
            return body;
        }
        return new Result<>().data(body);
    }

    private boolean isKnife4jUrl(String uri) {
        AntPathMatcher pathMatcher = new AntPathMatcher();
        for (String s : WhiteList.KNIFE4J) {
            if (pathMatcher.match(s, uri)) {
                return true;
            }
        }
        return false;
    }
}

工具

JWT工具

package com.example.demo.common.util.auth;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.example.demo.common.util.ApplicationContextHolder;
import com.example.demo.config.properties.JwtProperty;

import java.util.Date;

public class JwtUtil {
    private static final JwtProperty JWT_PROPERTY;

    static {
        JWT_PROPERTY = ApplicationContextHolder.getContext().getBean(JwtProperty.class);
    }

    // 创建jwt token
    public static String createToken(String userId) {
        try {
            Date date = new Date(System.currentTimeMillis() + JWT_PROPERTY.getExpire() * 1000);
            Algorithm algorithm = Algorithm.HMAC512(JWT_PROPERTY.getSecret());
            return JWT.create()
                    // 自定义私有的payload的key-value。比如:.withClaim("userName", "Tony")
                    // .withClaim("key1", "value1")
                    .withAudience(userId)  // 将 user id 保存到 token 里面
                    .withExpiresAt(date)   // date之后,token过期
                    .sign(algorithm);      // token 的密钥
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 校验token
     * 若校验失败,会抛出异常:{@link JWTVerificationException}
     * 失败情况(按先后顺序):
     * - 算法不匹配:{@link com.auth0.jwt.exceptions.AlgorithmMismatchException}
     * - 签名验证失败:{@link com.auth0.jwt.exceptions.SignatureVerificationException}
     * - Claim无效:{@link com.auth0.jwt.exceptions.InvalidClaimException}
     * - token超期:{@link com.auth0.jwt.exceptions.TokenExpiredException}
     */
    public static void verifyToken(String token) {
        Algorithm algorithm = Algorithm.HMAC512(JWT_PROPERTY.getSecret());

        JWTVerifier jwtVerifier = JWT.require(algorithm)
                // .withIssuer("auth0")
                // .withClaim("userName", userName)
                .build();

        DecodedJWT jwt = jwtVerifier.verify(token);
    }

    public static String getUserIdByToken(String token) {
        try {
            return JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    public static boolean isTokenExpired(String token) {
        DecodedJWT decodedJWT = JWT.decode(token);
        return decodedJWT.getExpiresAt().before(new Date());
    }

}

ApplicationContextHolder

package com.example.demo.common.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext context;

    public void setApplicationContext(ApplicationContext context) throws BeansException {
        ApplicationContextHolder.context = context;
    }

    public static ApplicationContext getContext() {
        return context;
    }
}  

配置部分

Shiro

总配置

package com.example.demo.config.shiro;

import com.example.demo.common.constant.WhiteList;
import com.example.demo.config.shiro.filter.JwtFilter;
import com.example.demo.config.shiro.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/login", "anon");
        chainDefinition.addPathDefinition("/blog/page", "anon");
        chainDefinition.addPathDefinition("/blog/getThis", "anon");
        chainDefinition.addPathDefinition("/user/page", "anon");
        chainDefinition.addPathDefinition("/user/profile", "anon");

        WhiteList.ALL.forEach(str -> {
            chainDefinition.addPathDefinition(str, "anon");
        });

        // all other paths require a logged in user
        chainDefinition.addPathDefinition("/**", "jwt");
        return chainDefinition;
    }

    // 这样是不行的,会导致标记了anon的路径也会走到JwtFilter。
    // 也就是说:不能将自定义的filter注册成bean。
    // @Bean("authc")
    // public AuthenticatingFilter authenticatingFilter() {
    //     return new JwtFilter();
    // }

    /**
     * 设置过滤器,将自定义的Filter加入
     */
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 登录的地址
        factoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的地址
        // factoryBean.setSuccessUrl("/index");
        // 未授权地址
        // factoryBean.setUnauthorizedUrl("/unauthorized");

        factoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = factoryBean.getFilters();
        filterMap.put("jwt", new JwtFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());

        return factoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 关闭shiro自带的session。这样不能通过session登录shiro,后面将采用jwt凭证登录。
        // 见:http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStorage
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(getDatabaseRealm());
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    @Bean
    public AccountRealm getDatabaseRealm() {
        return new AccountRealm();
    }

    /**
     * setUsePrefix(true)用于解决一个奇怪的bug。如下:
     *  在引入spring aop的情况下,在@Controller注解的类的方法中加入@RequiresRole等
     *  shiro注解,会导致该方法无法映射请求,导致返回404。加入这项配置能解决这个bug。
     */
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启shiro 注解。比如:@RequiresRole
     * 本处不用此方法开启注解,使用引入spring aop依赖的方式。原因见:application.yml里的注释
     */
    /*@Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }*/

    /**
     * 此种配置方法在本项目中跑不通。
     */
    /* @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 认证失败要跳转的地址。
        // shiroFilterFactoryBean.setLoginUrl("/login");
        // // 登录成功后要跳转的链接
        // shiroFilterFactoryBean.setSuccessUrl("/index");
        // // 未授权界面;
        // shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon");

        WhiteList.ALL.forEach(str -> {
            filterChainDefinitionMap.put(str, "anon");
        });

        // filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/**", "jwtAuthc");

        Map<String, Filter> customisedFilters = new LinkedHashMap<>();
        // 不能用注入来设置过滤器。若用注入,则本过滤器优先级会最高(/**优先级最高,导致前边所有请求都无效)。
        // springboot会扫描所有实现了javax.servlet.Filter接口的类,无需加@Component也会扫描到。
        customisedFilters.put("jwtAuthc", new JwtFilter());

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        shiroFilterFactoryBean.setFilters(customisedFilters);

        return shiroFilterFactoryBean;
    }*/
}

Filter

package com.example.demo.config.shiro.filter;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.util.ResponseUtil;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;

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

public class JwtFilter extends AuthenticatingFilter {
    /**
     * 所有请求都会到这里来(无论是不是anon)。
     * 返回true:表示允许向下走。
     * 返回false:表示不允许向下走。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest,
                                     ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);

        if (!StringUtils.hasText(token)) {
            ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,
                    HttpStatus.UNAUTHORIZED.value(), "授权信息不能为空");
            return false;
        } else {
            try {
                JwtUtil.verifyToken(token);
            } catch (TokenExpiredException e) {
                ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,
                        HttpStatus.UNAUTHORIZED.value(), "授权信息已过期,请重新登录");
                return false;
            } catch (Exception e) {
                ResponseUtil.jsonResponse((HttpServletResponse) servletResponse,
                        HttpStatus.UNAUTHORIZED.value(), "授权信息校验失败");
                return false;
            }
        }

        // 此登录并非调用login接口,而是shiro层面的登录。
        // 里边会调用下边的createToken方法
        return executeLogin(servletRequest, servletResponse);
    }

    /**
     * 这里的token会传给AuthorizingRealm子类(本处是AccountRealm)的doGetAuthenticationInfo方法作为参数
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest,
                                              ServletResponse servletResponse) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);
        if (!StringUtils.hasText(token)) {
            return null;
        }
        return new JwtToken(token);
    }
}

Realm

package com.example.demo.config.shiro.realm;

import com.example.demo.business.user.entity.User;
import com.example.demo.business.user.service.UserService;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.AccountProfile;
import com.example.demo.config.shiro.entity.JwtToken;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

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

public class AccountRealm extends AuthorizingRealm {
    @Lazy
    @Autowired
    private UserService userService;

    //使realm支持jwt的认证方案
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    // 登录认证
    // 此处的 SimpleAuthenticationInfo 可返回任意值,密码校验时不会用到它。
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) token;

        String userId = JwtUtil.getUserIdByToken((String) jwtToken.getPrincipal());
        if (userId == null) {
            throw new UnknownAccountException("token为空,请重新登录");
        }
        // 获取数据库中的密码
        User user = userService.getById(userId);
        if (user == null) {
            throw new UnknownAccountException("token为空,请重新登录");
        }

        AccountProfile accountProfile = new AccountProfile();
        accountProfile.setId(userId);
        accountProfile.setUserName(user.getUserName());

        String salt = user.getSalt();
        // 认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :accountRealm
        // 盐也放进去,通过ShiroConfig里配置的 HashedCredentialsMatcher 进行自动校验
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                accountProfile, jwtToken.getCredentials(), ByteSource.Util.bytes(salt), getName());
        return authenticationInfo;
    }

    // 权限验证
    // 只有用到org.apache.shiro.web.filter.authz包里默认的过滤器才会走到这里。
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入到这里,表示账号已经通过认证了
        AccountProfile profile = (AccountProfile) principalCollection.getPrimaryPrincipal();

        // 通过service获取角色和权限
        // Set<String> permissions = permissionService
        //         .getPermissionsByUserId(Long.parseLong(profile.getId()));
        // Set<String> roles = roleService.getRolesByUserId(profile.getId());

        Set<String> permissions = new HashSet<>();
        Set<String> roles = new HashSet<>();

        // 授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        // 把通过service获取到的角色和权限放进去
        s.setStringPermissions(permissions);
        s.setRoles(roles);
        return s;
    }
}

实体类

package com.example.demo.config.shiro.entity;

import lombok.Data;

@Data
public class AccountProfile {

    private String id;

    private String userName;

    private String avatar;

    private String email;

}
package com.example.demo.config.shiro.entity;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * JwtToken代替官方的UsernamePasswordToken,是Shiro用户名、密码等信息的载体,
 * 前后端分离,服务器不保存用户状态,所以不需要RememberMe等功能。
 */
public class JwtToken implements AuthenticationToken {

    private final String token;

    public JwtToken(String jwt) {
        this.token = jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

猜你喜欢

转载自blog.csdn.net/feiying0canglang/article/details/124087449