springboot+shiro+swagger+redis+vue+element完成权限系统

1. 回顾

1. Vue通过脚手架创建Vue工程。
   1.组件【网页】--->组件【父组件】可以引用另一个组件[子组件].

        父组件怎么传参给子组件
   2.路由:
     [1]路由跳转 
        (<router-link to="/路由路径"></router-link>)     this.$router.push("/路由路径")
        路由配置: {path:"",component:""}
        路由渲染: <router-view/>  理解:<iframe/>

2. 权限系统

 权限系统:
   1.前端使用: vue + elementui + axios + css + html
   2.后端使用: springboot+mybatis-plus +mybatis+druid+shiro+swagger2+redis

3. 完成登录

3.1. 前端布局

(1)Login.vue组件

<template>
    <!--这里必须使用一个双标签-->
    <div id="building">
        <div class="login-container">
            <el-form :model="ruleForm" :rules="rules"
                     status-icon
                     width="200px"
                     ref="ruleForm"
                     class="demo-ruleForm login-page">
                <div class="block"><el-avatar :size="100" :src="circleUrl"></el-avatar></div>
                <p class="denglu" style="font-family: 幼圆;font-size: 30px ">用户登录</p>
                <hr style="background-color: mintcream; overflow:hidden; "/>
                <p style="font-family: 幼圆;font-size: 16px; color:white">User&nbsp;&nbsp;login</p>
                <el-form-item  label="" prop="username">
                    <el-input type="text"
                              v-model="ruleForm.username"
                              auto-complete="off"
                              placeholder="请输入账号"
                    ></el-input>
                </el-form-item>
                <el-form-item label="" prop="password">
                    <el-input type="password"
                              v-model="ruleForm.password"
                              auto-complete="off"
                              placeholder="请输入密码"
                              @keyup.enter.native="handleSubmit"
                    ></el-input>
                </el-form-item>

                <el-form-item style="width:100%;">
                    <el-button type="primary" style="width:100px;background-color: #348C77" @click="handleSubmit" :loading="logining">登录</el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data(){
            return{
                logining: false,
                //绑定表单数据
                ruleForm: {
                    username:'',
                },
                //表单验证规则
                rules: {
                    username: [
                        {required: true, message: '请输入账号,账号不能为空', trigger: 'blur'},
                        //{ type: 'number', message: '账号必须为数字值'},
                    ],
                    password: [
                        {required: true, message: '请输入密码,密码不能为空', trigger: 'blur'},
                        {min: 6, max: 12, message: "密码的长度必须{6~12}", trigger: "blur"},
                    ]
                },
                checked: false,
                //头像绑定
                circleUrl:'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
            }
        },
        methods:{
            handleSubmit(){
                //表单校验
                this.$refs['ruleForm'].validate((valid) => {
                    if (valid) {
                        this.$router.push("/home")
                    }
                })
            }
        }
    }
</script>

<style>
    body{
        padding:0px;
        margin:0px;
    }
    /* 登录文字 */
    .denglu {
        color: white;
        font-size: 20px;
    }
    .login-container {
        width: 450px;
        height: 200px;
        margin:auto auto;
    }
    .login-page {
        /* 背景图片 */
        /*background-image: url(/imgs/);*/
        background-size: 100% 100%;


        /*background-color: #3CA993; */

        margin-top: 10rem;
        border-radius: 12px;
        /* 内边距 */
        padding: 20px;
        text-align: center;
        /*box-shadow: 0 8px 5px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);*/
    }
    label.el-checkbox.rememberme {
        margin: 0px 0px 25px;
        text-align: left;
    }
    #building {
        background: url("../assets/login.jpg");
        width: 100%;
        height: 100%;
        position: fixed;
        background-size: 100% 100%;
    }
</style>

(2)配置路由

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '../views/Login'
Vue.use(VueRouter)

const routes = [
  {
    /*路由路径为/则重定向到/login路由*/
    path: '/',
    redirect: '/login'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
    //component: ()=>import("../views/Login.vue")
  },
]

const router = new VueRouter({
  //删除地址栏的#
  mode: 'history',
  routes
})

export default router

(3)登录按钮事件

