Part01-集成MP完成项目初始化

blog-tiny
│  pom.xml

└─src
    └─main
        ├─java
        │  └─org
        │      └─org.myslayers
        │          │  Application.java
        │          │  CodeGenerator.java
        │          │
        │          ├─common
        │          │  └─lang
        │          │          Result.java
        │          │
        │          ├─config
        │          │      MyBatisPlusConfig.java
        │          │
        │          ├─controller
        │          │      BaseController.java
        │          │      PostController.java
        │          │      UserController.java
        │          │
        │          ├─entity
        │          │      BaseEntity.java
        │          │      Post.java
        │          │      User.java
        │          │
        │          ├─mapper
        │          │      PostMapper.java
        │          │      UserMapper.java
        │          │
        │          └─service
        │              │  PostService.java
        │              │  UserService.java
        │              │
        │              └─impl
        │                      PostServiceImpl.java
        │                      UserServiceImpl.java

        └─resources
            │  application-win.yml
            │  application.yml
            │  spy.properties

            ├─mapper
            │      PostMapper.xml
            │      UserMapper.xml

            ├─static
            └─templates

1 MP 环境

  • pom.xml :项目依赖
<dependencies>
  <!--mp、druid、mysql、mp-generator(MyBatis-Plus 从 3.0.3后移除了代码生成器与模板引擎的默认依赖)、MP支持的SQL分析器、MP代码生成使用 freemarker 模板引擎-->
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
  </dependency>
  <!--        <dependency>-->
  <!--            <groupId>com.alibaba</groupId>-->
  <!--            <artifactId>druid-spring-boot-starter</artifactId>-->
  <!--            <version>1.1.10</version>-->
  <!--        </dependency>-->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.2.0</version>
  </dependency>
  <dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.8.6</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
  </dependency>
</dependencies>
  • application.yml :配置文件,【识别 Mapper 层】
spring:
  datasource:
    #    driver-class-name: com.mysql.cj.jdbc.Driver
    #    url: jdbc:mysql://127.0.0.1:3306/xblog_tiny?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://127.0.0.1:3306/xblog_tiny?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456
 
server:
  port: 8080
 
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
  • spy.properties :配置文件,【p6spy 组件对应的 spy.properties 配置】
#3.2.1以下使用或者不配置
module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2

2 MP 代码生成器

  • CodeGenerator.java:代码生成器,【配置 MySQL 相关信息,比如表名、用户名、密码】
// 演示例子,执行 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();
        final String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("org/myslayers");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);
 
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl(
            "jdbc:mysql://localhost:3306/xblog_tiny?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("org.org.myslayers");
        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();
 
        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();
 
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
 
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // 你自己的父类实体,没有就不用设置!
        strategy.setSuperEntityClass("org.org.myslayers.entity.BaseEntity");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 你自己的父类控制器,没有就不用设置!
        strategy.setSuperControllerClass("org.org.myslayers.controller.BaseController");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setSuperEntityColumns("id", "created", "modified");
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

3 MP 分页

  • MyBatisPlusConfig.java :配置类
/**
 * MP配置类
 */
@Configuration
@EnableTransactionManagement
@MapperScan("org.org.myslayers.mapper")
public class MyBatisPlusConfig {
 
    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        return paginationInterceptor;
    }
}

4 前后端分离,统一封装返回结果

  • Result.java :实体类,【前后端分离,统一封装返回结果】
/**
 * 统一封装返回结果
 */
@Data
public class Result implements Serializable {
 
    // 操作状态:200代表成功,非200为失败/异常
    private int code;
 
    // 携带msg
    private String msg;
 
    // 携带data
    private Object data;
 
    public static Result success(int code, String msg, Object data) {
        Result result = new Result();
        result.code = code;
        result.msg = msg;
        result.data = data;
        return result;
    }
 
    public static Result success(String msg) {
        return Result.success(200, msg, null);
    }
 
    public static Result success(String msg, Object data) {
        return Result.success(200, msg, data);
    }
 
    public static Result fail(int code, String msg, Object data) {
        Result result = new Result();
        result.code = code;
        result.msg = msg;
        result.data = data;
        return result;
    }
 
    public static Result fail(String msg) {
        return fail(500, msg, null);
    }
 
    public static Result fail(String msg, Object data) {
        return fail(500, msg, data);
    }
}

Part02-集成Shiro-Redis-Jwt实现会话共享(一)

blog-tiny
│  pom.xml

└─src
    └─main
        ├─java
        │  └─org
        │      └─org.myslayers
        │          ├─config
        │          │      ShiroConfig.java
        │          │
        │          ├─shiro
        │          │      AccountProfile.java
        │          │      AccountRealm.java
        │          │      JwtFilter.java
        │          │      JwtToken.java
        │          │
        │          └─utils
        │                  JwtUtils.java

        └─resources
            │  application-win.yml
            │  application.yml

            └─META-INF
                    spring-devtools.properties

5 集成 Shiro-Redis、Jwt 环境

  • pom.xml :项目依赖,【shiro-redis、jwt 实现会话共享、身份验证】
<dependencies>
  <!--shiro-redis、jwt 实现会话共享、身份验证-->
  <dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis-spring-boot-starter</artifactId>
    <version>3.2.1</version>
  </dependency>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
  </dependency>
</dependencies>

6 编写 ShiroConfig.java 配置文件

  • ShiroConfig.java :配置类,【安全管理器、过滤器链、过滤器工厂】
/**
 * 配置类:安全管理器、过滤器链、过滤器工厂
 */
@Configuration
public class ShiroConfig {
 
