Spring Boot整合Spring Security

Spring Security是Spring官方推荐的认证、授权框架,功能相比Apache Shiro功能更丰富也更强大,但是使用起来更麻烦。

如果使用过Apache Shiro,学习Spring Security会比较简单一点,两种框架有很多相似的地方。


一、准备工作

1、创建项目

在IntelliJ IDEA中创建一个springboot项目springboot-springsecurity

2、添加依赖

修改pom.xml,添加项目所需的依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.9</version>
        <relativePath />
    </parent>

    <groupId>cn.edu.sgu.www</groupId>
    <artifactId>springboot-springsecurity</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-springsecurity</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <jjwt.version>0.9.1</jjwt.version>
        <mysql.version>8.0.28</mysql.version>
        <druid.version>1.1.21</druid.version>
        <lombok.version>1.18.22</lombok.version>
        <mybatis.version>2.2.2</mybatis.version>
        <mybatis-plus.version>3.5.1</mybatis-plus.version>
    </properties>

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

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

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

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

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!--jjwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
    </dependencies>

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

3、修改配置

将配置文件application.properties重命名为application.yml,修改配置文件的内容。

server:
  port: 8080
  servlet:
    context-path: /

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/spring-security
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis-plus:
  mapper-locations: classpath:mapper/*Mapper.xml

logging:
  level:
    springfox: error
    cn.edu.sgu.www.security: debug

system:
  login-page: /login.html
  login-url: /user/login
  index-page: /index.html
  logout-url: /user/logout
  parameter:
    username: username
    password: password
  white-url:
    - /js/**
    - /css/**
    - /images/**
    - /user/login
    - /login.html

二、创建相关的类

公共类

ResponseCode.java

响应状态码枚举类

package cn.edu.sgu.www.security.restful;

/**
 * 响应状态码
 * @author heyunlin
 * @version 1.0
 */
public enum ResponseCode {
    /**
     * 请求成功
     */
    OK(200),
    /**
     * 失败的请求
     */
    BAD_REQUEST(400),
    /**
     * 未授权
     */
    UNAUTHORIZED(401),
    /**
     * 禁止访问
     */
    FORBIDDEN(403),
    /**
     * 找不到(该状态不可用)
     */
    NOT_FOUND(404),
    /**
     * 不可访问
     */
    NOT_ACCEPTABLE(406),
    /**
     * 冲突
     */
    CONFLICT(409),
    /**
     * 服务器发生异常
     */
    ERROR(500);

    private final Integer value;

    ResponseCode(Integer value) {
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }

}

GlobalException.java

自定义的全局异常类

package cn.edu.sgu.www.security.exception;

import cn.edu.sgu.www.security.restful.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 自定义的全局异常类
 * @author heyunlin
 * @version 1.0
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class GlobalException extends RuntimeException {

    private ResponseCode responseCode;

    public GlobalException(ResponseCode responseCode, String message) {
        super(message);

        setResponseCode(responseCode);
    }

}

JsonResult.java

自定义的统一响应实体类

package cn.edu.sgu.www.security.restful;

import lombok.Data;

/**
 * 响应实体类
 * @param <T> 响应数据的类型
 * @author heyunlin
 * @version 1.0
 */
@Data
public class JsonResult<T> {

    /**
     * 响应数据
     */
    private T data;

    /**
     * 响应状态码
     */
    private Integer code;

    /**
     * 响应提示信息
     */
    private String message;

    public static JsonResult<Void> success() {
        return success(null);
    }

    public static JsonResult<Void> success(String message) {
        return success(message, null);
    }

    public static <T> JsonResult<T> success(String message, T data) {
        JsonResult<T> jsonResult = new JsonResult<>();

        jsonResult.setCode(ResponseCode.OK.getValue());
        jsonResult.setMessage(message);
        jsonResult.setData(data);

        return jsonResult;
    }

    public static JsonResult<Void> error(String message) {
        JsonResult<Void> jsonResult = new JsonResult<>();

        jsonResult.setCode(ResponseCode.ERROR.getValue());
        jsonResult.setMessage(message);

        return jsonResult;
    }

    public static JsonResult<Void> error(ResponseCode responseCode, Throwable e) {
        return error(responseCode, e.getMessage() != null ? e.getMessage() : "系统发生异常,请联系管理员!");
    }

    public static JsonResult<Void> error(ResponseCode responseCode, String message) {
        JsonResult<Void> jsonResult = new JsonResult<>();

        jsonResult.setCode(responseCode.getValue());
        jsonResult.setMessage(message);

        return jsonResult;
    }

}

GlobalExceptionHandler.java

全局异常处理类

package cn.edu.sgu.www.security.restful.handler;

import cn.edu.sgu.www.security.exception.GlobalException;
import cn.edu.sgu.www.security.restful.JsonResult;
import cn.edu.sgu.www.security.restful.ResponseCode;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

