基于Shiro+Springboot+Mybatis+Vue权限管理系统(精)

代码上传到了github

后端:GitHub - chenqi13814529300/my-csdn-shiro

前端:GitHub - chenqi13814529300/my-shiro-ui

sql代码在后端代码里,这里没有用jpa,因为易上手,难精通,比较抽象,不利于未来发展

目录

数据库设计

 后端项目构建

创建项目

pom依赖

application 

实现效果

具体实现

整体目录

entity类

dto层,用于封装前端传来的用户名和密码

Mapper.xml层跟数据库相关

dao层,也就是mapper接口层

service层,业务处理层

contorller层

config

auth

utils

hander

启动类 

前端项目构建

创建项目

前端具体实现

目录如下

main.js router.js token.js 

api

views 

流程思路


数据库设计

学到这个阶段,大家Mysql应该不陌生了,既然是走Mybatis的路线,先搭建一个健壮的数据结构很重要,秉着高内聚低耦合去设计。

不可取的设计方式如下:

Id   username    password    role   permission

我建立如下数据结构,一组是用户角色权限,一组是角色的token

用户角色权限的一组表

user

 user_role

role

 role_permission

permission 

角色token存储的表 

user_token (这些数据是生成的,你们设计个表即可)

 后端项目构建

创建项目

按步骤来即可

 

 

pom依赖

  <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- shiro 缓存框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

application 

server:
  port: 9000
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/myshiro?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
    username: root
    password: 123
    name: defaultDataSource

建立目录

auth包下放权限过滤器,判断token等

cofig包下是shiro的配置文件

dto包下是前端传入后端的用户名和密码的实体类

handler包下是全局异常处理类

其他的你们应该知道了

实现效果

 不同用户登录进去有不同的权限

登录后如下

具体实现

整体目录

entity类

User

package com.mycsdnshiro.mycsdnshiro.entity;

public class User {

    private Integer userId;
    private String username;
    private String password;
    private Integer roleId;
    private String roleName;
    private String permission;
    private Integer permissionId;

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roleId=" + roleId +
                ", roleName='" + roleName + '\'' +
                ", permission='" + permission + '\'' +
                ", permissionId=" + permissionId +
                '}';
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getRoleId() {
        return roleId;
    }

    public void setRoleId(Integer roleId) {
        this.roleId = roleId;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }

    public Integer getPermissionId() {
        return permissionId;
    }

    public void setPermissionId(Integer permissionId) {
        this.permissionId = permissionId;
    }

    public User() {
    }

    public User(Integer userId, String username, String password, Integer roleId, String roleName, String permission, Integer permissionId) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.roleId = roleId;
        this.roleName = roleName;
        this.permission = permission;
        this.permissionId = permissionId;
    }
}

UserToken

package com.mycsdnshiro.mycsdnshiro.entity;

import java.time.LocalDateTime;

public class UserToken {


    private Integer userId;

    private String token;

//    过期时间
    private LocalDateTime expireTime;


//    更新时间
    private LocalDateTime updateTime;

    @Override
    public String toString() {
        return "UserTokenMapper{" +
                "userId=" + userId +
                ", token='" + token + '\'' +
                ", expireTime=" + expireTime +
                ", updateTime=" + updateTime +
                '}';
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }

    public UserToken(Integer userId, String token, LocalDateTime expireTime, LocalDateTime updateTime) {
        this.userId = userId;
        this.token = token;
        this.expireTime = expireTime;
        this.updateTime = updateTime;
    }

    public UserToken() {
    }
}

RoleAndPermission

package com.mycsdnshiro.mycsdnshiro.entity;

public class RoleAndPermission {
    private Integer userId;
    private Integer roleId;
    private String roleName;
    private String permission;

    public RoleAndPermission(Integer userId, Integer roleId, String roleName, String permission) {
        this.userId = userId;
        this.roleId = roleId;
        this.roleName = roleName;
        this.permission = permission;
    }

    @Override
    public String toString() {
        return "RoleAndPermission{" +
                "userId=" + userId +
                ", roleId=" + roleId +
                ", roleName='" + roleName + '\'' +
                ", permission='" + permission + '\'' +
                '}';
    }