    @Autowired
    JwtFilter jwtFilter;
 
    /**
     * 1.安全管理器:根据 “https://github.com/alexxiyang/shiro-redis/tree/master/docs” 说明,将 【自定义Realm】、【自定义的session会话管理器】、【自定义的redis缓存管理器】 注入 DefaultWebSecurityManager,并关闭shiro自带的session
     *
     * 具体内容如下:
     * - 引入 RedisSessionDAO 和 RedisCacheManager,为了解决 shiro 的权限数据和会话信息能保存到 redis 中,实现会话共享
     * - 重写 SessionManager 和 DefaultWebSecurityManager,同时在 DefaultWebSecurityManager 中为了关闭 shiro 自带的 session 方式,我们需要设置为 false,这样用户就不再能通过 session 方式登录 shiro,而后将采用 jwt 凭证登录
     */
    @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);
        // 将 自定义Realm 注册到安全管理器中
        securityManager.setRealm(accountRealm);
 
        // 将 自定义的session会话管理器 注册到安全管理器中
        securityManager.setSessionManager(sessionManager);
 
        // 将 自定义的redis缓存管理器 注册到安全管理器中
        securityManager.setCacheManager(redisCacheManager);
 
        // 关闭 shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
 
        return securityManager;
    }
 
    /**
     * 2.过滤器链
     *
     * 具体内容如下:不再通过编码形式拦截 Controller 访问路径,而将全部路由经过 JwtFilter 处理
     * - 如果 JwtFilter 在判断请求头时,如果存在 jwt 信息,校验 jwt 的有效性,如果有效,则直接执行 executeLogin 方法实现自动登录
     * - 如果 JwtFilter 在判断请求头时,如果没有 jwt 信息,则跳过;跳过之后,有 Controller 中的 Shiro 注解【@RequiresAuthentication】 来控制权限访问
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt");   // 将全部路由交给 JwtFilter 过滤器进行处理
        chainDefinition.addPathDefinitions(filterMap);
 
        return chainDefinition;
    }
 
    /**
     * 3.过滤器工厂
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        // shiroFilter 设置 自定义安全管理器(securityManager)
        shiroFilter.setSecurityManager(securityManager);
 
        // shiroFilter 设置 自定义jwtFilter
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
 
        // shiroFilter 设置 自定义过滤器链(chainDefinition)
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
 
        return shiroFilter;
    }
 
}

7 编写 JwtFilter.java 配置文件

  • 采用 Jwt 作为跨域身份验证解决方案,原理如下:

  • JwtFilter.java :Filter,【继承 Shiro 内置的 AuthenticatingFilter】
/**
 * Filter:继承 Shiro 内置的 AuthenticatingFilter
 */
@Component
public class JwtFilter extends AuthenticatingFilter {
 
    @Autowired
    JwtUtils jwtUtils;
 
    /**
     * createToken:实现登录,【从 request 的 header 中获取 自定义的jwt(Authorization),然后生成 JwtToken 信息并返回】
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            return null;
        }
        return new JwtToken(jwt);
    }
 
    /**
     * onAccessDenied:拦截校验,
     * - 如果 JwtFilter 在判断请求头时,如果存在 jwt 信息,校验 jwt 的有效性,如果有效,则直接执行 executeLogin 方法实现自动登录
     * - 如果 JwtFilter 在判断请求头时,如果没有 jwt 信息,则跳过;跳过之后,有 Controller 中的 Shiro 注解【@RequiresAuthentication】 来控制权限访问
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        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);
        }
    }
 
    /**
     * onLoginFailure:登录异常时候进入的方法,【前后端项目,使用JSON格式的数据,将异常信息封装然后抛出】
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result result = Result.fail(throwable.getMessage());
        String json = JSONUtil.toJsonStr(result);
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {
        }
        return false;
    }
 
    /**
     * preHandle:拦截器的前置拦截,【前后端项目,除了需要跨域全局配置之外,拦截器中也需要提供跨域支持。这样,拦截器才不会在进入 Controller 之前就被限制了】
     */
    @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);
    }
 
}
  • JwtUtils.java :生成和校验 jwt 的工具类:【来自 application.yml 配置文件】
/**
 * 生成和校验 jwt 的工具类:【来自 application.yml 配置文件】
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "org.myslayers.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();
    }
 
    /**
     * 获取 jwt 信息
     */
    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 是否过期,true代表过期;false代表有效
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}
  • application.yml :配置文件,用于 JwtUtils.java 的 属性注入,【开启 shiro-redis 会话管理、向 JwtUtils.java 类中的属性注入值】
shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379
    password: org.myslayers
 
org.myslayers:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效时长,7天,单位秒
    expire: 604800
    # header
    header: authorization
  • spring-devtools.properties :配置文件,【项目使用 spring-boot-devtools 时,在热部署的情况下,防止重启报错】
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

8 编写 AccountRealm.java 配置文件

  • AccountRealm.java :Realm,【继承 Shiro 内置的 AuthorizingRealm】
/**
 * Realm:继承 Shiro 内置的 AuthorizingRealm
 */
@Component
public class AccountRealm extends AuthorizingRealm {
 
    @Autowired
    JwtUtils jwtUtils;
 
    @Autowired
    UserService userService;
 
    /**
     * supports:让 realm 支持 jwt 的凭证校验,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
 
    /**
     * doGetAuthorizationInfo(授权):权限校验
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
 
    /**
     * doGetAuthenticationInfo(认证):登录认证,【通过 JwtToken 获取到用户信息 来 判断用户的状态,并抛出对应的异常信息】,同时,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
     */
    @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); //将 user对象 拷贝一份至 AccountProfile 对象
        return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
    }
}
  • JwtToken.java :Token,用于 AccountRealm.java 的 supports 方法,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
