SpringSecurity

SpringSecurity

它是一个身份认证及权限控制的框架。shiro是它的对标框架,springsecurity已经开发很多年了,由于shiro的优势,它发展的不是很好,直到springboot出现,security的配置变的非常简单,现在微服务的项目都会优先选择security框架。security实现原理是通过Filter实现的。

代码分布截图

 注意:导包导的user要是自己的,不能导依赖里面自带的user包。

1、基础代码

Result

import lombok.Data;
​
import java.util.HashMap;
import java.util.Map;
​
import static com.wztsl.springboo.contants.ResultCode.ERROR;
import static com.wztsl.springboo.contants.ResultCode.SUCCESS;
​
/**
 * @author szsw
 * @date 2023/2/3 18:18:15
 */
@Data
public class Result {
​
    private Integer code;
​
    private String message;
​
    private Map<String, Object> map = new HashMap<>();
​
    private Result() {
    }
​
    public static Result ok() {
        Result r = new Result();
        r.setCode(SUCCESS.getCode());
        r.setMessage(SUCCESS.getMessage());
        return r;
    }
​
    public static Result error() {
        Result r = new Result();
        r.setCode(ERROR.getCode());
        r.setMessage(ERROR.getMessage());
        return r;
    }
​
    public Result put(String key, Object value) {
        map.put(key, value);
        return this;
    }
​
    public Object get(String key) {
        return map.get(key);
    }
​
}

ResultCode

/**
 * @author szsw
 * @date 2023/2/3 18:26:03
 */
public enum ResultCode {
​
    /**
     *
     */
    SUCCESS(0, "请求成功"),
    ERROR(1, "请求失败"),
    ;
​
    private int code;
    private String message;
​
    ResultCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
​
    public int getCode() {
        return code;
    }
​
    public String getMessage() {
        return message;
    }
}

StringConstant

/**
 * @author szsw
 * @date 2023/2/3 19:42:55
 */
public class StringConstant {
​
    /**
     * session当前登录人信息
     */
    public static final String SESSION_USER = "SESSION_USER";
    public static final String AUTH_LIST = "AUTH_LIST";
    public static final String TOKEN = "token";
​
​
    private StringConstant() {
    }
}
​

AuthenticationEnum

/**
 * @author szsw
 * @date 2023/2/3 19:43:10
 */
public enum AuthenticationEnum {
}
​

2、工具代码

Md5

import lombok.extern.slf4j.Slf4j;
​
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
​
@Slf4j
@SuppressWarnings("java:S112")
public final class Md5 {
​
    private Md5() {
    }
​
    public static String encrypt(String strSrc) {
        try {
            char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f'};
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("Md5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            int i;
            for (i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            log.info("MD5计算异常!", e);
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
​
}

ResponseUtil

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wztsl.springboo.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
​
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
​
/**
 * 响应工具
 *
 * @author by szsw
 */
@Slf4j
public class ResponseUtil {
​
    private ResponseUtil() {
    }
​
    public static void out(HttpServletResponse response, Result r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getOutputStream(), r);
        } catch (IOException e) {
            log.info("响应IO异常!", e);
        }
    }
}
​

TokenManager

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
​
import java.util.Date;
​
/**
 * token管理
 *
 * @author by szsw
 */
@Component
public class TokenManager {
​
    private static final long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000L;
​
    private String tokenSignKey = "123456";
​
    /**
     * 生成token
     *
     * @param username 用户名
     * @return 通过用户名生成的token
     */
    public String createToken(String username) {
        return Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXPIRATION))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
    }
​
    /**
     * 通过token获取用户名
     *
     * @param token 令牌
     * @return 令牌对应的用户名
     */
    public String getUserFromToken(String token) {
        return Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
    }
​
    @SuppressWarnings("all")
    public void removeToken(String token) {
        //jwttoken无需删除,客户端扔掉即可。
    }
​
}

3、实体代码

User

import lombok.Data;
​
/**
 * @author szsw
 * @date 2023/2/3 20:11:37
 */
@Data
public class User {
​
    private String username;
    private String password;
​
}
​

SecurityUser

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

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

/**
 * @author szsw
 * @date 2023/2/3 20:11:45
 */
@Data
public class SecurityUser implements UserDetails {

    private User user;
    private List<String> authList;

    public SecurityUser(User user) {
        this.user = user;
    }