如果想在vue工程中使用axios进行异步请求,则需要在main.js中导入axios
[1]//导入axios

import axios from "axios";

[2]//把axios挂载到vue对象中,以后在vue中如果使用axios直接可以用$http名称,可以随意起名
 

Vue.prototype.$http=axios

methods:{
            handleSubmit(){
                //表单校验
                this.$refs['ruleForm'].validate((valid) => {
                    if (valid) {
                //this.$http就是自己刚刚定义的axios
                                       //url:后端登录接口的路径                                  //es=>函数
                        this.$http.post("http://localhost:8081/system/login",this.ruleForm).then(result=>{

                        })
                    }
                })
            }
        }

3.2. 完成后端登录接口

(1) 创建springboot项目

 pox.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.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wzh</groupId>
    <artifactId>springboot-vue0808</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-vue0808</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.9.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <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>
    </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>

 (2) 使用mybatis-plus的代码生成器

package com.wzh;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.Collections;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh
 * @ClassName: Generator
 * @Author: 王振华
 * @Description: mp代码生成器
 * @Date: 2022/8/8 18:33
 * @Version: 1.0
 */
public class Generator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/shiro_permission?serverTimezone=Asia/Shanghai", "root", "123456")
                .globalConfig(builder -> {
                    builder.author("王振华") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir(".\\src\\main\\java\\"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.wzh") // 设置父包名
                            .moduleName("system") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "src\\main\\resources\\mapper\\")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("acl_user","acl_role","acl_permission")// 设置需要生成的表名
                            .addTablePrefix("acl_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();

    }
}

(3)配置application文件

#端口号
server.port=8081

#druid数据源
spring.datasource.druid.url=jdbc:mysql://localhost:3306/shiro_permission?serverTimezone=Asia/Shanghai
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
#初始化的个数
spring.datasource.druid.initial-size=5
# 最大活跃数
spring.datasource.druid.max-active=10
# 最大等待时间
spring.datasource.druid.max-wait=3000
# 最小的闲置个数
spring.datasource.druid.min-idle=5

#mybatis-plus日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

(4)登录接口

package com.wzh.system.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh.system.controller
 * @ClassName: LoginController
 * @Author: 王振华
 * @Description:登录接口
 * @Date: 2022/8/8 18:47
 * @Version: 1.0
 */
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin      //解决跨域问题
public class LoginController {
    @Autowired
    private IUserService userService;

    @PostMapping("/login")
    @ApiOperation(value = "登录接口")
    public CommonResult login(@RequestBody LoginVo loginVo){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username",loginVo.getUsername());
        wrapper.eq("password",loginVo.getPassword());
        wrapper.eq("is_deleted",0);
        User user = userService.getOne(wrapper);
        if(user!=null){
            return new CommonResult(2000,"登录成功",null);
        }else{
            return CommonResult.LOGIN_ERROR;
        }
    }
}

vo包:它也是实体类的一种; view obeject 视图对象。 作用:接受和响应网页的对象,

如果用map接收的话,无法使用swagger注解

 前端调用后端登录接口时出现如下的错误

 

 当使用异步请求从一个网址访问另一个网址时可能会出现跨域问题。
前提:
   1. 必须为异步请求
   2. 当端口号或协议或ip不同时则会出现跨域

出现两个请求: 有一个请求的方式为: OPTIONS 和真实的请求方式

理解: OPTIONS先头部队。---探视后台有没有解决跨域。

如何解决跨域:

1.前端解决
2.后端解决---->这里也有几种方式:
   【1】可以借助nginx.
   【2】在代码中解决 (重点)

在控制层接口上添加@CrossOrigin

@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST})

(origins = {"192.168.0.111:8080","192.168.0.120:8081"},allowedHeaders="运行哪些请求头跨域",methods={"GET","POST"})

origins: 允许哪些域可以跨域访问我这个接口
allowedHeaders:允许哪些请求头信息跨域
methods: 允许哪些请求方式跨域

上面在控制层接口处加上注解的方式解决跨域,麻烦的地方就需要对每个控制类都加该注解。 设置一个全局跨域配置类

package com.wzh.system.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
 
    // 当前跨域请求最大有效时长。这里默认1天
    private static final long MAX_AGE = 24 * 60 * 60;
 
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        corsConfiguration.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

