基于jwt与springSecurity实现方法拦截(单点登录)

0、什么是jwt,什么是Security

两篇完成项目主要需要的基础知识
jwt是什么?:https://www.cnblogs.com/yan7/p/7857833.html
security登录的过程:https://blog.csdn.net/abcwanglinyong/article/details/80981389

1、导包

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.0.RELEASE</spring-boot.version>
        <akka.version>2.5.21</akka.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.22</version>
            </dependency>
            <dependency>
                <groupId>com.kejin.util</groupId>
                <artifactId>autoCoding</artifactId>
                <version>1.0.0</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.13</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.0.6</version>
                <exclusions>
                    <exclusion>
                        <artifactId>spring-boot-autoconfigure</artifactId>
                        <groupId>org.springframework.boot</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.1.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.4.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

基本上用到的包在都了。

2、创建一个springBoot工程,使用idea的spring initializr.然后导包

说一下实现思路

第一次登录:用户登录----security拦截-----根据用户名查信息是否存在-----存在放入缓存----生成token-----返回客户端
第二次访问:携带token—缓存取值–存在就放行。

要实现:
UsernamePasswordAuthenticationFilter拦截器执行前添加一个自定义拦截器(JwtAuthorizationTokenFilter)。
并且写一个拦截器(LoginFilter)替换UsernamePasswordAuthenticationFilter拦截器。

JwtAuthorizationTokenFilter用于验证token信息(是否过期等等),并将其存在security的上下文中(及当前用户信息)SecurityContextHolder.getContext().setAuthentication(这里存用户信息)

0、贴上JwtUtil工具类
package com.top.system.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.UUID;

/**
 * @author 网上搜索的
 * @desc JWT工具类
 **/
public class JwtUtil {
    /**
     * 一周过期时间
     */
    private static final long EXPIRE_TIME =7 * 24 * 3600 * 1000;
 
    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            //根据密码生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            //效验TOKEN
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }
 
    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getVersion(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("version").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,5min后过期
     *
     * @param username 用户名
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String version, String secret) {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withClaim("version", version)
                .withExpiresAt(date)
                .withClaim("random", UUID.randomUUID().toString())
                .sign(algorithm);
    }
}

1、新建JwtAuthorizationTokenFilter 继承BasicAuthenticationFilter(基础权限过滤)
/**
 * @author 薛向毅
 * @description JWT token认证过滤器
 */
@Slf4j
public class JwtAuthorizationTokenFilter extends BasicAuthenticationFilter {

    @Autowired
    private Cache<String, JwtUser> cache;

    @Autowired
    public JwtAuthorizationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 校验token合法性,然后组装成认证实体给后续的组件{@link MyPermissionEvaluator}判断权限
     *MyPermissionEvaluator Security的自定义方法拦截类,他可以获取SecurityContextHolder.getContext().setAuthentication(jwtUser);存Authentication
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("token");
        //若是登录的请求,就放行。
        if (request.getRequestURI().equals("/login")) {
            //登录url
            chain.doFilter(request, response);
            return;
        }
        if(request.getRequestURI().equals("/")) {
            return;
        }
        if (StringUtil.isEmpty(token)) {
            //没有token直接放行。 在securityConfig配置了认证,故 SecurityContextHolder.getContext().setAuthentication();没设置值会抛出认证异常
            chain.doFilter(request, response);
            return;
        }

        //下面的方法。根据token获取当前登录用户信息
        JwtUser jwtUser = buildAuthentication(token);
        //这里存储后的jwUser包含了用户的所有权限信息。 ***********即登陆后访问其他资源
        SecurityContextHolder.getContext().setAuthentication(jwtUser);
        chain.doFilter(request, response);
    }

    /**
     * 解析token 利用jwt解密并且本地缓存中存在才算具有凭证
     * <p>
     * 抛出异常或者返回空最终都会走到 {@link JwtAuthenticationEntryPoint#commence(
     *javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
     * org.springframework.security.core.AuthenticationException)}方法
     *
     * @param token
     * @return
     */
    private JwtUser buildAuthentication(String token) {
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new BadCredentialsException("token无效!");
        }
        //缓存中取出用户信息
        JwtUser jwtUser = cache.getIfPresent(username);
        if (jwtUser == null || !jwtUser.getVersion().equals(JwtUtil.getVersion(token))) {
            throw new BadCredentialsException("token无效!");
        }
        if (!jwtUser.getEnable()) {
            throw new DisabledException("该账号已被禁用!");
        }
        return jwtUser;
    }
}