    /**
     * 返回用户的权限
     *
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        if (CollectionUtils.isEmpty(authList)) {
            return authorities;
        }
        for (String auths : authList) {
            if (StringUtils.isEmpty(auths)) {
                continue;
            }
            authorities.add(new SimpleGrantedAuthority(auths));
        }
        return authorities;
    }

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * Returns the username used to authenticate the user. Cannot return <code>null</code>.
     *
     * @return the username (never <code>null</code>)
     */
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     *
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     *
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

4、配置代码

TokenWebSecurityConfig

import com.wztsl.springboo.security.filter.TokenAuthenticationFilter;
import com.wztsl.springboo.security.filter.TokenLoginFilter;
import com.wztsl.springboo.security.filter.TokenLogoutHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * Security配置类
 *
 * @author by szsw
 * @date 2020/10/27 16:39
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;
    private TokenManager tokenManager;
    private DefaultPasswordEncoder defaultPasswordEncoder;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder, TokenManager tokenManager) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
    }


    /**
     * 配置设置
     *
     * @param http httpSecurity配置对象
     * @throws Exception 异常
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /*
         * always – 如果session不存在总是需要创建;
         * ifRequired – 仅当需要时,创建session(默认配置);
         * never – 框架从不创建session,但如果已经存在,会使用该session ;
         * stateless – Spring Security不会创建session,或使用session;
         * 解决Feign调用时session丢失问题
         */
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
        /*
         * "migrateSession",即认证时,创建一个新http session,原session失效,属性从原session中拷贝过来
         * “none”,原session保持有效;
         * “newSession”,新创建session,且不从原session中拷贝任何属性。
         * 解决Feign调用时session丢失问题
         */
        http.sessionManagement().sessionFixation().none();
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and()
                .cors()
                .and()
                .csrf()
                .disable()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .logout()
                .logoutUrl("/center/logout")
                .addLogoutHandler(new TokenLogoutHandler(tokenManager))
                .and()
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager))
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager))
                .httpBasic();

    }

    /**
     * 密码处理
     *
     * @param auth 权限管理创建者
     * @throws Exception 异常
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     *
     * @param web webSecurity配置对象
     * @throws Exception 异常
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        String[] matchers = {"/**/allowAuth/**", "/assets/**"};
        web.ignoring().antMatchers(matchers);
    }

}

DefaultPasswordEncoder

import com.wztsl.springboo.utils.Md5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * @author szsw
 * @date 2023/2/3 19:46:52
 */
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    /**
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
     * greater hash combined with an 8-byte or greater randomly generated salt.
     *
     * @param rawPassword 密码
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return Md5.encrypt(rawPassword.toString());
    }

    /**
     * Verify the encoded password obtained from storage matches the submitted raw
     * password after it too is encoded. Returns true if the passwords match, false if
     * they do not. The stored password itself is never decoded.
     *
     * @param rawPassword     the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return true if the raw password, after encoding, matches the encoded password from
     * storage
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if(rawPassword == null || encodedPassword == null){
            return false;
        }
        return encodedPassword.equals(Md5.encrypt(rawPassword.toString()));
    }
}

UnauthorizedEntryPoint

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

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

/**
 * @author szsw
 * @date 2023/2/3 19:47:06
 */
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    /**
     * Commences an authentication scheme.
     * <p>
     * <code>ExceptionTranslationFilter</code> will populate the <code>HttpSession</code>
     * attribute named
     * <code>AbstractAuthenticationProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY</code>
     * with the requested target URL before calling this method.
     * <p>
     * Implementations should modify the headers on the <code>ServletResponse</code> as
     * necessary to commence the authentication process.
     *
     * @param request       that resulted in an <code>AuthenticationException</code>
     * @param response      so that the user agent can begin authentication
     * @param authException that caused the invocation
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

    }
}

5、Filter代码

TokenAuthenticationFilter

import com.wztsl.springboo.utils.ResponseUtil;
import com.wztsl.springboo.utils.StringUtils;
import com.wztsl.springboo.utils.TokenManager;
import com.wztsl.springboo.vo.Result;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collection;
import java.util.List;

import static com.wztsl.springboo.contants.StringConstant.*;

/**
 * @author szsw
 * @date 2023/2/3 19:47:36
 */
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager, TokenManager tokenManager) {
        super(authenticationManager);
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String requestUri = request.getRequestURI();
        handing(request, response, chain, requestUri);
    }

    @SuppressWarnings("unchecked")
    private void handing(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String requestUri) throws IOException, ServletException {
        HttpSession session = request.getSession();
        Object userObj = session.getAttribute(SESSION_USER);
        if (userObj != null) {
            UsernamePasswordAuthenticationToken auth = createUpat(request);
            if(auth == null){
                ResponseUtil.out(response, Result.error("没有权限!"));
            }
            SecurityContextHolder.getContext().setAuthentication(auth);
            List<SimpleGrantedAuthority> authorityList = (List<SimpleGrantedAuthority>) session.getAttribute(AUTH_LIST);
            boolean authFlag = authorityList.stream().anyMatch(t -> requestUri.contains(t.getAuthority()));
            if (authFlag) {
                chain.doFilter(request, response);
            }else{
                ResponseUtil.out(response, Result.error("没有权限!"));
            }
        } else {
            ResponseUtil.out(response, Result.error("请登录!"));
        }
    }

    @SuppressWarnings("unchecked")
    private UsernamePasswordAuthenticationToken createUpat(HttpServletRequest request) {
        String token = request.getHeader(TOKEN);
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        String username = tokenManager.getUserFromToken(token);
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        Object authList = request.getSession().getAttribute(AUTH_LIST);
        return new UsernamePasswordAuthenticationToken(username, token, (Collection<? extends GrantedAuthority>) authList);
    }


}

