SpringBoot + Vue builds a blog project system with separation of front and back ends

One: Brief introduction

Functional outline

Basic addition, deletion, modification and investigation of the blog project system

Learning purpose

Build the skeleton of the front-end and back-end separation project

Two: Java back-end interface development

1 Introduction

  • To build a project skeleton from scratch, it is best to choose a suitable and familiar technology, and it is easy to expand in the future, suitable for micro-service system, etc. Therefore, SpringBoot is generally used as the basis of our framework.
  • Then the data layer, we usually use Mybatis, which is easy to use and easy to maintain. But single table operation is more difficult, especially when adding or reducing fields, it is more cumbersome, so here I recommend to use Mybatis Plus, which is born to simplify development, only need simple configuration, you can quickly perform CRUD operations, thus saving a lot of time .
  • As a project skeleton, we cannot ignore the permissions. Shiro is simple to configure and easy to use, so we use Shiro as our permission.
  • Taking into account that the project may need to deploy multiple units, at this time our session and other information need to be shared, Redis is now the mainstream cache middleware, and it is also suitable for our project.
  • Because the front and back ends are separated, we use Jwt as our user credentials.

2. Technology stack

  • SpringBoot
  • Mybatis Plus
  • Shiro
  • Lombok
  • Redis
  • Hibernate Validatior
  • Jwt

3. Create a new SpringBoot project

image.png

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.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pony</groupId>
    <artifactId>springboot_blog</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_blog</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--项目的热加载重启插件-->
        <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>
        <!--化代码的工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--mybatis plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--mybatis plus 代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--freemarker-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </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
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: password
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
Open mapper interface scanning, add paging plug-in
package com.pony.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 malf
 * @description 通过@MapperScan 注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。
 * PaginationInterceptor是一个分页插件。
 * @date 2021/5/22
 * @project springboot_blog
 */