2、先根据上面的拦截。/login 若为登录请求的话。创建LoginFilter
package com.top.system.security.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.top.system.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.RequestUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 薛向毅
 * @description 自定义登录拦截。重写了方法,只能使用json字符串方式登录。后面会添加进security拦截器链
 */
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        if ("application/json".equalsIgnoreCase(request.getHeader("Content-Type"))) {
            String json = HttpUtils.parseJsonContent(request);
            if (StringUtils.isNotBlank(json)) {
                JSONObject jsonObj = JSON.parseObject(json);
                if (jsonObj == null) {
                    return null;
                }
                String username = jsonObj.getString(super.getUsernameParameter());
                String password = jsonObj.getString(super.getPasswordParameter());
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(username, password);
                       //这里是个重点 会调用适合的privoder里面的authenticate()方法。
                return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
            } else {
                return null;
            }
        } else {
            return null;
        }
    }


    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        if (request.getRequestURI().equals("/login")) {
            return true;
        }
        return false;
    }
}

3、根据security的介绍。他会调用合适的provider执行authenticate来验证账号密码

package com.top.system.security.provider;


import com.qinwell.common.util.StringUtil;
import com.top.system.security.handler.AuthenticationSuccessHandler;
import com.top.system.security.model.AdminDetails;
import com.top.system.service.AdminRoleService;
import com.top.system.security.model.JwtUser;
import com.top.system.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * @author 薛向毅
 * @description 登录业务逻辑
 */
@Service
public class LoginProvider implements AuthenticationProvider {

    @Autowired
    private JwtUserDetailsService jwtUserDetailsService;

    @Autowired
    private AdminRoleService adminRoleService;

    @Autowired
    private PasswordEncoder bCryptPasswordEncoder;

    /**
     * 登录验证
     * 成功后返回值转发到{@link AuthenticationSuccessHandler#onAuthenticationSuccess
     * (javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
     * org.springframework.security.core.Authentication)}
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        if (StringUtil.isEmpty(username)) {
            throw new BadCredentialsException("用户名不能为空!");
        }
        //自定义账号详情 AdminDetails 。      jwtUserDetailsService是定义实现了UserDetailsService的
        AdminDetails adminDetails = (AdminDetails) jwtUserDetailsService.loadUserByUsername(username);
        if (!bCryptPasswordEncoder.matches(authentication.getCredentials().toString(), adminDetails.getPassword())) {
            throw new BadCredentialsException("密码错误!");
        }
        if (!adminDetails.getEnable()) {
            throw new DisabledException("该账户已被禁用!");
        }
	
			//这里会返回登录成功的用户信息
        return new JwtUser(adminDetails.getUsername(), adminDetails.getUsername(),
                adminDetails.getAuthorities(), adminDetails.getAdmin(), adminDetails.getPermissionVos(), adminDetails.getEnable());
    }

	//**这个方法就代表了 合适!!!! 自定义让他返回true。故会调用这个类进行登录验证
    @Override
    public boolean supports(Class<?> paramClass) {
        if (paramClass.isAssignableFrom(UsernamePasswordAuthenticationToken.class)) {
            return true;
        }
        return false;
    }

}

4、继承UserDetailsService重写loadUserByUsername方法

/**
 */
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class JwtUserDetailsService implements UserDetailsService {

    @Autowired
    private AdminMapper adminMapper;

    @Autowired
    private AdminRoleMapper adminRoleMapper;

    @Autowired
    private RolePermissionMapper rolePermissionMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    @Autowired
    private Cache cache;

    /**
     * 根据用户名获取用户信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);

        //查询账户基本信息
        Admin admin = adminMapper.selectOne(queryWrapper);
        if (admin == null) {
            throw new UsernameNotFoundException("未找到该用户!");
        }

     	//查权限啊 查角色啊 你想查什么查什么,放在自定义的adminDetails  然后return出去
     	
        AdminDetails adminDetails = new AdminDetails(admin.getUsername(), admin.getPassword(), authorities, enable);
        //存放菜单
        adminDetails.setPermissionVos(permissionVos);
        adminDetails.setAdmin(admin);
        adminDetails.setRoles(roles);
        return adminDetails;
    }

    /**
     * 清除指定用户缓存
     * @param username
     */
    public void clearCache(String username) {
        cache.invalidate(username);
    }

}

5、贴上UserDetail类


/**
 * User是Security里面的User 他实现了UserDetails 可以存储账号,密码 ,权限信息(字符串一般情况)
 */
@Data
public class AdminDetails extends User {

    private Admin admin;

    private List<PermissionVo> permissionVos;
    private List<Integer> roles;
    private Boolean enable;

    private AdminDetails() {
        super(null,null,null);
    }


    public AdminDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Boolean enable) {
        super(username, password, authorities);
        this.enable = enable;
    }


}

6、根据security特性,登陆成功后会调用AuthenticationSuccessHandler。我们重写他


/**
  SavedRequestAwareAuthenticationSuccessHandler 最终是实现了这个接口AuthenticationSuccessHandler
 */