TokenLoginFilter

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wztsl.springboo.utils.ResponseUtil;
import com.wztsl.springboo.utils.TokenManager;
import com.wztsl.springboo.vo.Result;
import com.wztsl.springboo.vo.SecurityUser;
import com.wztsl.springboo.vo.User;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

import static com.wztsl.springboo.contants.StringConstant.AUTH_LIST;
import static com.wztsl.springboo.contants.StringConstant.SESSION_USER;

/**
 * @author szsw
 * @date 2023/2/3 19:47:46
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login"));
    }

    /**
     * 获取数据和返回响应的
     *
     * @param request  请求
     * @param response 响应
     * @return 认证
     * @throws AuthenticationException 认证过程中可能会由Spring抛出认证异常
     */
    @Override
    @SuppressWarnings("java:S112")
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>());
            return authenticationManager.authenticate(authToken);
        } catch (IOException e) {
            throw new RuntimeException("认证失败!");
        }
    }

    /**
     * 认证成功
     *
     * @param request    请求
     * @param response   响应
     * @param chain      chain对象
     * @param authResult the object returned from the <tt>attemptAuthentication</tt>
     *                   method.
     * @throws IOException      认证过程中可能会由Spring抛出认证异常
     * @throws ServletException 认证过程中可能会由Spring抛出认证异常
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
        String token = tokenManager.createToken(securityUser.getUsername());
        request.getSession().setAttribute(SESSION_USER, securityUser.getUser());
        request.getSession().setAttribute(AUTH_LIST, securityUser.getAuthorities());
        ResponseUtil.out(response, Result.ok().put("token", token));
    }

    /**
     * 认证失败
     *
     * @param request  请求
     * @param response 响应
     * @param e        认证异常
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, Result.error("用户名或密码错误!"));
    }
}

TokenLogoutHandler

import com.wztsl.springboo.utils.TokenManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

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

/**
 * @author szsw
 * @date 2023/2/3 19:47:54
 */
public class TokenLogoutHandler implements LogoutHandler {

    private TokenManager tokenManager;

    public TokenLogoutHandler(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }

    /**
     * Causes a logout to be completed. The method must complete successfully.
     *
     * @param request        the HTTP request
     * @param response       the HTTP response
     * @param authentication the current principal details
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

    }
}

6、service代码

UserDetailsServiceImpl

import com.wztsl.springboo.utils.Md5;
import com.wztsl.springboo.vo.SecurityUser;
import com.wztsl.springboo.vo.User;
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.Service;

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

/**
 * @author szsw
 * @date 2023/2/3 19:48:10
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    /**
     * Locates the user based on the username. In the actual implementation, the search
     * may possibly be case sensitive, or case insensitive depending on how the
     * implementation instance is configured. In this case, the <code>UserDetails</code>
     * object that comes back may have a username that is of a different case than what
     * was actually requested..
     *
     * @param username the username identifying the user whose data is required.
     * @return a fully populated user record (never <code>null</code>)
     * @throws UsernameNotFoundException if the user could not be found or the user has no
     *                                   GrantedAuthority
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        SecurityUser securityUser = new SecurityUser(user);
        if ("admin".equals(username)) {
            user.setPassword(Md5.encrypt("123456"));
            user.setUsername(username);
            List<String> authList = new ArrayList<>();
            authList.add("start/test");
            securityUser.setAuthList(authList);
        }
        return securityUser;
    }
}

7、maven依赖

<jwt.version>0.7.0</jwt.version>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

8、resources

application

server:
  port: 8090
  servlet:
    context-path: /security
spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
mybatis:
  type-aliases-package: com.jr.pojo
  mapper-locations: classpath:mapper/*.xml

9、SpringBoot129Application

package com.jr;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBoot129Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringBoot129Application.class,args);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值