(5)加入swagger配置类

package com.wzh.system.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.VendorExtension;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.ArrayList;

/**
 * @Author 闫克起
 * @Date 2021/4/29 16:37
 * @Version 1.0
 */
@Configuration
public class SwaggerConfig {

    //获取swagger2的实例对象docket
    @Bean
    public Docket getDocket() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("QY151")
                .apiInfo(apiInfo())
                .select()//设置哪些包下的类生产api接口文档
                .apis(RequestHandlerSelectors.basePackage("com.wzh.system.controller"))
                //设置哪些请求路径生产接口文档
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    private ApiInfo apiInfo() {
        Contact DEFAULT_CONTACT = new Contact("王振华", "http://www.bing.com", "1430930278@qq.com");
        ApiInfo apiInfo = new ApiInfo("员工管理系统API接口文档", "员工管理系统API接口文档", "1.0", "http://www.bing.com",
                DEFAULT_CONTACT, "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0", new
                ArrayList<VendorExtension>());
        return apiInfo;
    }
}

(6)登录成功后前端路由跳转

 4. 登录的bug

 上面咱们写的登录,后端没有保存数据 前端也没有拿到数据进行保存

要用redis的原因是因为如果高并发访问数据库数据库会崩溃,造成系统瘫痪 

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

application配置文件

#redis的配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-wait=20000
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=5

(1)修改登录的接口

package com.wzh.system.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh.system.controller
 * @ClassName: LoginController
 * @Author: 王振华
 * @Description:登录接口
 * @Date: 2022/8/8 18:47
 * @Version: 1.0
 */
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST})      //解决跨域问题
public class LoginController {
    @Autowired
    private IUserService userService;


    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/login")
    @ApiOperation(value = "登录接口")
    public CommonResult login(@RequestBody LoginVo loginVo){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username",loginVo.getUsername());
        wrapper.eq("password",loginVo.getPassword());
        wrapper.eq("is_deleted",0);
        User user = userService.getOne(wrapper);
        if(user!=null){
            //随机生成一个唯一字符串。
            String token = UUID.randomUUID().toString();
            //把该token作为redis的key value为当前登录用户信息
            ValueOperations forValue = redisTemplate.opsForValue();
            forValue.set(token,user,24, TimeUnit.HOURS);
            return new CommonResult(2000,"登录成功",token);
        }else{
            return CommonResult.LOGIN_ERROR;
        }
    }
}

RedisTemplate类需要序列化,加入一个配置类

package com.wzh.system.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @program: qy151-redis-springboot
 * @description:
 * @author: 王振华
 * @create: 2022-08-08 15:16
 **/
@Configuration
public class RedisConfig {
    //这个是用来自己创建的RedisTemplate
/*    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }*/

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化  filed value
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(redisSerializer);
        return template;
    }
}

(2)修改前端登录方法

 后面每次请求都可以携带该token,

 每次请求都得要人为添加参数token. 我们可以使用axios的请求拦截器

 验证token有没有被使用

<template>
    <div>
        <el-button type="primary" @click="getInfo">获取用户信息</el-button>
    </div>
</template>

<script>
    export default {
        name: "User",
        methods:{
            getInfo(){
                this.$http.get("http://localhost:8081/system/user/getInfo").then(result=>{
                    console.log(result)
                })
            }
        }
    }
</script>

<style scoped>

</style>

接口:

package com.wzh.system.controller;

import com.wzh.system.entity.User;
import com.wzh.system.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author 王振华
 * @since 2022-08-08
 */
@RestController
@RequestMapping("/system/user")
public class UserController {

    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("getInfo")
    public CommonResult getInfo(HttpServletRequest request) {
        String token = request.getHeader("token");    //获取请求头
        System.out.println(token);
        //根据token从redis中获取用户信息
        ValueOperations forValue = redisTemplate.opsForValue();
        User o = (User) forValue.get(token);
        return new CommonResult(2000, "获取用户信息成功", o);
    }
}

 这时发现报500   原因我们的实体类有两个时间类型

 

 解决办法:

 <!--日期序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.9.3</version>
        </dependency>