/**
 * Token:用于 AccountRealm.java 的 supports 方法,【shiro 默认 supports 是 UsernamePasswordToken,使用 jwt 的方式,需要自定义 JwtToken 取代 UsernamePasswordToken】
 */
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;
    }
}
  • AccountProfile.java :Profile,用于 AccountRealm.java 的 AuthenticationInfo 方法,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
/**
 * Profile:用于 AccountRealm.java 的 AuthenticationInfo 方法,【登录成功之后返回的一个用户信息的载体,将 user 信息封装成 SimpleAuthenticationInfo 对象 返回给 shiro】
 */
@Data
public class AccountProfile implements Serializable {
 
    private Long id;
 
    private String username;
 
    private String avatar;
 
    private String email;
 
}
  • AuthenticatingFilter.java :源码分析,executeLogin() 执行登录时,会使用 JWT(token),并委托 AccountRealm.java 中的 doGetAuthenticationInfo 方法进行登录认证
public abstract class AuthenticatingFilter extends AuthenticationFilter {
 
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        } else {
            try {
                // 使用token,并委托 AccountRealm.java 中的 doGetAuthenticationInfo 方法进行登录认证
                Subject subject = this.getSubject(request, response);
                subject.login(token);
                return this.onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException var5) {
                return this.onLoginFailure(token, var5, request, response);
            }
        }
    }
}

Part03-集成Shiro-Redis-Jwt实现会话共享(二)

blog-tiny
└─src
    └─main
        ├─java
        │  └─org
        │      └─org.myslayers
        │          │
        │          ├─common
        │          │  ├─exception
        │          │  │       GlobalExceptionHandler.java.java  # 全局异常
        │          │  │
        │          │  └─lang
        │          │          Result.java   # 统一封装返回结果
        │          │
        │          ├─config
        │          │      CorsConfig.java.java  # 跨越问题
        │          │
        │          ├─entity
        │          │      User.java # 表单校验
        │          │
        │          ├─shiro
        │          │      JwtFilter.java  # 跨越问题

9 前后端分离:统一封装返回结果

  • Result.java :实体类,【前后端分离,统一封装返回结果】
/**
 * 统一封装返回结果
 */
@Data
public class Result implements Serializable {
 
    // 操作状态:200代表成功,非200为失败/异常
    private int code;
 
    // 携带msg
    private String msg;
 
    // 携带data
    private Object data;
 
    public static Result success(int code, String msg, Object data) {
        Result result = new Result();
        result.code = code;
        result.msg = msg;
        result.data = data;
        return result;
    }
 
    public static Result success(String msg) {
        return Result.success(200, msg, null);
    }
 
    public static Result success(String msg, Object data) {
        return Result.success(200, msg, data);
    }
 
    public static Result fail(int code, String msg, Object data) {
        Result result = new Result();
        result.code = code;
        result.msg = msg;
        result.data = data;
        return result;
    }
 
    public static Result fail(String msg) {
        return fail(500, msg, null);
    }
 
    public static Result fail(String msg, Object data) {
        return fail(500, msg, data);
    }
}

10 前后端分离:全局异常

  • 默认情况下,返回 Tomcat 或 Nginx 报错页面,对用户不太友好,除此之外,可以根据实际需要对状态码进一步处理。

  • GlobalExceptionHandler.java :全局异常,【Shiro 抛出的异常、处理实体校验的异常、处理 Assert 的异常、处理 Runtime 异常】

/**
 * 全局异常
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    //ShiroException:Shiro抛出的异常,比如用户权限、用户登录
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(value = ShiroException.class)
    public Result handler(ShiroException e) {
        log.error("运行时异常:----------------{}", e);
        return Result.fail(401, e.getMessage(), null);
    }
 
    //MethodArgumentNotValidException:处理实体校验的异常
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
        log.error("实体校验异常:----------------{}", e);
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
 
        return Result.fail(objectError.getDefaultMessage());
    }
 
    //IllegalArgumentException:处理 Assert 的异常
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) {
        log.error("Assert异常:----------------{}", e);
        return Result.fail(e.getMessage());
    }
 
    //RuntimeException:处理 Runtime 异常
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) {
        log.error("运行时异常:----------------{}", e);
        return Result.fail(e.getMessage());
    }
 
}
  • UserController.java :控制层,【测试全局异常是否生效,使用 @RequiresAuthentication 注解】
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
 
    @RequiresAuthentication //需要用户登录,否则无法访问该接口
    @GetMapping({"", "/", "/index", "/index.html"})
    public Object index() {
        User user = userService.getById(1);
        return Result.success(200, "操作成功!", user);
    }
}
  • 效果图如下:

11 前后端分离:表单校验

  • 默认提交表单数据时,前端校验可以使用类似于 jQuery Validate 等 js 插件实现,而后端校验可以使用 Hibernate validatior 来做校验。注:SpringBoot 默认集成 Hibernate validatior 校验

  • User.java :实体类

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("m_user")
public class User extends BaseEntity {
 
    private static final long serialVersionUID = 1L;
 
    /**
     * 用户的【昵称】
     */
    @NotBlank(message = "昵称不能为空")
    private String username;
 
    /**
     * 用户的【密码】
     */
    @NotBlank(message = "密码不能为空")
    private String password;
 
    /**
     * 用户的【邮件】
     */
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
 
    /**
     * 用户的【性别】:0代表男,1代表女
     */
    private String gender;
 
    /**
     * 用户的【头像】
     */
    private String avatar;
 
    /**
     * 用户的【状态】:0代表登录成功,-1代表登录失败
     */
    private Integer status;
 
    /**
     * 用户的【近期登陆日期】
     */
    private Date lasted;
}
  • Post.java :实体类
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("m_post")
public class Post extends BaseEntity {
 