/**
 * 全局异常处理类
 * @author heyunlin
 * @version 1.0
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理GlobalException
     * @param e GlobalException
     * @return JsonResult<Void>
     */
    @ExceptionHandler(GlobalException.class)
    public JsonResult<Void> handleGlobalException(HttpServletResponse response, GlobalException e) {
        printMessage(e);

        response.setStatus(e.getResponseCode().getValue());

        return JsonResult.error(e.getResponseCode(), e);
    }

    /**
     * 处理BindException
     * @param e BindException
     * @return JsonResult<Void>
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public JsonResult<Void> handleBindException(BindException e) {
        printMessage(e);

        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();
        String defaultMessage = Objects.requireNonNull(fieldError).getDefaultMessage();

        return JsonResult.error(ResponseCode.BAD_REQUEST, defaultMessage);
    }

    /**
     * 处理Exception
     * @param e Exception
     * @return JsonResult<Void>
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult<Void> handleException(Exception e) {
        printMessage(e);

        return JsonResult.error(ResponseCode.ERROR, e);
    }

    /**
     * 打印异常信息
     * @param e Exception
     */
    private void printMessage(Exception e) {
        System.out.println(e.getMessage());

        e.printStackTrace();
    }

}

业务类

User.java

package cn.edu.sgu.www.security.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @author heyunlin
 * @version 1.0
 */
@Data
@TableName("user")
public class User implements Serializable {
    private static final long serialVersionUID = 18L;

    @TableId(value = "id", type = IdType.INPUT)
    private String id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 性别
     */
    private Integer gender;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 是否启用
     */
    private boolean enable;

    /**
     * 最后一次登录时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime lastLoginTime;
}

UserMapper.java

package cn.edu.sgu.www.security.mapper;

import cn.edu.sgu.www.security.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;

/**
 * @author heyunlin
 * @version 1.0
 */
@Repository
public interface UserMapper extends BaseMapper<User> {
    
}

UserLoginDTO.java

package cn.edu.sgu.www.security.dto;

import lombok.Data;

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

/**
 * @author heyunlin
 * @version 1.0
 */
@Data
public class UserLoginDTO implements Serializable {
    private static final long serialVersionUID = 18L;

    /**
     * 用户名
     */
    @NotBlank(message = "用户名不允许为空")
    private String username;

    /**
     * 密码
     */
    @NotBlank(message = "密码不允许为空")
    private String password;
}

UserService.java

package cn.edu.sgu.www.security.service;

import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;

/**
 * @author heyunlin
 * @version 1.0
 */
public interface UserService {

    /**
     * 登录认证
     * @param userLoginDTO 用户登录信息
     */
    void login(UserLoginDTO userLoginDTO);

    /**
     * 退出登录
     */
    void logout();

    /**
     * 通过ID查询用户信息
     * @param userId 用户ID
     * @return User 通过ID查询到的用户信息
     */
    User selectById(String userId);
}

UserController.java

package cn.edu.sgu.www.security.controller;

import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;
import cn.edu.sgu.www.security.restful.JsonResult;
import cn.edu.sgu.www.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author heyunlin
 * @version 1.0
 */
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public JsonResult<Void> login(@Validated UserLoginDTO userLoginDTO) {
        userService.login(userLoginDTO);

        return JsonResult.success("登录成功");
    }

    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    public JsonResult<Void> logout() {
        userService.logout();

        return JsonResult.success("登出成功");
    }

    @RequestMapping(value = "/selectById", method = RequestMethod.GET)
    public JsonResult<User> selectById(@RequestParam(value = "id") String userId) {
        User user = userService.selectById(userId);

        return JsonResult.success(null, user);
    }

}

 UserServiceImpl.java

package cn.edu.sgu.www.security.service.impl;

import cn.edu.sgu.www.security.dto.UserLoginDTO;
import cn.edu.sgu.www.security.entity.User;
import cn.edu.sgu.www.security.mapper.UserMapper;
import cn.edu.sgu.www.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

/**
 * @author heyunlin
 * @version 1.0
 */
@Service
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public UserServiceImpl(UserMapper userMapper, AuthenticationManager authenticationManager) {
        this.userMapper = userMapper;
        this.authenticationManager = authenticationManager;
    }

    @Override
    public void login(UserLoginDTO userLoginDTO) {
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                userLoginDTO.getUsername(),
                userLoginDTO.getPassword()
        );

        authenticationManager.authenticate(authentication);
    }

    @Override
    public void logout() {
        // todo
    }

    @Override
    public User selectById(String userId) {
        return userMapper.selectById(userId);
    }

}

配置类

SystemProperties.java

创建配置读取类SystemProperties

package cn.edu.sgu.www.security.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

/**
 * @author heyunlin
 * @version 1.0
 */
@Data
@Component
@ConfigurationProperties(prefix = "system")
public class SystemProperties {

    /**
     * 登录页面
     */
    private String loginPage;

    /**
     * 登录的请求地址
     */
    private String loginUrl;

    /**
     * 登录成功后跳转的页面
     */
    private String indexPage;

    /**
     * 退出登录的请求地址
     */
    private String logoutUrl;