类属性上加入注解

    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)

运行程序成功

5.前置路由守卫

前置路由守卫:就是在路由跳转前加上自己得一些业务代码,在main.js中配置。类似于拦截器

//设置前置路由守卫 to:到哪个路由  from:从哪个路由来  next():放行到指定路由
router.beforeEach((to,from,next)=>{
      //获取跳转得路径
      var path = to.path;
      //判断是否为登录路由路径
      if(path==="/login"){
          console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
          //放行
          return next();
      }
      //其他路由路径 判断是否登录过
      var token = sessionStorage.getItem("token");
      if(token){
          return next();
      }
      //跳转登录
     return next("/login");
})

6.整合shiro

(1)添加依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.7.0</version>
        </dependency>

(2)shiro的配置类

package com.wzh.system.config;


import com.wzh.system.filter.LoginFilter;
import com.wzh.system.realm.MyRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.DelegatingFilterProxy;

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

/**
 * @ProjectName: springboot-shiro-swagger
 * @Package: com.wzh.config
 * @ClassName: ShiroConfig
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/5 17:32
 * @Version: 1.0
 */
@Configuration
public class ShiroConfig {
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        return securityManager;
    }

    @Bean
    public Realm realm() {
        MyRealm myRealm = new MyRealm();
        myRealm.setCredentialsMatcher(credentialsMatcher());
        return myRealm;
    }

    @Bean
    public CredentialsMatcher credentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        return credentialsMatcher;
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Bean(value = "shiroFilter")
    public ShiroFilterFactoryBean filterFactoryBean() {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager());

        //设置拦截规则
        HashMap<String, String> map = new HashMap<>();
        //放行路径
        map.put("/system/login", "anon");
        map.put("/doc.html", "anon");
        map.put("/swagger-ui.html", "anon");
        map.put("/swagger/**", "anon");
        map.put("/webjars/**", "anon");
        map.put("/swagger-resources/**", "anon");
        map.put("/v2/**", "anon");
        map.put("/static/**", "anon");
        //拦截路径
        map.put("/**", "authc");
        map.put("/login/logout", "logout");
        filterFactoryBean.setFilterChainDefinitionMap(map);

        //设置自定义认证过滤器
        HashMap<String, Filter> filterMap = new HashMap<String, Filter>();
        filterMap.put("authc", new LoginFilter(redisTemplate));
        filterFactoryBean.setFilters(filterMap);

        return filterFactoryBean;
    }

    @Bean //注册filter
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setName("shiroFilter");
        filterRegistrationBean.setFilter(new DelegatingFilterProxy());
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    //开始shiro注解
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public FilterRegistrationBean vv() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        //设置字符编码
        characterEncodingFilter.setEncoding("UTF-8");
        //设置强制使用指定字符编码
        characterEncodingFilter.setForceEncoding(true);


        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(characterEncodingFilter);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;

    }
}

(3)增加一个realm类对象

package com.wzh.system.realm;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.Permission;
import com.wzh.system.entity.User;
import com.wzh.system.service.IPermissionService;
import com.wzh.system.service.IRoleService;
import com.wzh.system.service.IUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh.system.realm
 * @ClassName: MyRealm
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/9 9:47
 * @Version: 1.0
 */
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private IUserService userService;

    @Autowired
    private IPermissionService permissionService;

    @Autowired
    private IRoleService roleService;

    //当你进行权限校验时会执行该方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //根据账号查找该用户具有哪些权限
        QueryWrapper<Permission>
        List<String> list = permissionService.getOne(user.get());
        if(list!=null&&list.size()>0){
            info.addStringPermissions(list);
        }
        List<String> roles = roleService.findRolesById(user.getUserid());
        if(roles!=null&&roles.size()>0){
            info.addRoles(roles);
        }*/
        return null;
    }

    //该方法用于完成认证的功能
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.根据token获取账号
        String username = (String) token.getPrincipal();
        /**
         * 以前登陆的逻辑是  把用户和密码全部发到数据库  去匹配
         * 在shrio里面是先根据用户名把用户对象查询出来,再来做密码匹配
         */

        //2.根据账号查询用户信息
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        wrapper.eq("is_deleted",0);
        User user = userService.getOne(wrapper);
        //表示该用户名在数据库中存在
        if(user!=null){
            /**
             * 参数说明
             * 参数1:可以传到任意对象
             * 参数2:从数据库里面查询出来的密码
             * 参数3:盐
             * 参数4:当前类名
             */
            ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),credentialsSalt,this.getName());
            return info;
        }
        return null;
    }

}