    private static final long serialVersionUID = 1L;
 
    /**
     * 文章的【用户ID】
     */
    private Long userId;
 
    /**
     * 文章的【标题】
     */
    @NotBlank(message = "标题不能为空")
    private String title;
 
    /**
     * 文章的【描述】
     */
    @NotBlank(message = "描述不能为空")
    private String description;
 
    /**
     * 文章的【内容】
     */
    @NotBlank(message = "内容不能为空")
    private String content;
 
    /**
     * 文章的【状态】
     */
    private Integer status;
 
}
  • UserController.java :控制层,【测试表单校验是否生效,使用 @Validated 注解】
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
 
    @PostMapping("/save")
    public Object testUser(@Validated @RequestBody User user) {
        return user.toString();
    }
}
  • 效果图如下:

12 前后端分离:跨域问题

  • CorsConfig.java :配置类,【前后端分离,跨域问题】
/**
 * 前后端分离:跨域问题
 */
@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 :配置类,【前后端分离,拦截器中加入跨域支持,这样拦截器才不会在进入 Controller 之前就被限制了】
/**
 * Filter:继承 Shiro 内置的 AuthenticatingFilter
 */
@Component
public class JwtFilter extends AuthenticatingFilter {
 
    /**
     * preHandle:拦截器的前置拦截,【前后端项目,除了需要跨域全局配置之外,拦截器中也需要提供跨域支持。这样,拦截器才不会在进入 Controller 之前就被限制了】
     */
    @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);
    }
 
}

Part04-使用Shiro-Redis-Jwt开发登录接口

blog-tiny
└─src
    └─main
        ├─java
        │  └─org
        │      └─org.myslayers
        │          ├─controller
        │          │      UserController.java   # 登录接口
        │          │
        │          └─utils
        │                 ValidationUtil.java  # 工具类

13 集成 ValidationUtil 工具类

  • ValidationUtil.java :工具类,【常见校验】
/**
 * ValidationUtil 工具类
 */
@Component
public class ValidationUtil {
 
    /**
     * 开启快速结束模式 failFast (true)
     */
    private static Validator validator = Validation.byProvider(HibernateValidator.class).configure()
        .failFast(false).buildValidatorFactory().getValidator();
 
    /**
     * 校验对象
     *
     * @param t      bean
     * @param groups 校验组
     * @return ValidResult
     */
    public static <T> ValidResult validateBean(T t, Class<?>... groups) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(violation.getPropertyPath().toString(), violation.getMessage());
            }
        }
        return result;
    }
 
    /**
     * 校验bean的某一个属性
     *
     * @param obj          bean
     * @param propertyName 属性名称
     * @return ValidResult
     */
    public static <T> ValidResult validateProperty(T obj, String propertyName) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(propertyName, violation.getMessage());
            }
        }
        return result;
    }
 
    /**
     * 校验结果类
     */
    @Data
    public class ValidResult {
 
        /**
         * 是否有错误
         */
        private boolean hasErrors;
 
        /**
         * 错误信息
         */
        private List<ErrorMessage> errors;
 
        public ValidResult() {
            this.errors = new ArrayList<>();
        }
 
        public boolean hasErrors() {
            return hasErrors;
        }
 
        public void setHasErrors(boolean hasErrors) {
            this.hasErrors = hasErrors;
        }
 
        /**
         * 获取所有验证信息
         *
         * @return 集合形式
         */
        public List<ErrorMessage> getAllErrors() {
            return errors;
        }
 
        /**
         * 获取所有验证信息
         *
         * @return 字符串形式
         */
        public String getErrors() {
            StringBuilder sb = new StringBuilder();
            for (ErrorMessage error : errors) {
                sb.append(error.getPropertyPath()).append(":").append(error.getMessage())
                    .append(" ");
            }
            return sb.toString();
        }
 
        public void addError(String propertyName, String message) {
            this.errors.add(new ErrorMessage(propertyName, message));
        }
    }
 
    @Data
    public class ErrorMessage {
 
        private String propertyPath;
 
        private String message;
 
        public ErrorMessage() {
        }
 
        public ErrorMessage(String propertyPath, String message) {
            this.propertyPath = propertyPath;
            this.message = message;
        }
    }
 
}

14 集成 Shiro-Redis、Jwt 开发登录接口

  • UserController.java :控制层,【用户登录/登出】
@RestController
public class UserController extends BaseController {
 
    @Autowired
    UserService userService;
 
    @Autowired
    JwtUtils jwtUtils;
 
    /*--------------------------------------1.用户登录/登出------------------------------------>*/
 
