springboot+jwt+shiro+vue+elementUI+axios+redis+mysql完成一个前后端分离的博客项目

目录

简易博客项目(springboot+jwt+shiro+vue+elementUI+axios+redis+mysql)

项目github地址:https://github.com/huang-hanson/vueblog

第一章 整合新建springboot,整合mybatisplus

第一步 创建项目(第八步骤就行)+数据库:

1、 修改pom.xml

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.hanson</groupId>
    <artifactId>vueblog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>vueblog</name>
    <description>vueblog</description>

    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- framework: mybatis-plus代码生成需要一个模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!--mp代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- shiro-redis -->
        <!--    <dependency>
                <groupId>org.crazycake</groupId>
                <artifactId>shiro-redis-spring-boot-starter</artifactId>
                <version>3.2.1</version>
            </dependency>-->
    </dependencies>

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

</project>

2、修改配置文件

yml 复制代码
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
server:
  port: 8081

3、创建数据库vueblog然后执行下面命令生成表

sql 复制代码
DROP TABLE IF EXISTS `m_blog`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE `m_blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `title` varchar(255) NOT NULL,
  `description` varchar(255) NOT NULL,
  `content` longtext,
  `created` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */; 

DROP TABLE IF EXISTS `m_user`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
 SET character_set_client = utf8mb4 ;
CREATE TABLE `m_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `status` int(5) NOT NULL,
  `created` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; 

