若依框架实现不同端用户登录(后台管理用户和前台会员登录——sping security多用户)

本文详细介绍了如何在若依分离版的后台管理系统中,使用SpringSecurity框架实现前端会员系统和后台管理系统各自独立的登录接口,包括自定义UserDetailsService接口、AuthenticationManager、loginUser类的改造以及自定义登录接口的配置过程。
摘要由CSDN通过智能技术生成

目录

需求背景

前期准备

实现UserDetailsService接口

改造loginUser

声明自定义AuthenticationManager 的bean

自定义登录接口

参考文章

效果如下

需求背景

用若依搭建的后台管理环境,但是前台用户系统(前端)并没有和若依的前端集成在一起。是两个独立的前端项目,但是后端是集成在一起的。

我现在有一个前端会员表,若依有个系统用户表,如果想要会员登录(前台用户系统)和后台登录(后台管理系统)互不干扰怎么实现(两个登录接口)?

若依分离版使用的是sping security框架,所以本质上是sping security如何实现多用户!如果你已经具备springsecurity的知识,那么你会更轻松!

明确步骤
准备:准备自定义的用户实体类,强烈建议放在common中!

1、登录时查询我们自己的表,缓存用户信息时缓存我们用户实体类型:
        实现UserDetailsService接口;
        改造loginuser类;
        自己声明AuthenticationManager 的bean;
2、自定义登录接口:
        使用我们自定义的AuthenticationManager
3、从token能够获取登录人的信息
4、前端携带token

前期准备


准备一个我们自己的用户表实体类,假设叫做CustomerUser,这个实体类再多个模块中使用,强烈建议放在common模块中!

实现UserDetailsService接口


熟悉springsecurity的都知道,登录时查询用户表是依靠UserDetailsService接口实现的,我们想要登录时查询我们自己的会员表的话,就需要实现这个接口,参照若依的UserDetailsServiceImpl,我们假定他叫CustomerDetailsServiceImpl(请定义在framework模块!建议参考若依的userSetailServiceImpl的位置

代码如下

package com.ruoyi.framework.web.service;

import com.ruoyi.common.core.domain.entity.CustomerUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.customer.mapper.CustomerUserMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 用户验证处理
 *
 * @author ruoyi
 */
@Component("CustomerDetailsServiceImpl")
public class CustomerDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(CustomerDetailsServiceImpl.class);

    @Autowired
    private CustomerUserMapper customerUserMapper;

    @Autowired
    private CustomerPasswordService customerPasswordService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        CustomerUser user = customerUserMapper.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException(MessageUtils.message("user.not.exists"));
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException(MessageUtils.message("user.password.delete"));
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getActive()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException(MessageUtils.message("user.blocked"));
        }

        customerPasswordService.validate(user);

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(CustomerUser user)
    {
        return new LoginUser(user.getUserId(), user);
    }
}

改造loginUser

新增一个属性,CustomerUser

代码如下 

package com.ruoyi.common.core.domain.model;

import com.alibaba.fastjson2.annotation.JSONField;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;

/**
 * 登录用户身份权限
 * 
 * @author ruoyi
 */
public class LoginUser implements UserDetails
{
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 邀请用户ID
     */
    private Long inviteId;

    /**
     * 部门ID
     */
    private Long deptId;

    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private SysUser user;

    /**
     * 客户信息
     */
    private CustomerUser customerUser;

    public LoginUser()
    {
    }