    @ResponseBody
    @PostMapping("/login")
    public Result login(@RequestBody Map<String, String> map) {
        //使用Map对象接收一个json对象
        String username = map.get("username");
        String password = map.get("password");
 
        //判断输入是否为空
        if (StrUtil.isEmpty(username) || StrUtil.isBlank(password)) {
            return Result.fail("账号或密码不能为空");
        }
        //根据username查询该用户
        User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
        //判断用户是否存在
        Assert.notNull(user, "用户不存在!");
        //判断密码是否正确
        if (!user.getPassword().equals(SecureUtil.md5(password))) {
            return Result.fail("密码不正确!");
        }
 
        //登录成功后,根据 用户id 生成 jwt token,并将 jwt 返回至 response 的 header 请求头中
        String jwt = jwtUtils.generateToken(user.getId());
        resp.setHeader("Authorization", jwt);
        resp.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()
        );
    }
 
    @RequiresAuthentication
    @GetMapping("/logout")
    public Result logout() {
        SecurityUtils.getSubject().logout();
        return Result.success("退出登录!");
    }
 
}
  • 效果图如下:

  • 效果图如下:

Part05-使用Shiro-Redis-Jwt开发项目接口

blog-tiny
└─src
    └─main
        ├─java
        │  └─org
        │      └─org.myslayers
        │          ├─controller
        │          │      UserController.java   # 项目接口

15 集成 Shiro-Redis、Jwt 开发项目接口

@RestController
public class PostController extends BaseController {
 
    @Autowired
    PostService PostService;
 
    /**
     * 【查询】全部文章
     */
    @GetMapping("/post/list")
    public Result list(@RequestParam(defaultValue = "1") Integer currentPage) {
        IPage pageData = PostService.page(new Page(currentPage, 5), new QueryWrapper<Post>().orderByDesc("created"));
        return Result.success("操作成功!", pageData);
    }
 
    /**
     * 【查看】文章
     */
    @GetMapping("/post/{id}")
    public Result detail(@PathVariable(name = "id") Long id) {
        Post post = PostService.getById(id);
        Assert.notNull(post, "该博客已被删除");
        return Result.success("操作成功", post);
    }
 
    /**
     * 【更新/添加】文章
     */
    @RequiresAuthentication
    @PostMapping("/post/edit")
    public Result edit(@Validated @RequestBody Post post) {
        Post temp = null;
 
        /**
         * 编辑文章:存在文章id,则判断【是否拥有权限编辑该篇文章】
         */
        if (post.getId() != null) {
            // 登录成功之后返回的登录用户的id 与 当前操作的文章的用户id 比较,判断【是否拥有权限编辑该篇文章】
            temp = PostService.getById(post.getId());
            Assert.isTrue(temp.getUserId().longValue() == getProfile().getId().longValue(), "没有权限编辑该篇文章!");
        } else {
            /**
             * 添加文章:不存在文章id,则进行字段补充,将post添加至数据库
             */
            // 对传入的post文章,进行字段补充,比如userId、status、created
            temp = new Post();
            temp.setUserId(getProfile().getId());
            temp.setStatus(0);
            temp.setCreated(new Date());
            temp.setModified(new Date());
        }
 
        // 从 post 拷贝至 temp,忽略"id", "userId", "created", "status"
        BeanUtil.copyProperties(post, temp, "id", "userId", "created", "status", "modified");
        PostService.saveOrUpdate(temp);
 
        return Result.success("操作成功!", temp);
    }
}
  • 效果图如下:

  • 效果图如下:

  • 效果图如下:

Part06-完成项目搭建-vuecli、elementui、axios

blog-tiny-vue
│  .gitignore
│  babel.config.js
│  blog-tiny-vue.iml
│  package-lock.json
│  package.json
│  README.md

├─node_modules

├─public
│      favicon.ico
│      index.html

└─src
    │  App.vue
    │  main.js

    ├─assets
    │      logo.png

    ├─components
    │      HelloWorld.vue

    ├─router
    │      index.js

    ├─store
    │      index.js

    └─views
            About.vue
            Home.vue

16 使用 vuecli 完成项目搭建

  • 初始化项目,命令如下:
a.安装
    npm install -g @vue/cli && npm install -g webpack && npm install -g webpack-cli
b.版本
    vue -V && webpack -v
    @vue/cli 4.5.12、webpack 5.30.0、webpack-cli 4.6.0
c.创建
    cd D:\software_ware\workspace_idea\blog-tiny && vue ui
d.相关配置
    项目名称:blog-tiny-vue
    包管理器:npm
    Git版本控制:关闭
    手动配置项目:Choose vue version(勾选)、Babel(勾选)、Router(勾选)、Vuex(勾选)、Linter/ Formatter(取消勾选)
    功能及配置:Vue.js使用2.x(默认)、是否使用旧的路由模式(是)
    保存为新预设:暂不设置

17 使用 elementui 完成项目搭建

  • 初始化项目,命令如下:
a.安装
    cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
    cnpm install element-ui --save                  # 安装 ElementUi
    yarn add element-ui                             # 安装 ElementUi
b.配置
    cd src/mian.js
    ------------------------------------------------------------
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import Element from 'element-ui'                # 添加该行
    import "element-ui/lib/theme-chalk/index.css"   # 添加该行
 
    Vue.config.productionTip = false;
 
    Vue.use(Element);                               # 添加该行
 
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app');

18 使用 axios 完成项目搭建

  • 初始化项目,命令如下:
a.安装
    cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
    cnpm install axios --save                       # 安装 Axios
    yarn add axios                                  # 安装 Axios
b.配置
    cd src/mian.js
    ------------------------------------------------------------
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import Element from 'element-ui'
    import "element-ui/lib/theme-chalk/index.css"
    import axios from 'axios'                       # 添加该行
 
    Vue.config.productionTip = false;
 
    Vue.use(Element);
    Vue.prototype.$axios = axios                    # 添加该行
 
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app');

Part07-实现用户登录-页面渲染、路由规则、状态管理、全局响应拦截、全局前置守卫

blog-tiny-vue