    public RoleAndPermission() {
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public Integer getRoleId() {
        return roleId;
    }

    public void setRoleId(Integer roleId) {
        this.roleId = roleId;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }
}

dto层,用于封装前端传来的用户名和密码

package com.mycsdnshiro.mycsdnshiro.dto;

public class LoginDTO {
    private String username;
    private String password;

    public LoginDTO() {
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public LoginDTO(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "LoginDTO{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

Mapper.xml层跟数据库相关

UserMapper

这些代码都没什么必要说的,来看此文章你们应该会增删改查了吧

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mycsdnshiro.mycsdnshiro.mapper.UserMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.mycsdnshiro.mycsdnshiro.entity.User">
        <id column="user_id" property="userId"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="role_id" property="roleId"/>
    </resultMap>
    <resultMap id="rolePermission" type="com.mycsdnshiro.mycsdnshiro.entity.RoleAndPermission">
        <id column="user_id" property="userId"/>
        <result column="role_id" property="roleId"/>
        <result column="password" property="password"/>
        <result column="role_name" property="roleName"/>
        <result column="permission" property="permission"/>

    </resultMap>

    <select id="findByUsername" resultMap="BaseResultMap">
        select
        u.user_id,u.username,u.password,ur.role_id
        from user u,user_role ur
        where u.user_id=ur.user_id and u.username=#{username}
    </select>

    <select id="rolePermissionByRoleId" resultMap="rolePermission">
        select
            u.user_id,ur.role_id,r.role_name,p.permission
        from user u,user_role ur,role r,role_permission rp,permission p
        where u.user_id=ur.user_id
          and  r.role_id=ur.role_id
          and rp.role_id=u.user_id
          and rp.permission_id=p.permission_id
          and ur.role_id=#{roleId}
    </select>

    <select id="findByUserId" resultMap="BaseResultMap">
        select
        u.user_id,u.username,u.password,ur.role_id
        from user u,user_role ur
        where u.user_id=ur.user_id and u.user_id=#{userId}
    </select>




</mapper>

UserTokenMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mycsdnshiro.mycsdnshiro.mapper.UserTokenMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.mycsdnshiro.mycsdnshiro.entity.UserToken">
        <id column="user_id" property="userId"/>
        <result column="token" property="token"/>
        <result column="expire_time" property="expireTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <select id="findByToken" resultMap="BaseResultMap">
        select
            user_id,token,expire_time,update_time
        from
            user_token
        where token=#{token}
    </select>

    <select id="findByUserId" resultMap="BaseResultMap">
        select
            user_id,token,expire_time,update_time
        from
            user_token
        where user_id=#{userId}
    </select>

    <insert id="insertUserToken">
        insert into user_token
            (user_id,token,update_time,expire_time)
        values(#{userId},#{token},#{updateTime},#{expireTime})
    </insert>

    <update id="updateUserToken">
        UPDATE user_token
        set expire_time=#{expireTime},
            token=#{token},
            update_time=#{updateTime}
        where user_id=#{userId};
    </update>



</mapper>

dao层,也就是mapper接口层

UserMapper

package com.mycsdnshiro.mycsdnshiro.mapper;

import com.mycsdnshiro.mycsdnshiro.entity.RoleAndPermission;
import com.mycsdnshiro.mycsdnshiro.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    User findByUsername(String username);
    User findByUserId(Integer userId);

    List<RoleAndPermission> rolePermissionByRoleId(Integer roleId);
}

UserTokenMapper

package com.mycsdnshiro.mycsdnshiro.mapper;

import com.mycsdnshiro.mycsdnshiro.entity.UserToken;

public interface UserTokenMapper {

    UserToken findByToken(String token);

    UserToken findByUserId(Integer userId);

    int  insertUserToken(UserToken userToken);
    int updateUserToken(UserToken userToken);
}

service层,业务处理层

你们将明白为什么要多这一层,而不直接controller调用mapper接口

ShiroLoginService

package com.mycsdnshiro.mycsdnshiro.service;

import com.mycsdnshiro.mycsdnshiro.entity.RoleAndPermission;
import com.mycsdnshiro.mycsdnshiro.entity.User;
import com.mycsdnshiro.mycsdnshiro.entity.UserToken;

import java.util.List;
import java.util.Map;

public interface ShiroLoginService {

    List<User> queryAllUser();

    User findByUsername(String username);

    Map<String,Object> createToken(Integer userId);

    void logout(String token);

    UserToken findByToken(String accessToken);


    User findByUserId(Integer userId);

    List<RoleAndPermission> rolePermissionByRoleId(Integer roleId);

}

ShiroLoginServiceImpl

package com.mycsdnshiro.mycsdnshiro.service.impl;

import com.mycsdnshiro.mycsdnshiro.auth.TokenGenerator;
import com.mycsdnshiro.mycsdnshiro.entity.RoleAndPermission;
import com.mycsdnshiro.mycsdnshiro.entity.User;
import com.mycsdnshiro.mycsdnshiro.entity.UserToken;
import com.mycsdnshiro.mycsdnshiro.mapper.UserMapper;
import com.mycsdnshiro.mycsdnshiro.mapper.UserTokenMapper;
import com.mycsdnshiro.mycsdnshiro.service.ShiroLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ShiroLoginServiceImpl implements ShiroLoginService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserTokenMapper userTokenMapper;
    private  final  static  int EXPIRE=12;

    @Override
    public List<User> queryAllUser() {
        return null;
    }

    @Override
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }

//    给当前登录的用户生成token
    @Override
    public Map<String, Object> createToken(Integer userId) {
        Map<String,Object> result=new HashMap<>();
//        生成一个token,代表这个用户的标识
        String token = TokenGenerator.generateValue();
//        当前时间
        LocalDateTime now = LocalDateTime.now();
//        过期时间
        LocalDateTime expireTime = now.plusHours(EXPIRE);

//        通过Id 判断是否生成过token 若有则更新,若没有则添加
        UserToken tokenEntity = userTokenMapper.findByUserId(userId);

        if(tokenEntity==null){
//            新增
            tokenEntity = new UserToken();
            tokenEntity.setUserId(userId);
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
//            新增就sql添加
            userTokenMapper.insertUserToken(tokenEntity);

        }else {
//            更新
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
//            更新就sql更新
            userTokenMapper.updateUserToken(tokenEntity);

        }
        result.put("token",token);
        result.put("expire",expireTime);
        return result;

    }

    @Override
    public void logout(String token) {

    }

    @Override
    public UserToken findByToken(String accessToken) {
        return userTokenMapper.findByToken(accessToken);
    }

    @Override
    public User findByUserId(Integer userId) {
        return userMapper.findByUserId(userId);
    }

    @Override
    public List<RoleAndPermission> rolePermissionByRoleId(Integer roleId) {
        return userMapper.rolePermissionByRoleId(roleId);
    }
}

contorller层

ShiroLoginController     

用于用户登录的

package com.mycsdnshiro.mycsdnshiro.controller;


import com.mycsdnshiro.mycsdnshiro.dto.LoginDTO;
import com.mycsdnshiro.mycsdnshiro.entity.User;
import com.mycsdnshiro.mycsdnshiro.service.ShiroLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class ShiroLoginController {
@Autowired
private ShiroLoginService shiroLoginService;

    @PostMapping("login")
    public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
//       把后端处理后的数据,封装在里面,返回给前端
        Map<String, Object> result = new HashMap<>();

//前端返回值校验
        if (bindingResult.hasErrors()) {
            result.put("status", 400);
            result.put("msg", bindingResult.getFieldError().getDefaultMessage());
            return result;
        }

//        获取登录前端传入的值
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        System.out.println(username);
//        在数据库中查找是否有此用户名
        User user = shiroLoginService.findByUsername(username);
        if(user==null){
            result.put("status",300);
            result.put("mg","账号不存在");
        }else if(!user.getPassword().equals(password)){
            result.put("status",400);
            result.put("mg","密码错误");
        }else {
//            账号和密码验证正确,生成新token或者更新token并保存到数据库中
            result = shiroLoginService.createToken(user.getUserId());
            result.put("status",200);
            result.put("mg","检验通过,登录成功");
        }

        return result;
    }

}

ShiroBaseController

用于用户个人权限和对应操作权限的判断

package com.mycsdnshiro.mycsdnshiro.controller;


import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("base")
public class ShiroBaseController {