(4) 修改controller代码

package com.wzh.system.controller;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wzh.system.entity.User;
import com.wzh.system.service.IUserService;
import com.wzh.system.vo.CommonResult;
import com.wzh.system.vo.LoginVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh.system.controller
 * @ClassName: LoginController
 * @Author: 王振华
 * @Description:登录接口
 * @Date: 2022/8/8 18:47
 * @Version: 1.0
 */
@RestController
@RequestMapping("system")
@Api(tags = "登录的接口类")
//@CrossOrigin(origins = {"192.168.1.14:8081","192.168.2.34:8080"},allowedHeaders="运行哪些请求头跨域",methods={RequestMethod.GET,RequestMethod.POST})      //解决跨域问题
public class LoginController {
    @Autowired
    private IUserService userService;


    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping("/login")
    @ApiOperation(value = "登录接口")
    public CommonResult login(@RequestBody LoginVo loginVo){
        
        try{
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginVo.getUsername() ,loginVo.getPassword());
            subject.login(usernamePasswordToken);
            
            Object user = subject.getPrincipal();
            //随机生成一个唯一字符串。
            String token = UUID.randomUUID().toString();
            //把该token作为redis的key value为当前登录用户信息
            ValueOperations forValue = redisTemplate.opsForValue();
            forValue.set(token,user,24, TimeUnit.HOURS);
            return new CommonResult(2000,"登录成功",token);
        }catch (Exception e){
            e.printStackTrace();
            return CommonResult.LOGIN_ERROR;
        }
    }
}

 测试登录

登录成功后获取用户信息时出现如下得错误

 被shiro的拦截器给拦截了。

package com.wzh.system.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wzh.system.vo.CommonResult;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;

/**
 * @ProjectName: springboot-vue0808
 * @Package: com.wzh.system.filter
 * @ClassName: LoginFilter
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/9 9:46
 * @Version: 1.0
 */
//如果类没有交于spring容器来管理 那么该类中得属性也不能让spring帮你注入

public class LoginFilter extends FormAuthenticationFilter {

    private RedisTemplate redisTemplate; //LoginFilter必须交于spring容器来管理。

    public LoginFilter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    //当登录成功后执行得方法,如果该方法返回false,则执行onAccessDenied
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        //1.请求方式是否为OPTIONS
        String options = req.getMethod();
        if(options!=null && options.equals("OPTIONS")){
            return true;
        }
        String token = req.getHeader("token");
        //token可以伪造,所以也要查询redis里有没有该记录
        if(token!=null && redisTemplate.hasKey(token)){
            return true;
        }

        return false;
    }
    //未登录时调用该方法? 为什么进入没有登录方法:
    // --->第一个请求是OPTIONS,没有携带token  第二个因为前端和后端不是用得同一个session.默认shiro以sessionId为是否登录得标准
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        CommonResult commonResult = CommonResult.UNLOGIN;
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(commonResult);
        writer.print(json);
        writer.flush();
        writer.close();
        return false;
    }
}

7.主页的布局 

<template>
    <el-container>
        <el-header>
                <span id="logo" style="display: inline-block;width: 30%;height: 100%;float: left" >
                     <a href="https://www.bilibili.com/video/BV14g41197PY/"><img src="../assets/logo.png" height="100%" width="180px"></a>
                </span>
            <span id="avatar" style="float: right">
                    <el-dropdown @command="handleCommand">
                  <span class="el-dropdown-link" style="margin-top: 10px; display: inline-block;">
                    <el-avatar ></el-avatar>
                  </span>
                    <el-dropdown-menu slot="dropdown" style="margin-top: -10px">
                        <el-dropdown-item command="info">个人信息</el-dropdown-item>
                        <el-dropdown-item command="logout">退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                </span>
        </el-header>
        <el-container>
            <el-aside width="200px">

            </el-aside>
            <el-main>

            </el-main>
        </el-container>
        <el-footer>Footer</el-footer>
    </el-container>