└─src
    │  App.vue
    │  axios.js                   # 2.4 用户登录 - 全局响应拦截
    │  main.js
    │  permission.js              # 2.5 用户登录 - 全局前置守卫

    ├─router
    │      index.js               # 2.2 用户登录 - 路由规则

    ├─store
    │      index.js               # 2.3 用户登录 - 状态管理

    └─views
            Login.vue             # 2.1 用户登录 - 页面渲染

19 用户登录 - 页面渲染

  • /views/Login.vue :页面渲染
<template>
  <div>
    <el-container>
      <el-header>
        <img class="logo" src="https://www.markerhub.com/dist/images/logo/markerhub-logo.png">
      </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: 'admin',
        password: '123456',
      },
      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
 
          // axios.js 中 axios.defaults.baseURL + /login 等同于 http://localhost:8765/login
          this.$axios.post('/login', this.ruleForm).then(res => {
            //请求后的数据
            console.log(res.data)
            console.log(res.headers)
 
            const token = res.headers['authorization']
            const userInfo = res.data.data
 
            //调用vuex中的set方法
            _this.$store.commit("SET_TOKEN", token);
            _this.$store.commit("SET_USERINFO", userInfo);
 
            //获取vuex中的get方法
            console.log(_this.$store.getters.GET_TOKEN);
            console.log(_this.$store.getters.GET_USERINFO);
            console.log(_this.$store.getters.GET_USERINFO.avatar);
            console.log(_this.$store.getters.GET_USERINFO.username);
 
            //跳转页面
            _this.$router.push("/");
          })
 
        } else {
          console.log('登录失败!!!');
          return false;
        }
      });
    },
 
    resetForm(formName) {
      this.$refs[formName].resetFields();
    }
 
  }
}
</script>
 
<style>
.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 {
  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;
}
 
/*----------------自定义样式----------------*/
.logo {
  height: 60%;
  margin-top: 10px;
}
 
.demo-ruleForm {
  max-width: 500px;
  margin: 0 auto;
}
 
</style>

20 用户登录 - 路由规则

  • /router/index.js :路由规则
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import PostList from "@/views/PostList";
import PostDetail from "@/views/PostDetail";
import PostEdit from "@/views/PostEdit";
 
Vue.use(VueRouter);
 
const routes = [
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/',
    name: 'Index',
    redirect: {name: "PostList"}   /*重定向至 Blogs 组件*/
  },
  {
    path: '/post/list',
    name: 'PostList',
    component: PostList           /*主页【查看全部文章】*/
  },
  {
    path: '/post/add',
    name: 'PostAdd',
    component: PostEdit,          /*发表【新建一篇文章】*/
    meta: {
      requireAuth: true           /*对应【权限拦截】自定义规则*/
    }
  },
  {
    path: '/post/:postId',
    name: 'PostDetail',
    component: PostDetail         /*详情【查看某篇文章】*/
  },
  {
    path: '/post/:postId/edit',
    name: 'PostEdit',
    component: PostEdit,          /*编辑【编辑某篇文章】*/
    meta: {
      requireAuth: true           /*对应【权限拦截】自定义规则*/
    }
  }
];
 
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
 
export default router

21 用户登录 - 状态管理

  • /store/index.js :状态管理,【用途一:将 token、userInfo 存储到 vuex 中供其他组件调用;用途二:将其存储到 localStorage 中供下次打开浏览器使用】
import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)
 
export default new Vuex.Store({
  //attr属性
  state: {
    token: localStorage.getItem("token"),
    userInfo: JSON.parse(localStorage.getItem("userInfo"))
  },
 
  //set方法
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token;
      localStorage.setItem("token", token)
    },
    SET_USERINFO: (state, userInfo) => {
      state.userInfo = userInfo;
      localStorage.setItem("userInfo", JSON.stringify(userInfo))
    },
    REMOVE_ALL: (state) => {
      state.token = ''
      state.userInfo = {}
      localStorage.setItem("token", '')
      localStorage.setItem("userInfo", JSON.stringify(''))
    },
  },
 
  //get方法
  getters: {
    GET_TOKEN: state => {
      return state.token;
    },
    GET_USERINFO: state => {
      return state.userInfo;
    }
  },
 
  actions: {},
  modules: {}
})
import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)
 
export default new Vuex.Store({
  //attr属性
  state: {
    token: localStorage.getItem("token"),
    userInfo: JSON.parse(localStorage.getItem("userInfo"))
  },
 
  //set方法
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token;
      localStorage.setItem("token", token)
    },
    SET_USERINFO: (state, userInfo) => {
      state.userInfo = userInfo;
      localStorage.setItem("userInfo", JSON.stringify(userInfo))
    },
    REMOVE_ALL: (state) => {
      state.token = ''
      state.userInfo = {}
      localStorage.setItem("token", '')
      localStorage.setItem("userInfo", JSON.stringify(''))
    },
  },
 
  //get方法
  getters: {
    GET_TOKEN: state => {
      return state.token;
    },
    GET_USERINFO: state => {
      return state.userInfo;
    }
  },
 
  actions: {},
  modules: {}
})

22 用户登录 - 全局响应拦截

  • axios.js :编写【全局响应拦截】,【对应 GlobalExceptionHandler.java 全局异常】
import axios from 'axios'
import Element from 'element-ui'
 
axios.defaults.baseURL = "http://localhost:8765"
 
/**
 * 前置拦截
 */
axios.interceptors.request.use(request => {
  return request
})
 
/**
 * 后置拦截
 */