    @RequiresRoles({"admin"}) //没有的话 AuthorizationException
    @GetMapping("/admin")
    public Map<String, Object> admin(@RequestHeader("token")String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有admin角色");
        return map;
    }
    @RequiresRoles({"teacher"}) //没有的话 AuthorizationException
    @GetMapping("/teacher")
    public Map<String, Object> teacher(@RequestHeader("token")String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有teacher角色");
        return map;
    }
    @RequiresRoles({"student"}) //没有的话 AuthorizationException
    @GetMapping("/student")
    public Map<String, Object> p(@RequestHeader("token")String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有student角色");
        return map;
    }

    @RequiresPermissions({"insert"}) //没有的话 AuthorizationException
    @PostMapping("insert")
    public Map<String, Object> save(@RequestHeader("token") String token) {
        System.out.println("insert");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有insert的权力");
        return map;
    }

    @RequiresPermissions({"delete"}) //没有的话 AuthorizationException
    @DeleteMapping("delete")
    public Map<String, Object> delete(@RequestHeader("token") String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有delete的权力");
        return map;
    }

    @RequiresPermissions({"update"}) //没有的话 AuthorizationException
    @PutMapping("update")
    public Map<String, Object> update(@RequestHeader("token") String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有update的权力");
        return map;
    }