</template>

<script>
    export default {
        name: "Home",
        methods:{
            getInfo(){
                this.$http.get("/system/user/getInfo").then(result=>{
                    console.log(result)
                })
            },
            
        }
    }
</script>
<!--当前vue有效-->
<style>
    html,body,#app{
        height: 100%;
    }
    body,#app{
        padding: 0px;
        margin:0px;
    }
    .el-container{
        height: 100%;
    }
    .el-header, .el-footer {
        background-color: #1F272F;
        color: #333;
        line-height: 60px;
    }

    .el-aside {
        background-color: #545c64;
        color: #333;
        line-height: 560px;
    }
    .el-aside>.el-menu{
        border: none;
    }
    .el-main {
        background-color: #E9EEF3;
        color: #333;
        line-height: 560px;
    }

    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;
    }
</style>

7.1. 退出

前端:

由于每个跳转都需要写ip和端口,我们可以在main.js中设置axios的基础路径

//设置axios基础路径
axios.defaults.baseURL="http://localhost:8082"

 后端:

    @GetMapping("/logout")
    public CommonResult logout(HttpServletRequest request){
        String token = request.getHeader("token");
        if(redisTemplate.hasKey(token)){
            redisTemplate.delete(token);
            return new CommonResult(2000,"退出成功",null);
        }
        return new CommonResult(5000,"请先登录",null);
    }

7.2. 查询左侧菜单

(1)前端

<template>
    <el-container>
        <el-header>
                <span id="logo" style="display: inline-block;width: 30%;height: 100%;float: left" >
                     <a href="https://www.bilibili.com/video/BV14g41197PY/"><img src="../assets/logo.png" height="100%" width="180px"></a>
                </span>
            <span id="avatar" style="float: right">
                    <el-dropdown @command="handleCommand">
                  <span class="el-dropdown-link" style="margin-top: 10px; display: inline-block;">
                    <el-avatar ></el-avatar>
                  </span>
                    <el-dropdown-menu slot="dropdown" style="margin-top: -10px">
                        <el-dropdown-item command="info">个人信息</el-dropdown-item>
                        <el-dropdown-item command="logout">退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                </span>
        </el-header>
        <el-container>
            <el-aside width="200px">
                <el-menu
                        default-active="2"
                        class="el-menu-vertical-demo"
                        background-color="#545c64"
                        text-color="#fff"
                        active-text-color="#ffd04b">
                    <el-submenu :index="menu.id+''" v-for="menu in leftMenus">
                        <template slot="title">
                            <i :class="menu.icon"></i>
                            <span>{{menu.name}}</span>
                        </template>
                        <el-menu-item :index="second.id+''" v-for="second in menu.children">
                            <i :class="second.icon"></i>
                            <span><a style="color: white;text-decoration: none"
                            >{{second.name}}</a></span>
                        </el-menu-item>
                    </el-submenu>

                </el-menu>
            </el-aside>
            <el-main>

            </el-main>
        </el-container>
        <el-footer>Footer</el-footer>
    </el-container>
</template>

<script>
    export default {
        name: "Home",
        data(){
            return{
                leftMenus:[]
            }
        },
        methods:{
            initLeftMenu(){
                this.$http.get("/system/permission/leftMenu").then(result=>{
                    if(result.data.code===2000){
                        this.leftMenus=result.data.data;
                    }
                })
            },
            getInfo(){
                this.$http.get("/system/user/getInfo").then(result=>{
                    console.log(result)
                })
            },
            //下拉的触发事件
            handleCommand(command) {
                if (command === 'logout') {
                    this.$http.get("/system/logout").then(result => {
                        if (result.data.code === 2000) {
                            console.log("11111")
                            sessionStorage.clear();
                            //sessionStorage.removeItem("token");
                            this.$router.push("/login")
                        }
                    })
                }
            }
        }
    }