axios.interceptors.response.use(
    //状态码错误
    succ => {
      //状态码,200
      if (succ.data.code === 200) {
        return succ
      }
 
      //状态码,非200
      if (succ.data.code !== 200) {
        var mess = succ.data.msg;
        Element.Message.error(mess, {duration: 3 * 1000}) //消息弹窗
        return Promise.reject(mess)
      }
    },
 
    //程序运行错误
    error => {
      //处理Assert的异常,比如:Assert.notNull(user, "用户不存在!");
      if (error.response.data) {
        var mess = error.response.data.msg;
        Element.Message.error(mess, {duration: 3 * 1000})  //消息弹窗
        return Promise.reject(mess)
      }
 
      //处理Shiro的异常,比如:用户权限、用户登录
      if (error.response.status === 401) {
        this.$store.commit("REMOVE_ALL")
        this.$router.push("/login")
      }
    }
)
  • main.js :使用【全局响应拦截】,【直接将编写好的 axios.js 导入 main.js】
import "./axios" /*axios全局拦截:前置拦截、后置拦截*/

23 用户登录 - 全局前置守卫

  • permission.js :编写【全局前置守卫】,【对应 /router/index.js 路由规则】
import router from "./router";
 
//权限拦截:根据【/router/index.js】中的路由规则,是否需要进行【权限验证:登录】
router.beforeEach((to, from, next) => {
 
  //判断该路由是否需要登录权限
  if (to.matched.some(record => record.meta.requireAuth)) {
    const token = localStorage.getItem("token")
    console.log(token)
    //判断当前的token是否存在
    if (token) {
      if (to.path === '/login') {
 
      } else {
        next()
      }
    } else {
      next({
        path: '/login'
      })
    }
  } else {
    next()
  }
})
  • main.js :使用【全局前置守卫】,【直接将编写好的 permission.js 导入 main.js】
import "./permission" /*全局前置守卫*/

Part08-实现博客页面-公共组件、博客列表、博客发表、博客编辑、博客详情

blog-tiny-vue

└─src
    ├─components
    │      Header.vue             # 2.1 公共组件

    └─views
            Login.vue
            PostDetail.vue        # 2.4 博客详情
            PostEdit.vue          # 2.3 博客发表 / 博客编辑
            PostList.vue          # 2.2 博客列表

24 博客页面 - 公共组件

  • /components/Header.vue :编写【公共组件】
<template>
  <div style="max-width: 960px; margin: 0 auto; text-align: center;">
    <h3>欢迎来到Halavah的博客!</h3>
 
    <div class="block">
      <el-avatar :size="50" :src="user.avatar"></el-avatar>
      <div>{{ user.username }}</div>
    </div>
 
    <div style="margin: 10px 0;">
      <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="!isLogin"><el-link type="primary" href="/login">登录</el-link></span>
      <span v-show="isLogin"><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'
      },
      isLogin: false
    }
  },
  methods: {
    logout() {
      const _this = this
      _this.$axios.get("/logout", {
        headers: {
          "authorization": this.$store.getters.GET_TOKEN
        },
      }).then(res => {
        _this.$store.commit("REMOVE_ALL")
        _this.$router.push("/login")
      })
    }
  },
  created() {
    if(this.$store.getters.GET_USERINFO.username) {
      this.user.username = this.$store.getters.GET_USERINFO.username
      this.user.avatar = this.$store.getters.GET_USERINFO.avatar
      this.isLogin = true
    }
  }
}
</script>
 
<style>
 
</style>
  • /views/PostList.vue :使用【公共组件】
<template>
  <div>
    <Header></Header>
  </div>
</template>
 
<script>
import Header from "@/components/Header";
 
export default {
  name: "PostList",
  components: {Header},
}
</script>
 
<style>
 
</style>

25 博客页面 - 博客列表

  • /views/PostList.vue :博客列表
<template>
  <div>
    <Header></Header>
    <div>
      <div class="block">
        <!--时间线-->
        <el-timeline>
          <el-timeline-item :timestamp="post.created" placement="top" v-for="(post, index) in posts" :key="index">
            <el-card>
              <h4>
                <!--使用router-link来跳转到某个组件,例如跳转到【BlogDetail组件】,需要传递参数【blogId: post.id】-->
                <router-link :to="{name: 'PostDetail', params: {postId: post.id}}">{{ post.title }}</router-link>
              </h4>
              <p>{{ post.description }}</p>
            </el-card>
          </el-timeline-item>
        </el-timeline>
        <!--分页-->
        <el-pagination style="margin: 0 auto; text-align: center" background layout="prev, pager, next" :current-page="currentPage" :page-size="pageSize" :total="total" @current-change=handleCurrentChange></el-pagination>
      </div>
    </div>
  </div>
</template>
 
<script>
import Header from "@/components/Header";
 
export default {
  name: "PostList",
  components: {Header},
  data() {
    return {
      posts: {},
      currentPage: 1,
      total: 0,
      pageSize: 5
    }
  },
  methods: {
    handleCurrentChange(currentPage) {
      const _this = this
      _this.$axios.get("/post/list?currentPage=" + currentPage).then(res => {
        console.log(res)
        _this.posts = res.data.data.records
        _this.currentPage = res.data.data.current
        _this.total = res.data.data.total
        _this.pageSize = res.data.data.size
      })
    }
  },
 
  //页面一开始渲染时,调用mounted()方法
  mounted() {
    this.handleCurrentChange(1)
  }
}
</script>
 
<style>
 
</style>

26 博客页面 - 博客发表 / 博客编辑

  • 基于 Vue 的 markdown 编辑器(编写):mavon-editor