@Configuration
@EnableTransactionManagement
@MapperScan("com.pony.mapper")
public class MybatisPlusConfig {

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }

}
Code generation (directly generate entity, service, mapper and other interfaces and implementation classes based on database table information)
  • Create Mysql database table

    CREATE TABLE `pony_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 DEFAULT CHARSET=utf8;
    CREATE TABLE `pony_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=11 DEFAULT CHARSET=utf8mb4;
    INSERT INTO `springboot_blog`.`pony_user` (`id`, `username`, `avatar`, `email`, `password`, `status`, `created`, `last_login`) VALUES ('1', 'pony', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', NULL, '96e79218965eb72c92a549dd5a330112', '0', '2021-05-20 10:44:01', NULL);
  • Code generator: CodeGenerator

    package com.pony;
    
    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 malf
     * @description 执行 main 方法,在控制台输入模块表名回车自动生成对应项目目录
     * @date 2021/5/22
     * @project springboot_blog
     */
    public class CodeGenerator {
      /**
       * 读取控制台内容
       */
      public static String scanner(String tip) {
          Scanner scanner = new Scanner(System.in);
          StringBuilder help = new StringBuilder();
          help.append("请输入" + tip + ":");
          System.out.println(help.toString());
          if (scanner.hasNext()) {
              String ipt = scanner.next();
              if (StringUtils.isNotEmpty(ipt)) {
                  return ipt;
              }
          }
          throw new MybatisPlusException("请输入正确的" + tip + "!");
      }
    
      public static void main(String[] args) {
          // 代码生成器
          AutoGenerator mpg = new AutoGenerator();
          // 全局配置
          GlobalConfig gc = new GlobalConfig();
          String projectPath = System.getProperty("user.dir");
          gc.setOutputDir(projectPath + "/src/main/java");
          gc.setAuthor("pony");
          gc.setOpen(false);
          // gc.setSwagger2(true); 实体属性 Swagger2 注解
          gc.setServiceName("%sService");
          mpg.setGlobalConfig(gc);
    
          // 数据源配置
          DataSourceConfig dsc = new DataSourceConfig();
          dsc.setUrl("jdbc:mysql://localhost:3306/springboot_blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
          dsc.setDriverName("com.mysql.cj.jdbc.Driver");
          dsc.setUsername("root");
          dsc.setPassword("password");
          mpg.setDataSource(dsc);
    
          // 包配置
          PackageConfig pc = new PackageConfig();
          pc.setModuleName(null);
          pc.setParent("com.pony");
          mpg.setPackageInfo(pc);
    
          // 自定义配置
          InjectionConfig cfg = new InjectionConfig() {
              @Override
              public void initMap() {
              }
          };
    
          // 如果模板引擎是 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("pony_");
          mpg.setStrategy(strategy);
          mpg.setTemplateEngine(new FreemarkerTemplateEngine());
          mpg.execute();
      }
    
    }
  • Execute the main method, enter pony_user, pony_blog and press Enter. The code directory structure is as follows:
    image.png
  • Test that the current steps are correct and the database connection is normal

    package com.pony.controller;
    
    import com.pony.service.UserService;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * <p>
     * 前端控制器
     * </p>
     *
     * @author pony
     * @since 2021-05-22
     */
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
      @Resource
      UserService userService;
    
      @GetMapping("/{id}")
      public Object test(@PathVariable("id") Long id) {
          return userService.getById(id);
      }
    
    }
  • Run the project, visit address http://localhost:8080/user/1

    4. Unified result package

    package com.pony.common;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * @author malf
     * @description 用于异步统一返回的结果封装。
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Data
    public class Result implements Serializable {
    
      private String code;    // 是否成功
      private String message; // 结果消息
      private Object data;    // 结果数据
    
      public static Result success(Object data) {
          Result result = new Result();
          result.setCode("0");
          result.setData(data);
          result.setMessage("操作成功");
          return result;
      }
    
      public static Result success(String message, Object data) {
          Result result = new Result();
          result.setCode("0");
          result.setData(data);
          result.setMessage(message);
          return result;
      }
    
      public static Result fail(String message) {
          Result result = new Result();
          result.setCode("-1");
          result.setData(null);
          result.setMessage(message);
          return result;
      }
    
      public static Result fail(String message, Object data) {
          Result result = new Result();
          result.setCode("-1");
          result.setData(data);
          result.setMessage(message);
          return result;
      }
      
    }

    5. Integrate Shiro + Jwt and share sessions

    Considering that clustering, load balancing, etc. may be needed later, session sharing is required. For Shiro's cache and session information, we generally consider using Redis to store data. Therefore, we not only need to integrate Shiro, but also Redis.

    Introduce shiro-redis and jwt dependencies, in order to simplify development, at the same time introduce the hutool toolkit.
    <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>
    Write Shiro configuration class
  • Step 1: Generate and verify jwt tool classes, some of which are related to jwt key information are configured from the project configuration file

    package com.pony.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 malf
     * @description
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Slf4j
    @Data
    @Component
    @ConfigurationProperties(prefix = "pony.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());
      }
    
    }
  • Step 2: Customize a JwtToken to complete shiro's supports method

    package com.pony.shiro;
    
    import org.apache.shiro.authc.AuthenticationToken;
    
    /**
     * @author malf
     * @description shiro默认supports的是UsernamePasswordToken,我们现在采用了jwt的方式,
     * 所以这里自定义一个JwtToken,来完成shiro的supports方法。
     * @date 2021/5/22
     * @project springboot_blog
     */
    public class JwtToken implements AuthenticationToken {
    
      private String token;
    
      public JwtToken(String token) {
          this.token = token;
      }
    
      @Override
      public Object getPrincipal() {
          return token;
      }
    
      @Override
      public Object getCredentials() {
          return token;
      }
    
    }
  • Step 3: A carrier of user information returned after successful login

    package com.pony.shiro;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * @author malf
     * @description 登录成功之后返回的一个用户信息的载体
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Data
    public class AccountProfile implements Serializable {
    
      private Long id;
      private String username;
      private String avatar;
    
    }
  • Step 4: The logic of shiro's login or permission verification

    package com.pony.shiro;
    
    import cn.hutool.core.bean.BeanUtil;
    import com.pony.entity.User;
    import com.pony.service.UserService;
    import com.pony.util.JwtUtils;
    import lombok.extern.slf4j.Slf4j;
    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.stereotype.Component;
    
    import javax.annotation.Resource;
    
    /**
     * @author malf
     * @description shiro进行登录或者权限校验的逻辑所在
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Slf4j
    @Component
    public class AccountRealm extends AuthorizingRealm {
    
      @Resource
      JwtUtils jwtUtils;
      @Resource
      UserService userService;
    
      @Override
      public boolean supports(AuthenticationToken token) {
          return token instanceof JwtToken;
      }
    
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
          return null;
      }
    
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          JwtToken jwt = (JwtToken) token;
          log.info("jwt----------------->{}", jwt);
          String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();
          User user = userService.getById(Long.parseLong(userId));
          if (user == null) {
              throw new UnknownAccountException("账户不存在!");
          }
          if (user.getStatus() == -1) {
              throw new LockedAccountException("账户已被锁定!");
          }
          AccountProfile profile = new AccountProfile();
          BeanUtil.copyProperties(user, profile);
          log.info("profile----------------->{}", profile.toString());
          return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
      }
    
    }
    • supports: In order for realm to support jwt credential verification
    • doGetAuthorizationInfo: Authorization verification
    • doGetAuthenticationInfo: Login authentication verification, obtain user information through jwt, judge the user's status, and finally throw the corresponding exception information when the exception occurs, or encapsulate it as SimpleAuthenticationInfo and return it to shiro.
  • Step 5: Define the filter JwtFilter of jwt

    package com.pony.shiro;
    
    import cn.hutool.json.JSONUtil;
    import com.baomidou.mybatisplus.core.toolkit.StringUtils;
    import com.pony.common.Result;
    import com.pony.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.apache.shiro.web.util.WebUtils;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import javax.annotation.Resource;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @author malf
     * @description 定义jwt的过滤器JwtFilter。
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Component
    public class JwtFilter extends AuthenticatingFilter {
      @Resource
      JwtUtils jwtUtils;
    
      @Override
      protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
          // 获取 token
          HttpServletRequest request = (HttpServletRequest) servletRequest;
          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;
          String token = request.getHeader("Authorization");
          if (StringUtils.isEmpty(token)) {
              return true;
          } else {
              // 判断是否已过期
              Claims claim = jwtUtils.getClaimByToken(token);
              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 httpResponse = (HttpServletResponse) response;
          try {
              //处理登录失败的异常
              Throwable throwable = e.getCause() == null ? e : e.getCause();
              Result r = Result.fail(throwable.getMessage());
              String json = JSONUtil.toJsonStr(r);
              httpResponse.getWriter().print(json);
          } catch (IOException e1) {
          }
          return false;
      }
    
      /**
       * 对跨域提供支持
       */
      @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);
      }
      
    }
    • createToken: To achieve login, we need to generate our custom supported JwtToken
    • onAccessDenied: interception verification, when the header does not have Authorization, we pass it directly, without automatic login; when it has, first we verify the validity of jwt, and if there is no problem, we directly execute the executeLogin method to realize automatic login
    • onLoginFailure: The method to enter when the login is abnormal, we directly encapsulate the exception information and throw it
    • preHandle: The pre-interception of the interceptor, because it is a front-end and back-end analysis project. In addition to the cross-domain global configuration, the interceptor also needs to provide cross-domain support. In this way, the interceptor will not be restricted before entering the Controller
  • Step 6: Shiro configuration class

    package com.pony.config;
    
    import com.pony.shiro.AccountRealm;
    import com.pony.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.security.interceptor.AuthorizationAttributeSourceAdvisor;
    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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.annotation.Resource;
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author malf
     * @description shiro 启用注解拦截控制器
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Configuration
    public class ShiroConfig {
    
      @Resource
      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;
      }
    
      // 开启注解代理(默认好像已经开启,可以不要)
      @Bean
      public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
          AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
          authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
          return authorizationAttributeSourceAdvisor;
      }
    
      @Bean
      public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
          DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
          return creator;
      }
    
    }
    • Introduce RedisSessionDAO and RedisCacheManager, in order to solve the problem that shiro's permission data and session information can be saved in redis to realize session sharing.
    • The SessionManager and DefaultWebSecurityManager are rewritten. At the same time, in order to turn off the session method that comes with shiro in DefaultWebSecurityManager, we need to set it to false so that users can no longer log in to shiro through session. The jwt credential will be used to log in later.
    • In ShiroFilterChainDefinition, we no longer intercept Controller access paths through encoding, but all routes need to pass through the JwtFilter filter, and then determine whether the request header contains jwt information, log in if there is one, and skip if not. After skipping, the shiro annotation in the Controller is intercepted again, such as @RequiresAuthentication, to control access to permissions.
  • Step 7: Configuration file

    shiro-redis:
    enabled: true
    redis-manager:
      host: 127.0.0.1:6379
    pony:
    jwt:
      # 加密秘钥
      secret: f4e2e52034348f86b67cde581c0f9eb5
      # token有效时长,7天,单位秒
      expire: 604800
      header: token
  • Step 8: Hot deployment configuration (if devtools dependency is added)
    resources/META-INF/spring-devtools.properties

    restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

    So far, shiro has been integrated and used jwt for identity verification.