</script>
<!--当前vue有效-->
<style>
    html,body,#app{
        height: 100%;
    }
    body,#app{
        padding: 0px;
        margin:0px;
    }
    .el-container{
        height: 100%;
    }
    .el-header, .el-footer {
        background-color: #1F272F;
        color: #333;
        line-height: 60px;
    }

    .el-aside {
        background-color: #545c64;
        color: #333;
        line-height: 560px;
    }
    .el-aside>.el-menu{
        border: none;
    }
    .el-main {
        background-color: #E9EEF3;
        color: #333;
        line-height: 560px;
    }

    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;
    }
</style>

(2)后端:

@RestController
@RequestMapping("/system/permission")
public class PermissionController {

    @Autowired
    private IPermissionService permissionService;

    @GetMapping("leftMenu")
    public CommonResult leftMenu(HttpServletRequest request){
        String token = request.getHeader("token");
        return permissionService.findPermissionByUserId(token);
    }

}

service

package com.wzh.system.service.impl;

import com.wzh.system.entity.Permission;
import com.wzh.system.entity.User;
import com.wzh.system.mapper.PermissionMapper;
import com.wzh.system.service.IPermissionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wzh.system.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

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

/**
 * <p>
 * 权限 服务实现类
 * </p>
 *
 * @author 王振华
 * @since 2022-08-09
 */
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements IPermissionService {

    @Autowired
    private PermissionMapper permissionMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public CommonResult findPermissionByUserId(String token) {
        //根据token获取用户信息
        ValueOperations forValue = redisTemplate.opsForValue();
        User user = (User) forValue.get(token);
        //根据用户id查询该用户具有的权限。
        List<Permission> permissionList = permissionMapper.selectByUserId(user.getId());

        //设置层级关系
        List<Permission> firstMenus = new ArrayList<>();
        for (Permission firstMenu : permissionList){
            if(firstMenu.getPid().equals("1")){
                firstMenus.add(firstMenu);
            }
        }

        //为一级菜单设置二级菜单
        for (Permission first : firstMenus){
            //根据一级菜单id 查询 该菜单得二级菜单。如果出现不确定有几级菜单 那么我们可以使用方法得递归调用
            first.setChildren(findChildren(permissionList, first.getId()));
        }
        return new CommonResult(2000,"查询成功",firstMenus);
    }
    //方法递归
    private List<Permission> findChildren(List<Permission> permissionList, String pid) {
        List<Permission> children = new ArrayList<>();
        for (Permission p : permissionList){
            if(p.getPid().equals(pid)){
                children.add(p);
            }
        }

        for(Permission child : children){
            child.setChildren(findChildren(permissionList,child.getId()));
        }

        return children;
    }
}

实体类添加列

推荐使用下一篇文章的,用到了递归,不管几级菜单都可以

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个开源的Java框架,用于构建独立的、可执行的、生产级的Spring应用程序。它极大地简化了Spring应用程序的搭建和部署过程,提供了一整套开箱即用的特性和插件,极大地提高了开发效率。 Shiro是一个强大且灵活的开源Java安全框架,提供了身份验证、授权、加密和会话管理等功能,用于保护应用程序的安全。它采用插件化的设计,支持与Spring等常用框架的无缝集成,使开发者能够轻松地在应用程序中添加安全功能。 JWT(JSON Web Token)是一种用于在客户端和服务端之间传输安全信息的开放标准。它使用JSON格式对信息进行包装,并使用数字签名进行验证,确保信息的完整性和安全性。JWT具有无状态性、可扩展性和灵活性的特点,适用于多种应用场景,例如身份验证和授权。 Redis是一个开源的、高性能的、支持多种数据结构的内存数据库,同时也可以持久化到磁盘中。它主要用于缓存、消息队列、会话管理等场景,为应用程序提供高速、可靠的数据访问服务。Redis支持丰富的数据类型,并提供了强大的操作命令,使开发者能够灵活地处理各种数据需求。 综上所述,Spring Boot结合Shiro、JWT和Redis可以构建一个安全、高性能的Java应用程序。Shiro提供了强大的安全功能,包括身份验证和授权,保护应用程序的安全;JWT用于安全传输信息,确保信息的完整性和安全性;Redis作为缓存和持久化数据库,提供了高速、可靠的数据访问服务。通过使用这些技术,开发者能够快速、高效地构建出符合安全和性能需求的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值