a.安装
    cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
    cnpm install mavon-editor --save                # 用于编写md文档
    yarn add mavon-editor                           # 用于编写md文档
b.配置
    cd src/mian.js
    ------------------------------------------------------------
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import Element from 'element-ui'
    import "element-ui/lib/theme-chalk/index.css"
    import mavonEditor from 'mavon-editor'          # 添加该行
    import 'mavon-editor/dist/css/index.css'        # 添加该行
    import axios from 'axios'
 
    import "./axios" /*axios全局拦截:前置拦截、后置拦截*/
 
    Vue.config.productionTip = false;
 
    Vue.use(Element);
    Vue.prototype.$axios = axios;
    Vue.use(mavonEditor);                           # 添加该行
 
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app');
c.使用
    /views/PostEdit.vue
    ------------------------------------------------------------
    <el-form-item label="内容" prop="content">
      <mavon-editor v-model="ruleForm.content"></mavon-editor>
    </el-form-item>
  • /views/PostEdit.vue :博客发表 / 博客编辑
<template>
  <div>
    <Header></Header>
    <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" style="max-width: 80%;">
      <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>
</template>
 
<script>
import Header from "@/components/Header";
 
export default {
  name: "PostEdit",
  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: [
          {required: true, message: '请输入内容', trigger: 'blur'}
        ]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          const _this = this
          this.$axios.post('/post/edit', this.ruleForm, {headers: {"authorization": localStorage.getItem("token")}})
          .then(res => {
            console.log(res)
            _this.$alert('操作成功', '提示', {      /*MessageBox 弹框*/
              confirmButtonText: '确定',
              callback: action => {
                _this.$router.push("/post/list")
              }
            });
          })
        } else {
          console.log('操作失败!!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    }
  },
 
  //页面一开始渲染时,调用mounted()方法(回写操作)
  mounted() {
    /*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
    const postId = this.$route.params.postId
    console.log(postId)
    const _this = this
    if (postId) {
      this.$axios.get('/post/' + postId).then(res => {
        const post = res.data.data
        _this.ruleForm.id = post.id
        _this.ruleForm.title = post.title
        _this.ruleForm.description = post.description
        _this.ruleForm.content = post.content
      })
    }
  }
}
</script>
 
<style>
 
</style>

27 博客页面 - 博客详情

  • 基于 Vue 的 markdown 编辑器(解析):markdown-it、github-markdown-css
a.安装
    cd D:\software_ware\workspace_idea\blog-tiny\blog-tiny-vue
    cnpm install markdown-it --save                # 用于解析md文档
    cnpm install github-markdown-css               # 用于解析md样式
    yarn add markdown-it                           # 用于解析md文档
    yarn add github-markdown-css                   # 用于解析md样式
b.配置

c.使用
    /views/PostDetail.vue
    ------------------------------------------------------------
    <template>
      <div>
        <div>
          <!--使用【解析md样式、解析md文档】-->
          <div class="markdown-body" v-html="post.content"></div>
        </div>
      </div>
    </template>
 
    <script>
    /*配置【解析md样式】*/
    import 'github-markdown-css'
 
    export default {
      //页面一开始渲染时,调用mounted()方法(回写操作)
      mounted() {
        /*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
        const postId = this.$route.params.postId
        console.log(postId)
        const _this = this
        this.$axios.get('/post/' + postId).then(res => {
          const post = res.data.data
          _this.post.id = post.id
          _this.post.title = post.title
 
          /*配置【解析md文档】*/
          var MardownIt = require("markdown-it")
          var md = new MardownIt()
          var result = md.render(post.content)
          _this.post.content = result
 
          _this.editButton = (post.userId === _this.$store.getters.GET_USERINFO.id)
        })
      }
    }
    </script>
  • /views/PostDetail.vue :博客详情
<template>
  <div>
    <Header></Header>
    <div style="box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); width: 100%; min-height: 700px; padding: 20px 15px;">
      <h2> {{ post.title }} </h2>
 
      <!--编辑按钮-->
      <el-link icon="el-icon-edit" v-if="editButton">
        <!--使用router-link来跳转到某个组件,例如跳转到【BlogDetail组件】,需要传递参数【blogId: post.id】-->
        <router-link :to="{name: 'PostEdit', params: {postId: post.id}}">编辑</router-link>
      </el-link>
 
      <!--分割线-->
      <el-divider></el-divider>
 
      <!--使用【解析md样式、解析md文档】-->
      <div class="markdown-body" v-html="post.content"></div>
    </div>
  </div>
</template>
 
<script>
/*配置【解析md样式】*/
import 'github-markdown-css'
import Header from "@/components/Header";
 
export default {
  name: "PostDetail",
  components: {Header},
  data() {
    return {
      post: {
        id: "",
        title: "",
        content: ""
      },
      editButton: false
    }
  },
 
  //页面一开始渲染时,调用mounted()方法(回写操作)
  mounted() {
    /*根据 /router/index.js 路由规则:【path: '/post/:postId/edit'】中包含参数postId*/
    const postId = this.$route.params.postId
    console.log(postId)
    const _this = this
    this.$axios.get('/post/' + postId).then(res => {
      const post = res.data.data
      _this.post.id = post.id
      _this.post.title = post.title
 
      /*解析md文档*/
      var MardownIt = require("markdown-it")
      var md = new MardownIt()
      var result = md.render(post.content)
      _this.post.content = result
 
      _this.editButton = (post.userId === _this.$store.getters.GET_USERINFO.id)
    })
  }
}
</script>
 
<style>
 
</style>