    @RequiresPermissions({"select"}) //没有的话 AuthorizationException
    @GetMapping("select")
    public Map<String, Object> select(@RequestHeader("token") String token) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", 200);
        map.put("msg", "当前用户有select的权力");
        return map;
    }

}

config

ShiroConfig配置文件

package com.mycsdnshiro.mycsdnshiro.config;


import com.mycsdnshiro.mycsdnshiro.auth.AuthFilter;
import com.mycsdnshiro.mycsdnshiro.auth.AuthRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public SecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }


    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shifoFilter(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //auth过滤
        Map<String, javax.servlet.Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilter.setFilters(filters);
        LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
//        auno 匿名访问  auth验证
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/doc.html", "anon");
//        下面这个是放行这个接口(不进行权限判断),因为我们登录需要请求这个接口
        filterMap.put("/user/login", "anon");

//除了以上,其他都需要权限验证
        filterMap.put("/**", "auth");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;


    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


}

auth

用户权限判断的一些类

AuthFilter

package com.mycsdnshiro.mycsdnshiro.auth;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.mycsdnshiro.mycsdnshiro.utils.HttpContextUtil;
import com.mycsdnshiro.mycsdnshiro.utils.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

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


@Component
public class AuthFilter extends AuthenticatingFilter {


    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 生成自定义token
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        System.out.println("后端获取前端headers或者参数处的token="+token);