INSERT INTO `vueblog`.`m_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1', 'markerhub', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2020-04-20 10:44:01', NULL);

第二步 配置分页MybatisPlusConfig+生成代码(dao 、service、serviceImpl等)

1、配置分页

创建MybatisPlusConfig类(创建路径com/vueblog/config)

java 复制代码
package com.vueblog.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author hanson
 * @date 2024/5/17 14:02
 */
@Configuration
@EnableTransactionManagement
@MapperScan("com.hanson.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

2、生成代码

创建CodeGenerator(在com.vueblog包下面)
修改对应的数据库:账号密码、数据库名、包配置(ctrl+f可找到对应位置)
然后运行输入俩表 ,号隔开是多表

java 复制代码
package com.vueblog;

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

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

/**
 * @author hanson
 * @date 2024/5/17 14:06
 */
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

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

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

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        // gc.setOutputDir("D:\\test");
        gc.setAuthor("anonymous");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // 数据源配置 数据库名 账号密码
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(null);
        pc.setParent("com.vueblog");
        mpg.setPackageInfo(pc);

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

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

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

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

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

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

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

效果如下:

第三步 做测试

1、UserController类(ctrl+R可全局搜索类)

java 复制代码
package com.vueblog.controller;


import com.vueblog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author anonymous
 * @since 2024-05-17
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/index")
    public Object index(){
        return userService.getById(1L);
    }
}

2、运行项目 查看效果

第二章 统一结果封装

这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示异常)

  • 结果消息

  • 结果数据

  • Result类(路径com.vueblog.common.lang;)

java 复制代码
package com.vueblog.common.lang;

import lombok.Data;

import java.io.Serializable;

/**
 * @author hanson
 * @date 2024/5/17 14:32
 */
@Data
public class Result implements Serializable {

    private int code; // 200是正常,非200表示异常
    private String msg;
    private Object data;

    //成功
    public static Result success(Object data) {
        return success(200, "操作成功", data);
    }

    //成功
    public static Result success(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

    //失败
    public static Result fail(String msg) {
        return fail(400, msg, null);
    }

    //失败
    public static Result fail(String msg, Object data) {
        return fail(400, msg, data);
    }

    //失败
    public static Result fail(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
}
  • 在UserController 中引用测试
java 复制代码
package com.vueblog.controller;


import com.vueblog.common.lang.Result;
import com.vueblog.entity.User;
import com.vueblog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author anonymous
 * @since 2024-05-17
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/index")
    public Result index(){
        User user = userService.getById(1L);
        return Result.success(user);
    }
}

第三章 Shiro整合jwt逻辑分析

考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis。在开源的项目中,我们找到了一个starter可以快速整合shiro-redis,配置简单,这里也推荐大家使用。

而因为我们需要做的是前后端分离项目的骨架,所以一般我们会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro的过程中,我们需要引入jwt的身份验证过程。

那么我们就开始整合:

我们使用一个shiro-redis-spring-boot-starter的jar包,具体教程可以看官方文档:
https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter

1、导入shiro-redis的starter包:还有jwt的工具包,以及为了简化开发,引入hutool工具包。

pom.xml中导入:

xml 复制代码
<!-- shiro-redis -->
<dependency>
     <groupId>org.crazycake</groupId>
     <artifactId>shiro-redis-spring-boot-starter</artifactId>
     <version>3.2.1</version>
 </dependency> 
<!-- hutool工具类 -->
 <dependency>	
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.3.3</version>
 </dependency>
     <!-- jwt 生成工具 校验工具-->
 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
 </dependency>

2、创建ShiroConfig

文件路径:com/vueblog/config

java 复制代码
package com.vueblog.config;

import com.vueblog.shiro.AccountRealm;
import com.vueblog.shiro.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;

/**
 * @author hanson
 * @date 2024/5/17 15:13
 */
@Configuration
public class ShiroConfig {
    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(redisCacheManager);
        /*
         * 关闭shiro自带的session,详情见文档
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();

        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }
}

上面ShiroConfig,我们主要做了几件事情:

  1. 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
  2. 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
  3. 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

那么,接下来,我们聊聊ShiroConfig中出现的AccountRealm,还有JwtFilter。

3、创建MybatisPlusConfig

路径:com.vueblog.config

java 复制代码
package com.vueblog.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author hanson
 * @date 2024/5/17 14:02
 */
@Configuration
@EnableTransactionManagement
@MapperScan("com.vueblog.mapper")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

4、创建AccountRealm

路径: com.vueblog.shiro

AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是

  • supports:为了让realm支持jwt的凭证校验
  • doGetAuthorizationInfo:权限校验
  • doGetAuthenticationInfo:登录认证校验

我们先来总体看看AccountRealm的代码,然后逐个分析:

java 复制代码
package com.vueblog.shiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;

/**
 * @author hanson
 * @date 2024/5/17 15:17
 */
@Component
public class AccountRealm extends AuthorizingRealm {

    //为了让realm支持jwt的凭证校验
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    //权限校验
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //登录认证校验
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到我们通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。

接下来我们逐步分析里面出现的新类:

1、shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。

注意事项如果启动不了在VueblogApplication加入@MapperScan扫描mapper

java 复制代码
@SpringBootApplication
@MapperScan(basePackages = "com.vueblog.mapper")
public class VueblogApplication {

    public static void main(String[] args) {
        SpringApplication.run(VueblogApplication.class, args);
    }
}

5、创建JwtFilter

路径:com.vueblog.shiro;

这个过滤器是我们的重点,这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。

我们需要重写几个方法:

  1. createToken:实现登录,我们需要生成我们自定义支持的JwtToken
  2. onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
  3. onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
  4. preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

下面我们看看总体的代码:

java 复制代码
package com.vueblog.shiro;

import cn.hutool.json.JSONUtil;
import com.vueblog.common.lang.Result;
import com.vueblog.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author hanson
 * @date 2024/5/17 15:25
 */
@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    //验证token
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        // 获取头部token
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            return null;
        }
        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        // 获取头部token
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            return true;
        } else {

            // 检验jwt
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
                throw new ExpiredCredentialsException("token已失效,请重新登录");
            }

            // 执行登录
            return executeLogin(servletRequest, servletResponse);
        }
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();

        Result result = Result.fail(throwable.getMessage());

        String json = JSONUtil.toJsonStr(result);

        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ex) {

        }

        return false;
    }
}

6、创建JwtToken

路径:com.vueblog.shiro

java 复制代码
package com.vueblog.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author hanson
 * @date 2024/5/17 15:41
 */
public class JwtToken implements AuthenticationToken {

    private String token;

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

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

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

7、创建JwtUtils

路径:com.vueblog.util

java 复制代码
package com.vueblog.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author hanson
 * @date 2024/5/17 15:55
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtils {

    private String secret;
    private long expire;
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.debug("validate is token error ", e);
            return null;
        }
    }

    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

8、创建spring-devtools.properties

路径:resources/WETA-INF/

java 复制代码
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

第四章 Shiro逻辑开发

小提示:登录调用AccountRealm类下面的doGetAuthenticationInfo

创建类AccountProfile 用于传递数据

路径:com.vueblog.shiro

java 复制代码
package com.vueblog.shiro;

import lombok.Data;

import java.io.Serializable;

/**
 * @author hanson
 * @date 2024/5/17 17:33
 */
@Data
public class AccountProfile implements Serializable {
    private Long id;

    private String username;

    private String avatar;

    private String email;

}

完善AccountRealm

java 复制代码
package com.vueblog.shiro;

import cn.hutool.core.bean.BeanUtil;
import com.vueblog.entity.User;
import com.vueblog.service.UserService;
import com.vueblog.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author hanson
 * @date 2024/5/17 15:17
 */
@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserService userService;


    //为了让realm支持jwt的凭证校验
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    //权限校验
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //登录认证校验
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) token;

        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();

        User user = userService.getById(Long.valueOf(userId));

        if (user == null){
            throw new UnknownAccountException("账户不存在");
        }

        if (user.getStatus() == -1){
            throw new LockedAccountException("账户已经被锁定");
        }

        AccountProfile profile = new AccountProfile();

        BeanUtil.copyProperties(user,profile);

        System.out.println("----------------------");

        return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());
    }
}

第五章 异常处理

创建GlobalExceptionHandler 类

捕获全局异常

java 复制代码
package com.vueblog.common.exception;

import com.vueblog.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
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;

/**
 * 日志输出
 * 全局异常捕获
 *
 * @author hanson
 * @date 2024/5/17 17:39
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.UNAUTHORIZED)//因为前后端分离 返回一个状态 一般是401 没有权限
    @ExceptionHandler(value = RuntimeException.class)//捕获运行时异常ShiroException是大部分异常的父类
    public Result handler(ShiroException e) {
        log.error("运行时异常:--------------------{}", e);
        return Result.fail(401, e.getMessage(), null);
    }


    @ResponseStatus(HttpStatus.BAD_REQUEST)//因为前后端分离 返回一个状态
    @ExceptionHandler(value = RuntimeException.class)//捕获运行时异常
    public Result handler(RuntimeException e) {
        log.error("运行时异常:--------------------{}", e);
        return Result.fail(e.getMessage());
    }
}

然而我们运行测试发现并没有拦截

因为我们没有进行登录拦截

@RequiresAuthentication//登录拦截注解

运行效果:

提示401登录异常

第六章 实体校验

当我们表单数据提交的时候,前端的校验我们可以使用一些类似于jQuery Validate等js插件实现,而后端我们可以使用Hibernate validatior来做校验。
我们使用springboot框架作为基础,那么就已经自动集成了Hibernate validatior。(校验登录非空等等)

  • User实体类中
java 复制代码
package com.vueblog.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

/**
 * <p>
 * 
 * </p>
 *
 * @author anonymous
 * @since 2024-05-17
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("m_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @NotBlank(message = "昵称不能为空")
    private String username;

    private String avatar;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    private String password;

    private Integer status;

    private LocalDateTime created;

    private LocalDateTime lastLogin;
}
  • 在userController类中写一个方法测试
java 复制代码
/**
 *
 *@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);
 * GET方式无请求体,所以使用@RequestBody接收数据时,
 * 前端不能使用GET方式提交数据,
 * 而是用POST方式进行提交。在后端的同一个接收方法里,
 * @RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,
 * 而@RequestParam()可以有多个。
 *
 * @Validated注解用于检查user中填写的规则  如果不满足抛出异常
 * 可在GlobalExceptionHandler中捕获此异常 进行自定义 返回数据信息
 */
@PostMapping("/save")
public  Result save(@Validated @RequestBody User user){

    return Result.succ(user);
}
  • 启动postMan测试
  • 定义捕获异常返回处理

在捕获异常 GlobalExceptionHandler类中增加如下:

java 复制代码
@ResponseStatus(HttpStatus.BAD_REQUEST)//因为前后端分离 返回一个状态
@ExceptionHandler(value = MethodArgumentNotValidException.class)//捕获运行时异常
public Result handler(RuntimeException e) {
    log.error("实体校验异常:--------------------{}", e);
    return Result.fail(e.getMessage());
}

效果如下:(变得简短了)

输入正确的格式如下:

返回了我们需要的信息

第七章 跨域问题

因为是前后端分析,所以跨域问题是避免不了的,我们直接在后台进行全局跨域处理:

路径:com.vueblog.config

注意:此配置是配置到confroller的,在confroller之前是经过jwtFilter,所以在进行访问之前配置一下Filter的跨域问题

java 复制代码
package com.vueblog.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 解决跨域问题
 *
 * @author hanson
 * @date 2024/5/17 19:10
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

jwtFilter进行跨域处理:

java 复制代码
/**
 * 对跨域提供支持
 * @param request
 * @param response
 * @return
 * @throws Exception
 */
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

    HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
    HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
    httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
    httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
    httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
    // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
        httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
        return false;
    }

    return super.preHandle(request, response);
}

----基本框架已经搭建完成-----

第八章 登录接口开发

  • 创建LoginDto
  • 路径: com.vueblog.common.dto
java 复制代码
package com.vueblog.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
 * @author hanson
 * @date 2024/5/17 19:17
 */
@Data
public class LoginDto implements Serializable {

    @NotBlank(message = "昵称不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;
}
  • 在GlobalExceptionHandler类中增加断言异常
  • 路径:com.vueblog.common.exception
java 复制代码
/**
 * 断言异常
 * @param e
 * @return
 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public Result handler(IllegalArgumentException e){
    log.error("Assert异常:------------------>{}",e);
    return Result.fail(e.getMessage());
}
  • 创建AccountController类

登录和退出逻辑

java 复制代码
package com.vueblog.controller;

import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.vueblog.common.lang.Result;
import com.vueblog.dto.LoginDto;
import com.vueblog.entity.User;
import com.vueblog.service.UserService;
import com.vueblog.util.JwtUtils;
import io.jsonwebtoken.lang.Assert;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

/**
 * @author hanson
 * @date 2024/5/17 19:16
 */
@RestController
public class AccountController {

    @Autowired
    UserService userService;

    @Autowired
    JwtUtils jwtUtils;


    @PostMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {

        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notNull(user,"用户不存在");

        if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
            return Result.fail("密码不正确");
        }
        String jwt = jwtUtils.generateToken(user.getId());

        response.setHeader("Authorization",jwt);
        response.setHeader("Access-control-Expose-Headers","Authorization");


        return Result.success(MapUtil.builder()
                .put("id",user.getId())
                .put("username",user.getUsername())
                .put("avatar",user.getAvatar())
                .put("password",user.getPassword())
                .map()
        );
    }

    @GetMapping("/logout")
    public Result logout(){
        SecurityUtils.getSubject().logout();

        return Result.success(null);
    }
}

测试运行效果:


假设输入错误的密码:

第九章 博客接口的开发

  • 创建工具类ShiroUtil

路径com.vueblog.util,可于判断等等

java 复制代码
package com.vueblog.util;

import com.vueblog.shiro.AccountProfile;
import org.apache.shiro.SecurityUtils;

/**
 * @author hanson
 * @date 2024/5/17 20:13
 */
public class ShiroUtil {

    public static AccountProfile getProfile(){
        return (AccountProfile) SecurityUtils.getSubject().getPrincipal();
    }
}
  • 完善BlogController类
java 复制代码
package com.vueblog.controller;


import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vueblog.common.lang.Result;
import com.vueblog.entity.Blog;
import com.vueblog.service.BlogService;
import com.vueblog.util.ShiroUtil;
import io.jsonwebtoken.lang.Assert;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author anonymous
 * @since 2024-05-17
 */
@RestController
public class BlogController {

    @Autowired
    BlogService blogService;

    //木有值默认为1
    @GetMapping("/blogs")
    public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {

        Page page = new Page(currentPage, 5);
        IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));

        return Result.success(pageData);
    }

    //@PathVariable动态路由
    @GetMapping("/blog/{id}")
    public Result detail(@PathVariable(name = "id") Long id) {
        Blog blog = blogService.getById(id);

        Assert.notNull(blog, "该博客已被删除");

        return Result.success(blog);
    }


    //@Validated校验
    //@RequestBody
    //添加删除  木有id则添加 有id则编辑
    @RequiresAuthentication //需要认证之后才能操作
    @PostMapping("/blog/edit")
    public Result edit(@Validated @RequestBody Blog blog) {
        System.out.println(blog.toString());
        Blog temp = null;
        //如果有id则是编辑
        if(blog.getId() != null) {
            temp = blogService.getById(blog.getId());//将数据库的内容传递给temp
            //只能编辑自己的文章
            Assert.isTrue(temp.getUserId().longValue()  == ShiroUtil.getProfile().getId().longValue() , "没有权限编辑");
        } else {
            temp = new Blog();
            temp.setUserId(ShiroUtil.getProfile().getId());
            temp.setCreated(LocalDateTime.now());
            temp.setStatus(0);
        }
        //将blog的值赋给temp 忽略 id userid created status 引用于hutool
        BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
        blogService.saveOrUpdate(temp);
        return Result.success(null);
    }

    //@PathVariable动态路由
    @RequiresAuthentication  //需要认证之后才能操作
    @PostMapping("/blogdel/{id}")
    public Result del(@PathVariable Long id){
        boolean b = blogService.removeById(id);
        //判断是否为空 为空则断言异常
        if(b==true){

            return Result.success("文章删除成功");
        }else{
            return Result.fail("文章删除失败");
        }
    }
}
  • 运行程序

查询测试:

  • 新增编辑测试:

测试修改别人文章

得到token

选中header=>填写token

选中body=>raw=>json填写请求

新增

java 复制代码
{
	"title":"标题测试",
	"description":"描述测试aabbcc",
	"content":"内容测试1234567"
}

文章删除

后端总结

后端的一个骨架基本完成然后开始我们的前端开发

第十章 Vue前端页面开发

1、前言

接下来,我们来完成vueblog前端的部分功能。可能会使用的到技术如下:

  • vue
  • element-ui
  • axios
  • mavon-editor
  • markdown-it
  • github-markdown-css

2、环境准备

  • node.js安装:

https://nodejs.org/zh-cn/

安装完成之后检查下版本信息:

  • 接下来,我们安装vue的环境
bash 复制代码
# 安装淘宝npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue-cli 安装依赖包
cnpm install --g vue-cli

如果报错信息显示证书过期,更换路径

java 复制代码
http://npm.taobao.org => http://npmmirror.com
http://registry.npm.taobao.org => http://registry.npmmirror.com

3、新建项目

方式一:使用Vue ui

选中要建立的文件cmd打开,输入vue ui


点击在此创建新项目

点手动配置

打开Router和Vuex,关掉Linter

勾选

前端基础框架初始化成功

目录下生成了对应文件

方式二:命令行

选中要建立的文件cmd打开,进入你的项目目录,创建一个基于 webpack 模板的新项目: vue init webpack 项目名

输入:

vue init webpack xx

全部enter即可

完成

我们来看下整个vueblog-vue的项目结构

├── README.md 项目介绍

├── index.html 入口页面

├── build 构建脚本目录

│ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面

│ ├── build.js 生产环境构建脚本

│ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新

│ ├── dev-server.js 运行本地开发服务器

│ ├── utils.js 构建相关工具方法

│ ├── webpack.base.conf.js wabpack基础配置

│ ├── webpack.dev.conf.js wabpack开发环境配置

│ └── webpack.prod.conf.js wabpack生产环境配置

├── config 项目配置

│ ├── dev.env.js 开发环境变量

│ ├── index.js 项目配置文件

│ ├── prod.env.js 生产环境变量

│ └── test.env.js 测试环境变量

├── mock mock数据目录

│ └── hello.js

├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息

├── src 源码目录

│ ├── main.js 入口js文件

│ ├── app.vue 根组件

│ ├── components 公共组件目录

│ │ └── title.vue

│ ├── assets 资源目录,这里的资源会被wabpack构建

│ │ └── images

│ │ └── logo.png

│ ├── routes 前端路由

│ │ └── index.js

│ ├── store 应用级数据(state)状态管理

│ │ └── index.js

│ └── views 页面目录

│ ├── hello.vue

│ └── notfound.vue

├── static 纯静态资源,不会被wabpack构建。

└── test 测试文件目录(unit&e2e)

└── unit 单元测试

├── index.js 入口脚本

├── karma.conf.js karma配置文件

└── specs 单测case目录

└── Hello.spec.js

4、安装element-ui

  • 官方文档:

https://element.eleme.cn/#/zh-CN/component/installation

ctrl+`(~键)打开终端输入安装命令