6. Exception handling

package com.pony;

import com.pony.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.io.IOException;

/**
 * @author malf
 * @description 全局异常处理
 * @ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。
 * @date 2021/5/22
 * @project springboot_blog
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result handle401(ShiroException e) {
        return Result.fail("401", e.getMessage(), null);
    }

    /**
     * 处理Assert的异常
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) throws IOException {
        log.error("Assert异常:-------------->{}", e.getMessage());
        return Result.fail(e.getMessage());
    }

    /**
     * 校验错误异常处理
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
        log.error("运行时异常:-------------->", e);
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) throws IOException {
        log.error("运行时异常:-------------->", e);
        return Result.fail(e.getMessage());
    }

}
  • ShiroException: exception thrown by shiro, such as no permission, user login exception
  • IllegalArgumentException: Exception handling Assert
  • MethodArgumentNotValidException: processing entity validation exception
  • RuntimeException: catch other exceptions

    7. Entity verification

    1. When the form data is submitted, the front-end verification can be implemented using some js plug-ins similar to jQuery Validate, and the back-end can use Hibernate validatior for verification.

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

    2. @Validated annotation verification entity

    /**
     * 测试实体校验
     * @param user
     * @return
     */
    @PostMapping("/save")
    public Object testUser(@Validated @RequestBody User user) {
      return user.toString();
    }

    8. Cross-domain issues

    package com.pony.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @author malf
     * @description 全局跨域处理
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
      @Override
      public void addCorsMappings(CorsRegistry registry) {
          registry.addMapping("/**")
                  .allowedOriginPatterns("*")
                  .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                  .allowCredentials(true)
                  .maxAge(3600)
                  .allowedHeaders("*");
      }
    
    }

    9. Login interface development

  • Login account password entity

    package com.pony.common;
    
    import lombok.Data;
    
    import javax.validation.constraints.NotBlank;
    
    /**
     * @author malf
     * @description
     * @date 2021/5/22
     * @project springboot_blog
     */
    @Data
    public class LoginDto {
    
      @NotBlank(message = "昵称不能为空")
      private String username;
    
      @NotBlank(message = "密码不能为空")
      private String password;
    
    }
  • Log in and log out

    package com.pony.controller;
    
    import cn.hutool.core.lang.Assert;
    import cn.hutool.core.map.MapUtil;
    import cn.hutool.crypto.SecureUtil;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.pony.common.LoginDto;
    import com.pony.common.Result;
    import com.pony.entity.User;
    import com.pony.service.UserService;
    import com.pony.util.JwtUtils;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authz.annotation.RequiresAuthentication;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * @author malf
     * @description 登录接口
     * 接受账号密码,然后把用户的id生成jwt,返回给前段,为了后续的jwt的延期,把jwt放在header上
     * @date 2021/5/22
     * @project springboot_blog
     */
    public class AccountController {
    
      @Resource
      JwtUtils jwtUtils;
      @Resource
      UserService userService;
    
      /**
       * 默认账号密码:pony / 111111
       */
      @CrossOrigin
      @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("email", user.getEmail())
                  .map()
          );
      }
    
      // 退出
      @GetMapping("/logout")
      @RequiresAuthentication
      public Result logout() {
          SecurityUtils.getSubject().logout();
          return Result.success(null);
      }
    
    }
    Login interface test

    image.png

    10. Blog interface development

  • ShiroUtils

    package com.pony.util;
    
    import com.pony.shiro.AccountProfile;
    import org.apache.shiro.SecurityUtils;
    
    /**
     * @author malf
     * @description
     * @date 2021/5/22
     * @project springboot_blog
     */
    public class ShiroUtils {
    
      public static AccountProfile getProfile() {
          return (AccountProfile) SecurityUtils.getSubject().getPrincipal();
      }
    
    }
  • Blog operation entry

    package com.pony.controller;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.lang.Assert;
    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.pony.common.Result;
    import com.pony.entity.Blog;
    import com.pony.service.BlogService;
    import com.pony.util.ShiroUtils;
    import org.apache.shiro.authz.annotation.RequiresAuthentication;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    import java.time.LocalDateTime;
    
    /**
     * <p>
     * 前端控制器
     * </p>
     *
     * @author pony
     * @since 2021-05-22
     */
    @RestController
    public class BlogController {
    
      @Resource
      BlogService blogService;
    
      @GetMapping("/blogs")
      public Result blogs(Integer currentPage) {
          if (currentPage == null || currentPage < 1) currentPage = 1;
          Page page = new Page(currentPage, 5);
          IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));
          return Result.success(pageData);
      }
    
      @GetMapping("/blog/{id}")
      public Result detail(@PathVariable(name = "id") Long id) {
          Blog blog = blogService.getById(id);
          Assert.notNull(blog, "该博客已删除!");
          return Result.success(blog);
      }
    
      @RequiresAuthentication
      @PostMapping("/blog/edit")
      public Result edit(@Validated @RequestBody Blog blog) {
          System.out.println(blog.toString());
          Blog temp = null;
          if (blog.getId() != null) {
              temp = blogService.getById(blog.getId());
              Assert.isTrue(temp.getUserId() == ShiroUtils.getProfile().getId(), "没有权限编辑");
          } else {
              temp = new Blog();
              temp.setUserId(ShiroUtils.getProfile().getId());
              temp.setCreated(LocalDateTime.now());
              temp.setStatus(0);
          }
          BeanUtil.copyProperties(blog, temp, "id", "userId", "created", "status");
          blogService.saveOrUpdate(temp);
          return Result.success("操作成功", null);
      }
    
    }
  • Interface test
    image.png
    So far, the development of the back-end interface is basically completed.

    Source code reference

    springboot_blog


巅峰小词典
948 声望1.3k 粉丝

百无一用是书生,春鸟秋虫自做声。