        return new AuthToken(token);
    }

    /**
     * 步骤1.所有请求全部拒绝访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    /**
     * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        System.out.println("前端请求token="+token);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
            httpResponse.setCharacterEncoding("UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", 403);
            result.put("msg", "请先登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * token失效时候调用
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Map<String, Object> result = new HashMap<>();
            result.put("status", 403);
            result.put("msg", "登录凭证已失效,请重新登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }

}

AuthToken

package com.mycsdnshiro.mycsdnshiro.auth;


import org.apache.shiro.authc.UsernamePasswordToken;


public class AuthToken extends UsernamePasswordToken {

    private String token;

    public AuthToken(String token) {
        this.token = token;
    }

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

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

TokenGenerator

生成Token,其中搭配utils工具包使用,在下文

package com.mycsdnshiro.mycsdnshiro.auth;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;

public class TokenGenerator {

//    token生成器

    public static String generateValue() {
      return generateValue(UUID.randomUUID().toString());
    }


    private static final char[] hexCode = "0123456789abcdefgh".toCharArray();

    public static String toHexString(byte[] data){
        if (data==null){
            return null;
        }
        StringBuffer r = new StringBuffer(data.length * 2);
        for (byte b: data){
            r.append(hexCode[(b >> 4) & 0xF]);
            r.append(hexCode[(b & 0xF)]);
        }
        return r.toString();
    }



//    生成Token

    public static String generateValue(String param){
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(param.getBytes());
            byte[] digest = algorithm.digest();
            return toHexString(digest);

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("生成Token失败");
        }
    }


}

AuthRealm

权限判断入口,登录的时候只会走 doGetAuthenticationInfo,权限判断的时候才会两个都走

package com.mycsdnshiro.mycsdnshiro.auth;



import com.mycsdnshiro.mycsdnshiro.entity.RoleAndPermission;
import com.mycsdnshiro.mycsdnshiro.entity.User;
import com.mycsdnshiro.mycsdnshiro.entity.UserToken;
import com.mycsdnshiro.mycsdnshiro.service.ShiroLoginService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

@Component
public class AuthRealm  extends AuthorizingRealm {

    @Autowired
    private ShiroLoginService shiroLoginService;


//    授权 获取用户角色和权限
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//        1.从 PrincipalCollection 中获取登录用户的信息
        User user = (User) principals.getPrimaryPrincipal();
        List<RoleAndPermission> users = shiroLoginService.rolePermissionByRoleId(user.getRoleId());
//        2.添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//
        Iterator<RoleAndPermission> it = users.iterator();
        while (it.hasNext()){
            RoleAndPermission u=it.next();
            simpleAuthorizationInfo.addRole(u.getRoleName());
            simpleAuthorizationInfo.addStringPermission(u.getPermission());
            System.out.println(u.getPermission());
        }


        return simpleAuthorizationInfo;
    }



//    认证 判断token的有效性
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//       获取token,即前端传入的token
        String accessToken=(String)token.getPrincipal();
        System.out.println("此处判断token有效性"+accessToken);

//        1.根据accessToken,查询用户信息(token里面就是前端传来的用户名和密码封装后的)findByToken
        UserToken tokenEntity = shiroLoginService.findByToken(accessToken);
        System.out.println("得到用户实体"+tokenEntity);

//        2.token失败
        if(tokenEntity==null||tokenEntity.getExpireTime().isBefore(LocalDateTime.now())){

            throw new IncorrectCredentialsException("token,请重新登录");
        }

//        3.调用数据库的方法,从数据库中查询 username 对应的用户记录
        User user = shiroLoginService.findByUserId(tokenEntity.getUserId());

//        4.若用户不存在,则异常
        if(user==null){
            throw new UnknownAccountException("用户不存在");
        }

//        5.根据用户的情况,来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, this.getName());


        return info;
    }


}

utils

工具类

TokenUtil

获取前端请求体中的token

package com.mycsdnshiro.mycsdnshiro.utils;

import org.apache.commons.lang.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * token工具类
 */
public class TokenUtil {
    /**
     * 获取请求的token
     */
    public static String getRequestToken(HttpServletRequest httpRequest) {

        //从header中获取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            token = httpRequest.getParameter("token");
        }
        return token;
    }
}

HttpContextUtil

package com.mycsdnshiro.mycsdnshiro.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

public class HttpContextUtil {
    public static HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public static String getDomain(){
        HttpServletRequest request = getHttpServletRequest();
        StringBuffer url = request.getRequestURL();
        return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
    }

    public static String getOrigin(){
        HttpServletRequest request = getHttpServletRequest();
        return request.getHeader("Origin");
    }
}

hander

全局异常处理类

package com.mycsdnshiro.mycsdnshiro.handler;

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class MyExceptionHandler {


    @ExceptionHandler(value = AuthorizationException.class)
    public Map<String, String> handleException(AuthorizationException e) {
        //e.printStackTrace();
        Map<String, String> result = new HashMap<String, String>();
        result.put("status", "400");
        //获取错误中中括号的内容
        String message = e.getMessage();
        String msg=message.substring(message.indexOf("[")+1,message.indexOf("]"));
        //判断是角色错误还是权限错误
        if (message.contains("role")) {
            result.put("msg", "对不起,您没有" + msg + "角色");
        } else if (message.contains("permission")) {
            result.put("msg", "对不起,您没有" + msg + "权限");
        } else {
            result.put("msg", "对不起,您的权限有误");
        }
        return result;
    }
}