bash 复制代码
# 安装element-ui
cnpm install element-ui --save

然后我们打开项目src目录下的main.js,引入element-ui依赖。

java 复制代码
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"

Vue.use(Element)
  • 测试elementUi是否引入成功

在AboutView.vue引入button组件

xml 复制代码
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <el-button plain>朴素按钮</el-button>
  </div>
</template>

运行查看结果

引入成功

5、安装axios

接下来,我们来安装axios(http://www.axios-js.com/),axios是一个基于 promise 的 HTTP 库,这样我们进行前后端对接的时候,使用这个工具可以提高我们的开发效率。

  • 安装命令:
bash 复制代码
 cnpm install axios --save
  • 然后同样我们在main.js中全局引入axios。
bash 复制代码
import axios from 'axios'
//引用全局
Vue.prototype.$axios = axios 

6、页面路由

接下来,我们先定义好路由和页面,因为我们只是做一个简单的博客项目,页面比较少,所以我们可以直接先定义好,然后在慢慢开发,这样需要用到链接的地方我们就可以直接可以使用:

我们在views文件夹下定义几个页面:

  • BlogDetail.vue(博客详情页)
  • BlogEdit.vue(编辑博客)
  • Blogs.vue(博客列表)
  • Login.vue(登录页面)

可以配置插件:VueHelper(新建vue项目 最上方输入vuet按键tab可直接生成模板)
几个页面相同,以Login为例

注意:每个页面下方 里面只能有一个


测试效果





所有路由正确

7、登录页面开发

登录页面制作

Login.vie

typescript 复制代码
<template>
  <div>
    <el-container>
      <el-header>
        <img class="mlogo" src="https://img1.baidu.com/it/u=3430690511,3867923153&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=453" alt="">
      </el-header>
      <el-main>
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="ruleForm.username"></el-input>
          </el-form-item>

          <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="ruleForm.password"></el-input>
          </el-form-item>

          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
            <el-button @click="resetForm('ruleForm')">重置</el-button>
          </el-form-item>
        </el-form>
      </el-main>
    </el-container>

  </div>
</template>


<script>
export default {
  name: "Login",
  data() {
    return {
      ruleForm: {
        username: '',
        password: ''
      },
      rules: {
        username: [
          {required: true, message: '请输入用户名', trigger: 'blur'},
          {min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur'}
        ],
        password: [
          {required: true, message: '请选输入密码', trigger: 'change'}
        ]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          alert('submit!');
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    }
  }
}

</script>


<style scoped>
.el-header, .el-footer {
  background-color: #B3C0D1;
  color: #333;
  text-align: center;
  line-height: 60px;
}

.el-aside {
  background-color: #D3DCE6;
  color: #333;
  text-align: center;
  line-height: 200px;
}

.el-main {
//background-color: #E9EEF3; color: #333; text-align: center;
  line-height: 160px;
}

body > .el-container {
  margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
  line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
  line-height: 320px;
}

.mlogo {
  height: 60%;
  margin-top: 10px;
}

.demo-ruleForm {
  max-width: 500px;
  margin: 0 auto;
}

</style>

在Login.vie中添加axios

测试

登陆成功,请求头中携带token

8、登录发起请求 (配置完善store)

配置完善store/index.js

在store下面的index.js添加
mutationsx相当于java中实体类的set
getters相当于get
*

userInfo可以存入会话的sessionStorage里面
sessionStorage中只能存字符串 不能存入对象所以我们存入序列化 jons串:
sessionStorage.setItem("userInfo",JSON.stringify(userInfo))

会话获取
sessionStorage.getInte(userInfo)
token可以存入浏览器的localStorage里面

localStorage.setItem("token",token)

token获取:

localStorage.getItem("token")

typescript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  //定义全局参数 其他页面可以直接获取state里面的内容
  state: {
    token: '', //方法一 localStorage.getItem("token") 
    //反序列化获取session会话中的 userInfo对象
    userInfo:JSON.parse(sessionStorage.getItem("userInfo"))
  },
  mutations: {
    //相当于实体类的set
    SET_TOKEN:(state,token)=>{
      state.token=token//将传入的token赋值 给state的token
      //同时可以存入浏览器的localStorage里面
      localStorage.setItem("token",token)
    },
    SET_USERINFO:(state,userInfo)=>{
      state.userInfo=userInfo//将传入的tuserInfo赋值 给state的userInfo
      //同时可以存入会话的sessionStorage里面 sessionStorage中只能存字符串 不能存入对象所以我们存入序列化 jons串
      sessionStorage.setItem("userInfo",JSON.stringify(userInfo))
    },
    //删除token及userInfo
    REMOVE_INFO:(state)=>{
      state.token = '';
      state.userInfo = {};
      localStorage.setItem("token",'')
      sessionStorage.setItem("userInfo",JSON.stringify(''))
    }
  },
  getters: {
    //相当于get
    //配置一个getUser可以直接获取已经反序列化对象的一个userInfo
   getUser: state=>{
     return state.userInfo;
   },getToken: state=>{
    return state.token;
  }
  },
  actions: {
    
  },
  modules: {
    
  }
})
  • 在Login.vue中添加
typescript 复制代码
_this.$axios.post("http://localhost:8081/login", _this.ruleForm).then(res => {

         console.log(res)
         const jwt = res.headers['authorization'];
         const userInfo = res.data.data
         _this.$store.commit('SET_TOKEN', token)
         _this.$store.commit('SET_USERINFO', userInfo)
       })
typescript 复制代码
<template>
  <div>
    <el-container>
      <el-header>
        <img class="mlogo"
             src="https://img1.baidu.com/it/u=3430690511,3867923153&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=453" alt="">
      </el-header>
      <el-main>
        <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="ruleForm.username"></el-input>
          </el-form-item>

          <el-form-item label="密码" prop="password">
            <el-input type="password" v-model="ruleForm.password"></el-input>
          </el-form-item>

          <el-form-item>
            <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
            <el-button @click="resetForm('ruleForm')">重置</el-button>
          </el-form-item>
        </el-form>
      </el-main>
    </el-container>

  </div>
</template>


<script>
export default {
  name: "Login",
  data() {
    return {
      ruleForm: {
        username: 'Hanson',
        password: '111111'
      },
      rules: {
        username: [
          {required: true, message: '请输入用户名', trigger: 'blur'},
          {min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur'}
        ],
        password: [
          {required: true, message: '请选输入密码', trigger: 'change'}
        ]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          const _this = this
          this.$axios.post('http://localhost:8082/login', this.ruleForm).then(res => {

            const jwt = res.headers['authorization']

            const userInfo = res.data.data

            console.log(userInfo)

            // 把数据共享出来
            _this.$store.commit("SET_TOKEN",jwt)
            _this.$store.commit("SET_USERINFO",userInfo)

            // 获取
            // console.log(this.$store.getters.getUser)

            // 跳转页面
            _this.$router.push("/blogs")
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    }
  }
}

</script>


<style scoped>
.el-header, .el-footer {
  background-color: #B3C0D1;
  color: #333;
  text-align: center;
  line-height: 60px;
}

.el-aside {
  background-color: #D3DCE6;
  color: #333;
  text-align: center;
  line-height: 200px;
}

.el-main {
//background-color: #E9EEF3; color: #333; text-align: center; line-height: 160px;
}

body > .el-container {
  margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
  line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
  line-height: 320px;
}

.mlogo {
  height: 60%;
  margin-top: 10px;
}

.demo-ruleForm {
  max-width: 500px;
  margin: 0 auto;
}

</style>

打开页面点击登录
可查看我们的信息已经存入浏览器中



localStorage中存入了token信息,sessionStorage中存入了用户信息

9、配置axios拦截

  • main.js中引入
typescript 复制代码
// 引入自定义axios.js
import "./axios.js"
  • 完善axios.js
typescript 复制代码
import axios from 'axios'
import Element from "element-ui";
import router from './router'
import store from './store'

axios.defaults.baseURL = "http://localhost:8082"

// 前置拦截
axios.interceptors.request.use(config => {
    return config
})

axios.interceptors.response.use(response => {
        let res = response.data;

        console.log("====================")
        console.log(res)
        console.log("====================")

        if (res.code === 200) {
            return response
        } else {
            Element.Message.error('错了哦,这是一条错误消息', {duration: 3 * 1000});

            return Promise.reject(response.data.msg)
        }
    },
    error => {
        console.log(error)
        if (error.response.data){
            error.message = error.response.data.msg
        }

        if (error.response.status === 401) {
            store.commit("REMOVE_INFO")
            router.push("/login")
        }

        Element.Message.error(error.message, {duration: 3 * 1000});
        return Promise.reject(error)
    }
)
  • 运行效果如下

10、公共组件Header

  • 首先,需要打开redis-serve

在application.yml中加入redis配置

typescript 复制代码
shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379
  • 完善配置Header组件
typescript 复制代码
<template>
  <div class="m-content">
    <h3>欢迎来到Hanson的博客</h3>
    <div class="block">
      <el-avatar :size="50" :src="user.avatar"></el-avatar>
      <div>{{ user.username }}</div>
    </div>

    <div class="maction">
      <span><el-link href="/blogs">主页</el-link></span>
      <el-divider direction="vertical"></el-divider>
      <span><el-link type="success" href="/blog/add">发表博客</el-link></span>

      <el-divider direction="vertical"></el-divider>
      <span v-show="!hasLogin"><el-link type="primary" href="/login">登录</el-link></span>

      <el-divider direction="vertical"></el-divider>
      <span v-show="hasLogin"><el-link type="danger" @click="logout">退出</el-link></span>
    </div>
  </div>
</template>

<script>
export default {
  name: "Header",
  data() {
    return {
      user: {
        username: '请先登录',
        avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
      },
      hasLogin: false
    }
  },
  methods: {
    logout() {
      const _this = this
      _this.$axios.get("/logout", {
        headers: {
          "Authorization": localStorage.getItem("token")
        }
      }).then(res => {
        _this.$store.commit("REMOVE_INFO")
        _this.$router.push("/login")
      })
    }
  },
  created() {
    if (this.$store.getters.getUser.username) {
      this.user.username = this.$store.getters.getUser.username
      this.user.avatar = this.$store.getters.getUser.avatar

      this.hasLogin = true
    }
  }
}

</script>

<style scoped>
.m-content {
  max-width: 960px;
  margin: 0 auto;
  text-align: center;
}

.maction {
  margin: 10px 0;
}
</style>
  • 运行项目可以自行测试登录退出功能


11、博客列表页面开发

  • 完善blogs.vue

先给实体类Blog加上JsonFormat

java 复制代码
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime created;
  • 完善Blogs.vue
typescript 复制代码
<template>
  <div class="mcontaner">
    <Header></Header>

    <div class="block">
      <el-timeline>

        <el-timeline-item :timestamp="blog.created" placement="top" v-for="blog in blogs">
          <el-card>
            <h4>
              <router-link :to="{name:'BlogDetail',params:{blogId: blog.id}}">
                {{ blog.title }}
              </router-link>
            </h4>
            <p>{{ blog.description }}</p>
          </el-card>
        </el-timeline-item>

      </el-timeline>

      <el-pagination class="mpage"
                     background
                     layout="prev, pager, next"
                     :current-page="currentPage"
                     :page-size="pageSize"
                     :total="total"
                     @current-change=page

      >
      </el-pagination>
    </div>
  </div>
</template>

<script>
import Header from "../components/Header"

export default {
  name: "Blogs.vue",
  components: {Header},
  data() {
    return {
      blogs: {},
      currentPage: 1,
      total: 0,
      pageSize: 5
    }
  },
  methods: {
    page(currentPage) {
      const _this = this
      _this.$axios.get("/blogs?currentPage=" + currentPage).then(res => {
        console.log(res)
        _this.blogs = res.data.data.records
        _this.currentPage = res.data.data.current
        _this.total = res.data.data.total
        _this.pageSize = res.data.data.size
      })
    }
  },
  created() {
    this.page(1)
  }
}

</script>


<style scoped>

.mpage {
  margin: 0 auto;
  text-align: center;
}
</style>

效果展示:


点击第二页能翻页

12、博客编辑(发表)

安装mavon-editor

  • 基于Vue的markdown编辑器mavon-editor
bash 复制代码
cnpm install mavon-editor --save
  • 然后在main.js中全局注册:
typescript 复制代码
// 全局注册 
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
// use
Vue.use(mavonEditor)
  • 编写BlogEdit.vue
typescript 复制代码
<template>
  <div>
    <Header></Header>


    <div class="m-content">

      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
        <el-form-item label="标题" prop="title">
          <el-input v-model="ruleForm.title"></el-input>
        </el-form-item>

        <el-form-item label="摘要" prop="description">
          <el-input type="textarea" v-model="ruleForm.description"></el-input>
        </el-form-item>

        <el-form-item label="内容" prop="content">
          <mavon-editor v-model="ruleForm.content"></mavon-editor>
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
          <el-button @click="resetForm('ruleForm')">重置</el-button>
        </el-form-item>
      </el-form>

    </div>


  </div>
</template>

<script>
import Header from "../components/Header"

export default {
  name: "BlogEdit",
  components: {Header},
  data() {
    return {
      ruleForm: {
        id: "",
        title: '',
        description: '',
        content: ''
      },
      rules: {
        title: [
          {required: true, message: '请输入标题', trigger: 'blur'},
          {min: 3, max: 25, message: '长度在 3 到 25 个字符', trigger: 'blur'}
        ],
        description: [
          {required: true, message: '请输入摘要', trigger: 'blur'}
        ],
        content: [
          {trequired: true, message: '请输入内容', trigger: 'blur'}
        ]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {

          const _this = this
          this.$axios.post('blog/edit', this.ruleForm, {
            headers: {
              "Authorization": localStorage.getItem("token")
            }
          }).then(res => {
            console.log(res)
            this.$alert('操作成功', '提示', {
              confirmButtonText: '确定',
              callback: action => {
                -this.$router.push("/blogs")
              }
            });
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    }
  },
  created() {
    // 内容渲染的时候进行回显
    const blogId = this.$route.params.blogId
    console.log(blogId)
    if (blogId) {
      this.$axios.get("/blog/" + blogId).then(res => {
        const _this = this
        const blog = res.data.data
        _this.ruleForm.id = blog.id
        _this.ruleForm.title = blog.title
        _this.ruleForm.description = blog.description
        _this.ruleForm.content = blog.content
      })
    }
  }
}

</script>

<style scoped>
.m-content {
  text-align: center;
}

</style>

效果展示

13、博客详情

博客详情中需要回显博客信息,然后有个问题就是,后端传过来的是博客内容是markdown格式的内容,我们需要进行渲染然后显示出来,这里我们使用一个插件markdown-it,用于解析md文档,然后导入github-markdown-c,所谓md的样式。

bash 复制代码
# 用于解析md文档
cnpm install markdown-it --save
# md样式
cnpm install github-markdown-css

然后就可以在需要渲染的地方使用:

  • views\BlogDetail.vue
typescript 复制代码
<template>
  <div>
    <Header></Header>
    <div class="mblog">
      <h2>{{ blog.title }}</h2>

      <el-link icon="el-icon-edit">
        <router-link :to="{name:'BlogEdit' ,params:{BlogId:blog.id}}">
          编辑
        </router-link>
      </el-link>

      <el-divider></el-divider>
      <div class="markdown-body" v-html="blog.content"></div>

    </div>
  </div>
</template>

<script>

import Header from "@/components/Header.vue";
import "github-markdown-css/github-markdown.css"

export default {
  name: "BlogDetail.vue",
  components: {Header},
  data() {
    return {
      blog: {
        id: "",
        title: "",
        content: ""
      }
    }
  },
  created() {
    const blogId = this.$route.params.blogId
    console.log(blogId)
    const _this = this
    this.$axios.get("/blog/" + blogId).then(res => {
      const _this = this
      const blog = res.data.data
      _this.blog.id = blog.id
      _this.blog.title = blog.title
      _this.blog.content = blog.content

      var MarkdownIt = require("markdown-it")
      var md = new MarkdownIt()

      var result = md.render(blog.content)
      _this.blog.content = result
    })
  }
}

</script>

<style scoped>
.mblog {
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  width: 100%;
  min-height: 700px;
  padding: 20px 15px;
}

</style>

效果展示:

自己的博客可以编辑

别人的博客不可以编辑

14、路由权限拦截

页面已经开发完毕之后,我们来控制一下哪些页面是需要登录之后才能跳转的,如果未登录访问就直接重定向到登录页面,因此我们在src目录下定义一个js文件:

//配置一个路由前置拦截 rounter是路由

  • src\permission.js
typescript 复制代码
import router from "./router";
// 路由判断登录 根据路由配置文件的参数
router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) { // 判断该路由是否需要登录权限
    const token = localStorage.getItem("token")
    console.log("------------" + token)
    if (token) { // 判断当前的token是否存在 ; 登录存入的token
      if (to.path === '/login') {
      } else {
        next()
      }
    } else {
      next({
        path: '/login'
      })
    }
  } else {
    next()
  }
})

通过之前我们再定义页面路由时候的的meta信息,指定requireAuth: true,需要登录才能访问,因此这里我们在每次路由之前(router.beforeEach)判断token的状态,觉得是否需要跳转到登录页面。

  • src/rouer/index.js
    添加:
typescript 复制代码
meta: {
	requireAuth: true
}
typescript 复制代码
{
  path: '/blog/add', // 注意放在 path: '/blog/:blogId'之前
  name: 'BlogAdd',
  meta: {
    requireAuth: true
  },
  component: BlogEdit
},{
      path: '/blog/:blogid/edit',
      name: 'BlogEdit', 
      component: BlogEdit,
      meta: {
        requireAuth: true
      }
typescript 复制代码
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Demo from '@/views/Demo' 
import Login from '@/views/Login' 
import Blogs from '@/views/Blogs' 
import BlogEdit from '@/views/BlogEdit' 
import BlogDetail from '@/views/BlogDetail' 

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Index',
      redirect:{name : "Blogs"}
    },
    {
      path: '/blogs',
      name: 'Blogs',
      component: Blogs
    },{
    path: '/Login',
    name: 'Login',
    component: Login
    },{
      path: '/blog/add',
      name: 'BlogEdit',
      component: BlogEdit,
      meta: {
        requireAuth: true
      }
    }, {
      path: '/Demo',
      name: 'Demo',
      component: Demo
    },{
      path: '/blog/:blogid',
      name: 'BlogDetail',
      component: BlogDetail
    } ,{
      path: '/blog/:blogid/edit',
      name: 'BlogEdit', 
      component: BlogEdit,
      meta: {
        requireAuth: true
      }
    }
]})
  • 然后我们再main.js中import我们的permission.js
typescript 复制代码
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
// 存储
import store from './store'
// 路由
import router from './router'
// 引入element-ui依赖
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"

// 引入axios依赖
import axios from 'axios'   

// 引入自定义axios.js
import "./axios.js"
import './permission.js' // 路由拦截

//mavonEditor
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'



//引用全局
Vue.prototype.$axios = axios 

// use
Vue.use(mavonEditor)
Vue.use(Element)
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store ,
  components: { App },
  template: '<App/>'
})

效果展示

未登录访问编辑页面

然后跳转登陆页面

16、前端总结

ok,基本所有页面就已经开发完毕啦,css样式信息我未贴出来,大家直接上github上clone下来查看。

相关推荐
武昌库里写JAVA2 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
ai产品老杨2 小时前
减少交通拥堵、提高效率、改善交通安全的智慧交通开源了。
前端·vue.js·算法·ecmascript·音视频
小白杨树树2 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
张老爷子3 小时前
记录uniapp开发安卓使用webRTC实现语音推送
vue.js
发渐稀4 小时前
vue项目引入tailwindcss
前端·javascript·vue.js
vanora11117 小时前
Vue在线预览excel、word、ppt等格式数据。
前端·javascript·vue.js
xiaogg36787 小时前
网站首页菜单顶部下拉上下布局以及可关闭标签页实现vue+elementui
javascript·vue.js·elementui
有梦想的攻城狮8 小时前
从0开始学vue:pnpm怎么安装
前端·javascript·vue.js
pzpcxy5208 小时前
安装VUE客户端@vue/cli报错警告npm WARN deprecated解决方法 无法将“vue”项识别为 cmdlet、函数
前端·vue.js·npm