    /**
     * 白名单
     */
    private List<String> whiteUrl;

    /**
     * 登录的参数
     */
    private Map<String, String> parameter;
}

MybatisPlusConfig.java

package cn.edu.sgu.www.security.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Mybatis-Plus配置类
 * @author heyunlin
 * @version 1.0
 */
@Configuration
@MapperScan(basePackages = "cn.edu.sgu.www.security.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 防全表更新与删除插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }

}

Spring Security相关的类

UserDetailsServiceImpl.java

UserDetailsService接口是Spring Security中非常重要的接口,在登录认证的时候会通过这个接口的loadUserByUsername()方法获取用户的信息,来完成登录的用户名、密码校验,完成登录流程。

package com.example.security.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.security.entity.User;
import com.example.security.exception.GlobalException;
import com.example.security.mapper.UserMapper;
import com.example.security.restful.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

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

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;

    @Autowired
    public UserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        User user = selectByUsername(username);

        if (user == null) {
            throw new BadCredentialsException("登录失败,用户名不存在!");
        } else {
            List<String> permissions = selectPermissions(username);

            return org.springframework.security.core.userdetails.User.builder()
                    .username(user.getUsername())
                    .password(user.getPassword())
                    .accountExpired(false)
                    .accountLocked(false)
                    .disabled(!user.getEnable())
                    .credentialsExpired(false)
                    .authorities(permissions.toArray(new String[] {}))
                    .build();
        }
    }

    /**
     * 通过用户名查询用户信息
     * @param username 用户名
     * @return User
     */
    private User selectByUsername(String username) {
        QueryWrapper<User> wrapper = new QueryWrapper<>();

        wrapper.eq("username", username);

        List<User> list = userMapper.selectList(wrapper);

        if (list.size() == 1) {
            return list.get(0);
        }

        return null;
    }

    /**
     * 通过用户名查询用户权限
     * @param username 用户名
     * @return List<String>
     */
    private List<String> selectPermissions(String username) {
        if (username == null) {
            throw new GlobalException(ResponseCode.BAD_REQUEST, "用户名不能为空");
        }

        List<String> permissions = new ArrayList<>();

        permissions.add("/user/login");
        permissions.add("/user/logout");
        permissions.add("/user/selectById");

        return permissions;
    }

}

LoginSuccessHandler.java

登录成功的处理器

package cn.edu.sgu.www.security.security;

import cn.edu.sgu.www.security.config.SystemProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

/**
 * @author heyunlin
 * @version 1.0
 */
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final SystemProperties systemProperties;

    public LoginSuccessHandler(SystemProperties systemProperties) {
        this.systemProperties = systemProperties;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.sendRedirect(systemProperties.getIndexPage());
    }

}

LoginFailHandler.java

登录失败的处理器

package cn.edu.sgu.www.security.security;

import cn.edu.sgu.www.security.exception.GlobalException;
import cn.edu.sgu.www.security.restful.ResponseCode;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author heyunlin
 * @version 1.0
 */
public class LoginFailHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
        throw new GlobalException(ResponseCode.BAD_REQUEST, "登录失败,用户名或密码错误~");
    }

}

SecurityConfig.java

在项目根包下创建security的配置类config.SecurityConfig

package cn.edu.sgu.www.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author heyunlin
 * @version 1.0
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final SystemProperties systemProperties;

    @Autowired
    public SecurityConfig(SystemProperties systemProperties) {
        this.systemProperties = systemProperties;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return (String) charSequence;
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return charSequence.equals(s);
            }
        };
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用防跨域攻击
        http.csrf().disable();

        // 配置各请求路径的认证与授权
        http.formLogin()
                .loginPage(systemProperties.getLoginPage()) // 自定义登录页面的地址
                .loginProcessingUrl(systemProperties.getLoginUrl()) // 处理登录的接口地址
                .usernameParameter(systemProperties.getParameter().get("username")) // 用户名的参数名
                .passwordParameter(systemProperties.getParameter().get("password")) // 密码的参数名
                .successHandler(new LoginSuccessHandler(systemProperties))
                //.successForwardUrl("/index.html") // 登录成功跳转的地址
                .failureHandler(new LoginFailHandler()); // 登录失败的处理器

        // 退出登录相关配置
        http.logout()
                .logoutUrl(systemProperties.getLogoutUrl()) // 退出登录的接口地址
                .logoutSuccessUrl(systemProperties.getLoginUrl()); // 退出登录成功跳转的地址

        // 配置认证规则
        String[] toArray = systemProperties.getWhiteUrl().toArray(new String[]{});
        http.authorizeRequests()
                .antMatchers(toArray).permitAll() // 白名单,也就是不需要登录也能访问的资源
                .anyRequest().authenticated();
    }

}

项目的包结构


文章就分享到这里了,文章的代码已上传到Gitee,可按需获取~

Spring Boot整合Spring Security案例项目icon-default.png?t=O83Ahttps://gitee.com/muyu-chengfeng/springboot-springsecurity.git


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沐雨橙风ιε

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值