启动类 

前面没写mapper在这记得扫描

package com.mycsdnshiro.mycsdnshiro;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.mycsdnshiro.mycsdnshiro.mapper")
public class MycsdnshiroApplication {

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

}

 到此后端代码结束!!!

前端项目构建

创建项目

照着即可,大伙应该都会

至于前端页面样式啥的我随便写写就好,主要是咋用token

再npm install

npm  run dev 运行

再加入less 

npm install --save-dev less-loader@4.1.0

element ui

 npm install --save element-ui

axios

npm install --save axios

前端具体实现

目录如下

config/index.js

跨域处理

'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    // 只改这里---------------前端处理跨域就好了

    proxyTable: {
      '/api': {
        target: 'http://127.0.0.1:9000',
        pathRewrite: {
          '^/api': ''
        }
      }

    },

    // --------------end

    
    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-


    
    devtool: 'cheap-module-eval-source-map',


    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    productionSourceMap: true,
    devtool: '#source-map',

    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],


    bundleAnalyzerReport: process.env.npm_config_report
  }
}

main.js router.js token.js 

main

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import token from './token/token.js'
import API from './api'

Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.prototype.$API=API
Vue.prototype.getToken = token.isLoginToken
Vue.prototype.setToken = token.setToken
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

router.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'welcome',
      component: ()=>import("@/views/Welcome"),
      redirect:'login',
      children:[
        {
          path:'login',
          name:'login',
          component:()=>import("@/views/Login")
        },
        {
          path:'home',
          name:'home',
          component:()=>import("@/views/Home")
        },
        {
          path:'adminManage',
          name:'adminManage',
          component:()=>import("@/views/AdminManage")
        },
        {
          path:'studentManage',
          name:'studentManage',
          component:()=>import("@/views/StudentManage")
        },
        {
          path:'teacherManage',
          name:'teacherManage',
          component:()=>import("@/views/TeacherManage")
        },
      ]
    }
  ]
})

token.js

import vue from 'vue'
import router from '../router'
import ElementUI from 'element-ui'
vue.use(ElementUI)
import { Message } from 'element-ui';

const isLoginToken = () => {
    const token = localStorage.getItem("token");
    if (!token) {
        Message({
            showClose: true,
            message: "请先登录",
            type: "error",
            duration: "3000"
        });
        router.push({ path: "/login" });

    }
    return token;
}

const setToken = (token) => {
    localStorage.setItem("token", token);
}


export default {
    isLoginToken,
    setToken
}

api

login.js

import axios from "axios";
const login = (loginInfo) => {
    console.log(loginInfo);
    return axios.post('api/user/login', {
            ...loginInfo
    })
}


export default {
    login,
}

base.js

import axios from 'axios'

const goAdmin = (token) => {
    return axios.get('api/base/admin', {
        headers: {
            token: token
        }
    })
}
const goTeacher = (token) => {
    return axios.get('api/base/teacher', {
        headers: {
            token: token
        }
    })
}
const goStudent = (token) => {
    return axios.get('api/base/student', {
        headers: {
            token: token
        }
    })
}

const goInsert = (token) => {
    return axios.post('/api/base/insert', {}, {
        headers:{
            token:token
        }
    })
}



const goUpdate = (token) => {
    return axios.put('api/base/update', {}, {
        headers:{
            token:token
        }
    })
}

const goDelete = (token) => {
    return axios.delete('api/base/delete', {
        headers: {
            token: token
        }
    })
}
const goSelect = (token) => {
    console.log(token);
    return axios.get('api/base/select', {
        headers: {
            token: token
        }
    })
}
export default {
    goAdmin,
    goTeacher,
    goStudent,
    goInsert,
    goDelete,
    goSelect,
    goUpdate
}

index.js

import login from '@/api/login/login.js'
import base from '@/api/base/base.js'


export default{
    login,
    base
}

views 

Welcome.vue

<template>
  <div>
      <router-view/>
  </div>
</template>

<script>
export default {
}
</script>

<style>

</style>

Login.vue