@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private Cache cache;


    /**
     * 登录成功生成jwt,并将认证信息放入缓存,具有可控性
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override  //一旦登录成功,调用。 讲token返回给前台页面。(无论谁登录都要携带token ,在header中。验证)
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //日期当作版本
        String version = new Date().toString();

        //生成jwt  这里密码也用用户名填写,避免不安全。这个token是为了验证登录
        String token = JwtUtil.sign(authentication.getPrincipal().toString(),
                version, authentication.getPrincipal().toString());

        //token中的version和缓存中的version一致才代表缓存有效
        JwtUser jwtUser = (JwtUser) authentication;
        jwtUser.setVersion(version);

        //放入缓存  key用户名,  用户信息
        cache.put(authentication.getPrincipal().toString(), jwtUser);
		//这个方法就是response的write 把token返回前端 
        ResponseUtil.out(response, JSON.toJSONString(R.ok(token)));

    }
}

7到目前回执,jwt登录已经完成。登录验证也完成。 下面进行方法过滤

url拦截在别的博客也写过,如果进行简单的方法过滤,可以使用自定义PermissionEvaluator

@Service
public class MyPermissionEvaluator implements PermissionEvaluator {

	//方法上面使用@PreAuthorize(" hasPermission('or', 'role_list')")    即可拦截
    @Override 
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        return hasPermission(authentication, null, (String) targetDomainObject, permission);
    }
	
	//第一个参数登陆后生成,上下文用户信息。  第三个 即传递的or  第四个传递的过滤权限信息
	//这里 取出 用户拥有的权限编码(因为如果用角色的话他的权限可改变,权限编码一个编码一个功能点,不可变)
	// 对比 用户是否拥有编码信息  有就放行,没有就给出权限不足
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        if ("OR".equalsIgnoreCase(targetType)) {
            String[] split = permission.toString().split(",");
            for (String s : split) {
                for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    if (s.equals(grantedAuthority.getAuthority())) {
                        return true;
                    }
                }
            }
            return false;
        } else {
            String[] split = permission.toString().split(",");
            for (String s : split) {
                boolean isMatch = false;
                for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    if (s.equals(grantedAuthority.getAuthority())) {
                        isMatch = true;
                        break;
                    }
                }
                if (!isMatch) {
                    return false;
                }
            }
            return true;
        }
    }


}

8、开启MyPermissionEvaluator

@EnableGlobalMethodSecurity(prePostEnabled = true)  //prePostEnabled 代表可以使用@PreAuthorize等注解,可以去官方文档查看
public class WebMvcConfigurationSupport extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler();
    }

    @Bean
    public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler();
        defaultMethodSecurityExpressionHandler.setPermissionEvaluator(myPermissionEvaluator());
        return defaultMethodSecurityExpressionHandler;
    }

    /**
     * 使用hasPermission自定义验证
     */
    @Bean
    public MyPermissionEvaluator myPermissionEvaluator() {
        MyPermissionEvaluator myPermissionEvaluator = new MyPermissionEvaluator();
        return myPermissionEvaluator;
    }


}

9重要的security配置类。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;


    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //auth.authenticationProvider(myAuthenticationProvider);
    }

    @Bean
    public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter() throws Exception {
        JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter = new JwtAuthorizationTokenFilter(authenticationManager());
        return jwtAuthorizationTokenFilter;
    }

    @Bean
    public LoginFilter loginFilter(AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler) throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setAuthenticationManager(authenticationManager());
        loginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        loginFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        return loginFilter;
    }


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoderBean() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public Cache cache() {
        return CacheBuilder.newBuilder().expireAfterAccess(3, TimeUnit.DAYS).build();
    }


    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 禁用 CSRF
                .csrf().disable()
                // 授权异常
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).
                accessDeniedHandler(customAccessDeniedHandler).and()
                // 不创建会话
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                .antMatchers(
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                        //"/error"
                ).permitAll()
                //.antMatchers(HttpMethod.POST, "/auth/" + loginPath).permitAll()
                .antMatchers("/websocket/**").permitAll()
                // swagger start
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                // swagger end
                .antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
                // 所有请求都需要认证
                .anyRequest().authenticated();


        httpSecurity.formLogin()
                .loginProcessingUrl("/login")
                .permitAll();

        LoginFilter loginFilter = loginFilter(null, null);
        httpSecurity.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(loginFilter);
    }

    //@Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }


    /**
     * 跨域
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

10 为什么叫单点登录、。

	使用springcloud分布式开发,将这个项目使用Feign远程调用。 每次请求去缓存里面看客户端携带的token是否正确即可

有问题直接留言,看到就会解答。比较懒 不喜欢扣字。所以代码多

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

SpringCloud1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值