    public LoginUser(SysUser user, Set<String> permissions)
    {
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
    {
        this.userId = userId;
        this.deptId = deptId;
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, CustomerUser user)
    {
        this.userId = userId;
        this.customerUser = user;
    }

    public Long getUserId() { return userId; }

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

    public Long getInviteId() { return inviteId; }

    public void setInviteId(Long inviteId)
    {
        this.inviteId = inviteId;
    }

    public Long getDeptId()
    {
        return deptId;
    }

    public void setDeptId(Long deptId)
    {
        this.deptId = deptId;
    }

    public String getToken()
    {
        return token;
    }

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

    @JSONField(serialize = false)
    @Override
    public String getPassword()
    {
        if(user != null){
            return user.getPassword();
        }else{
            return customerUser.getPassword();
        }
    }

    @Override
    public String getUsername()
    {
        if(user != null){
            return user.getUserName();
        }else{
            return customerUser.getUserName();
        }
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     * 
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     * 
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     * 
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled()
    {
        return true;
    }

    public Long getLoginTime()
    {
        return loginTime;
    }

    public void setLoginTime(Long loginTime)
    {
        this.loginTime = loginTime;
    }

    public String getIpaddr()
    {
        return ipaddr;
    }

    public void setIpaddr(String ipaddr)
    {
        this.ipaddr = ipaddr;
    }

    public String getLoginLocation()
    {
        return loginLocation;
    }

    public void setLoginLocation(String loginLocation)
    {
        this.loginLocation = loginLocation;
    }

    public String getBrowser()
    {
        return browser;
    }

    public void setBrowser(String browser)
    {
        this.browser = browser;
    }

    public String getOs()
    {
        return os;
    }

    public void setOs(String os)
    {
        this.os = os;
    }

    public Long getExpireTime()
    {
        return expireTime;
    }

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

    public Set<String> getPermissions()
    {
        return permissions;
    }

    public void setPermissions(Set<String> permissions)
    {
        this.permissions = permissions;
    }

    public SysUser getUser(){ return user; }

    public void setUser(SysUser user)
    {
        this.user = user;
    }

    public CustomerUser getCustomerUser(){ return customerUser; }

    public void setCustomerUser(CustomerUser customerUser) {  this.customerUser = customerUser; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        return null;
    }
}

声明自定义AuthenticationManager 的bean

原因

是这个authenticationManager调用的是若依定义的UserDetailsServiceImpl而不是我们定义的CustomerDetailsServiceImpl ,那我们怎么样才能让他调用我们定义的CustomerDetailsServiceImpl 呢?其中关键的是authenticationManager,他决定了调用哪一个

一看@Resource就知道,他注入了一个bean!那他肯定声明了这个bean!我们要找到这个AuthenticationManager的bean声明

这个AuthenticationManager用的就是若依定义的userDetailsService!

package com.ruoyi.framework.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;

/**
 * spring security配置
 * 
 * @author ruoyi
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{

    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    @Qualifier("CustomerDetailsServiceImpl")
    private UserDetailsService customerDetailsServiceImpl;
    
    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 允许匿名访问的地址
     */
    @Autowired
    private PermitAllUrlProperties permitAllUrl;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    @Primary
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @Bean("MemberAuthenticationManager")
    public AuthenticationManager MemberAuthenticationManagerBean() throws Exception
    {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(customerDetailsServiceImpl);
        // BCryptPasswordEncoder 使用BCrypt的强散列哈希加密实现,并可以由客户端指定加密的强度strength,强度越高安全性自然就越高,默认为10.
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // authenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());//明文密码存储
        authenticationProvider.setPasswordEncoder(passwordEncoder);// 加密密码存储
        return new ProviderManager(authenticationProvider);
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 注解标记允许匿名访问的url
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

        //        anonymous() 允许匿名用户访问,不允许已登入用户访问
        //        permitAll() 不管登入,不登入 都能访问
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").permitAll()
                .antMatchers( "/app/employee/user/employeeUserList").permitAll()
                .antMatchers( "/employee/user/employeeUserList", "/employee/user/info/*").permitAll()
                .antMatchers( "/system/dict/data/list").permitAll()
                .antMatchers("/customer/info/customerLogin").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

自定义登录接口


上面我们自定义了一个AuthenticationManager,并且这个AuthenticationManager指向的是我们自己的CustomerDetailsService。那我们就需要在使用这个AuthenticationManager。

package com.ruoyi.web.controller.module.customer;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.CustomerLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 登录验证
 * 
 * @author ruoyi
 */
@RestController
@RequestMapping("/customer/info")
public class CustomerInfoController
{
    @Autowired
    private CustomerLoginService customerLoginService;

    /**
     * 登录方法
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/customerLogin")
    public AjaxResult customerLogin(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = customerLoginService.customerLogin(loginBody.getUsername(), loginBody.getPassword());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

    /**
     * 获取用户信息
     *
     * @return 用户信息
     */
    @GetMapping("getCustomerInfo")
    public AjaxResult getCustomerInfo()
    {
        CustomerUser user = SecurityUtils.getLoginUser().getCustomerUser();
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        return ajax;
    }

}

sevice代码

package com.ruoyi.framework.web.service;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.*;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import com.ruoyi.customer.service.ICustomerUserService;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;


/**
 * 登录校验方法
 * 
 * @author ruoyi
 */
@Component
public class CustomerLoginService
{
    // 是否允许账户多终端同时登录(true允许 false不允许)
    @Value("${token.soloLogin}")
    private boolean soloLogin;

    @Autowired
    private RedisCache redisCache;

    @Resource
    @Qualifier("MemberAuthenticationManager")
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private ICustomerUserService customerUerService;

    @Autowired
    private TokenService tokenService;


    /**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String customerLogin(String username, String password)
    {
        // 登录前置校验
        loginPreCheck(username, password);
        CustomerUser user = null;
        // 用户验证
        Authentication authentication = null;

        try
        {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            System.out.println("authenticationToken");
            System.out.println(authenticationToken);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用CustomerDetailsServiceImpl.loadUserByUsernam
            authentication = authenticationManager.authenticate(authenticationToken);
            System.out.println("authentication");
            System.out.println(authentication);
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        if (!soloLogin)
        {
            // 如果用户不允许多终端同时登录,清除缓存信息
            String userIdKey = Constants.LOGIN_USERID_KEY + loginUser.getCustomerUser().getUserId();
            String userKey = redisCache.getCacheObject(userIdKey);
            if (StringUtils.isNotEmpty(userKey))
            {
                redisCache.deleteObject(userIdKey);
                redisCache.deleteObject(userKey);
            }
        }
        // 生成token
        return tokenService.createToken(loginUser);
    }

    /**
     * 登录前置校验
     * @param username 用户名
     * @param password 用户密码
     */
    public void loginPreCheck(String username, String password)
    {
        // 用户名或密码为空 错误
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
            throw new UserNotExistsException();
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    public void recordLoginInfo(Long userId)
    {
        CustomerUser customerUser = new CustomerUser();
        customerUser.setUserId(userId);
        customerUser.setLoginIp(IpUtils.getIpAddr());
        customerUser.setLoginDate(DateUtils.getNowDate());
        customerUerService.updateUserProfile(customerUser);
    }
}

参考文章

若依分离版新增用户表实现会员与后台分别登录(sping security多用户)_若依怎么向sys_user添加数据-CSDN博客

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值