<template>
  <div class="login_box">
    <h2>登录</h2>
    <el-form
      :model="ruleForm"
      status-icon
      ref="ruleForm"
      label-width="40px"
      class="demo-ruleForm"
    >
      <el-form-item label="账户" prop="username">
        <el-input
          type="text"
          v-model="ruleForm.username"
          autocomplete="off"
        ></el-input>
      </el-form-item>

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

      <el-form-item>
        <el-button type="primary" plain @click="submitForm('ruleForm')"
          >提交</el-button
        >
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    var checkUsername = (rule, value, callback) => {
      if (!value) {
        return callback(new Error("用户名不能为空"));
      }
    };
    var validatePassword = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("请输入密码"));
      } else {
        callback();
      }
    };

    return {
      ruleForm: {
        password: "",
        username: "",
      },
      rules: {
        password: [{ validator: validatePassword, trigger: "blur" }],
        username: [{ validator: checkUsername, trigger: "blur" }],
      },
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$API.login.login(this.ruleForm).then((result) => {
            if (result.status === 200) {
              var data = result.data;
              if (data.status === 200) {
                this.setToken(data.token);
                console.log(data.token);
                this.$message({
                  showClose: true,
                  message: "登录成功",
                  type: "success",
                  duration: "600",
                });
                var jsonUser = JSON.stringify({
                  username: this.ruleForm.username,
                });
                sessionStorage.setItem("user", jsonUser);
                this.ruleForm.username = "";
                this.ruleForm.password = "";
                this.$router.push("home");
              } else {
                this.$message({
                  showClose: true,
                  message: "登录失败,原因: " + data.msg,
                  type: "error",
                  duration: "3000",
                });
              }
            } else {
              this.$message({
                showClose: true,
                message: "登录失败,请联系管理员",
                type: "error",
                duration: "3000",
              });
            }
          });
        }
      });
    },
  },
};
</script>
<style lang="less" scoped>
.login_box {
  width: 100%;
}
h2 {
  margin-left: 20px;
  margin-top: 50px;
  text-align: center;
}

.el-form {
  width: 25%;
  margin: 20px auto;
  position: relative;
  .el-form-item {
    width: 100%;
  }
}
.el-input {
  width: 100%;
}
.el-button {
  width: 100%;

  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
</style>

Home.vue

权限检验页面

<template>
  <div>
    <button @click="goAdmin">admin进的</button>
    <button @click="goTeacher">teacher进的</button>
    <button @click="goStudent">student进的</button>

    <br />
    <button @click="A">增</button>
    <button @click="B">删</button>
    <button @click="C">改</button>
    <button @click="D">查</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      token: this.getToken(),
      headers: {
        token: this.getToken(),
      },
    };
  },
  methods: {
    showMsg(result) {
      if (result.status === 200) {
        const data = result.data;
        if (data.status === 200) {
          this.$message({
            showClose: true,
            message: data.msg,
            type: "success",
            duration: "600",
          });
        } else {
          this.$message({
            showClose: true,
            message: data.msg,
            type: "error",
            duration: "3000",
          });
        }
      } else {
        this.$message({
          showClose: true,
          message: "操作失败,请联系管理员",
          type: "error",
          duration: "3000",
        });
      }
    },
    goAdmin() {
      this.$API.base.goAdmin(this.token).then((res) => {
        this.showMsg(res);
        if (res.data.status == 200) {
          this.$router.push("adminManage");
        }
      });
    },
    goTeacher() {
      this.$API.base.goTeacher(this.token).then((res) => {
        this.showMsg(res);
        if (res.data.status == 200) {
          this.$router.push("teacherManage");
        }
      });
    },
    goStudent() {
      this.$API.base.goStudent(this.token).then((res) => {
        this.showMsg(res);
        if (res.data.status == 200) {
          this.$router.push("studentManage");
        }
      });
    },

    // 增删改查
    A() {
      this.$API.base.goInsert(this.token).then((res) => {
        this.showMsg(res);
      });
    },
    B() {
      this.$API.base.goDelete(this.token).then((res) => {
        this.showMsg(res);
      });
    },
    C() {
      this.$API.base.goUpdate(this.token).then((res) => {
        this.showMsg(res);
      });
    },
    D() {
      this.$API.base.goSelect(this.token).then((res) => {
        this.showMsg(res);
      });
    },
  },
};
</script>

<style>
</style>

流程思路

 本文章参考:这位博主使用的是Jpa一些和其它的技术,我是用了mybatis并进行一些修改

一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统_大誌的博客-CSDN博客_shiro前后端分离

  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
课程简介:历经半个多月的时间,Debug亲自撸的 “企业员工角色权限管理平台” 终于完成了。正如字面意思,本课程讲解的是一个真正意义上的、企业级的项目实战,主要介绍了企业级应用系统中后端应用权限管理,其中主要涵盖了六大核心业务模块、十几张数据库表。 其中的核心业务模块主要包括用户模块、部门模块、岗位模块、角色模块、菜单模块和系统日志模块;与此同时,Debug还亲自撸了额外的附属模块,包括字典管理模块、商品分类模块以及考勤管理模块等等,主要是为了更好地巩固相应的技术栈以及企业应用系统业务模块的开发流程! 核心技术栈列表: 值得介绍的是,本课程在技术栈层面涵盖了前端和后端的大部分常用技术,包括Spring Boot、Spring MVC、MybatisMybatis-Plus、Shiro(身份认证与资源授权跟会话等等)、Spring AOP、防止XSS攻击、防止SQL注入攻击、过滤器Filter、验证码Kaptcha、热部署插件Devtools、POI、Vue、LayUI、ElementUI、JQuery、HTML、Bootstrap、Freemarker、一键打包部署运行工具Wagon等等,如下图所示: 课程内容与收益: 总的来说,本课程是一门具有很强实践性质的“项目实战”课程,即“企业应用员工角色权限管理平台”,主要介绍了当前企业级应用系统中员工、部门、岗位、角色、权限、菜单以及其他实体模块的管理;其中,还重点讲解了如何基于Shiro的资源授权实现员工-角色-操作权限、员工-角色-数据权限管理;在课程的最后,还介绍了如何实现一键打包上传部署运行项目等等。如下图所示为本权限管理平台的数据库设计图: 以下为项目整体的运行效果截图: 值得一提的是,在本课程中,Debug也向各位小伙伴介绍了如何在企业级应用系统业务模块的开发中,前端到后端再到数据库,最后再到服务器的上线部署运行等流程,如下图所示:
Shiro-Vue 可以帮助我们在Vue.js项目中实现按钮的权限控制。以下是控制按钮操作的步骤: 1. 首先,我们需要在后端的权限管理系统中配置按钮的权限信息。这可以包括按钮的唯一标识符、所属菜单或页面、权限标识符等。 2. 在前端的Vue.js项目中,我们需要使用Shiro-Vue提供的指令来控制按钮的显示或隐藏。比如,在按钮的HTML代码中添加v-permission指令: ``` <el-button v-permission="btn_add">新增按钮</el-button> ``` 这里的btn_add就是在后端配置的按钮权限标识符。 3. 在Vue.js组件中,我们需要引入Shiro-Vue提供的mixin,以便在组件中使用权限控制指令。可以在组件的script标签中添加如下代码: ``` import { ShiroPlugin } from 'shiro-vue'; export default { mixins: [ShiroPlugin], // ... } ``` 4. 接下来,我们可以在组件的mounted钩子函数中调用Shiro-Vue的init方法,从后端获取当前用户的权限信息。获取到权限信息后,Shiro-Vue会自动根据权限配置来控制按钮的显示或隐藏。 ``` mounted() { this.init(); // 初始化权限信息 } ``` 5. 最后,我们可以根据具体的业务需求,在按钮的点击事件处理函数中做权限判断。比如,在新增按钮的点击事件中,可以使用checkPermission方法来判断当前用户是否有权限执行该操作。 ``` methods: { handleAdd() { if (this.checkPermission('btn_add')) { // 执行新增操作 } else { // 没有权限,给出提示或采取其他操作 } } } ``` 通过以上步骤,我们可以实现在Vue.js项目中对按钮的权限控制。当用户没有权限时,按钮将被隐藏或禁用,从而保证了系统的安全性和稳定性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江河地笑

实践是检验真理的唯一标准

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值