IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringSecurity源码分析 -> 正文阅读

[Java知识库]SpringSecurity源码分析

1、过滤器链加载源码

1.1、过滤器链加载流程分析

在第二部分的时候讲解的时候说springSecurity中主要功能是由过滤器链来完成的, 那么spring boot是如何加载这个流程的呢?

1.2、过滤器链加载流程源码分析?

1. spring boot启动中会加载spring.factories文件, 在文件中有对应针对Spring Security的过滤器链的配置信息

# 安全过滤器自动配置
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration

2. SecurityFilterAutoConfiguration类

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)// Security配置类
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class,
SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)// 这个类加载完后去加载
SecurityAutoConfiguration配置
public class SecurityFilterAutoConfiguration {
.....
}

3. SecurityAutoConfiguration类

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class,
WebSecurityEnablerConfiguration.class,//web安全启用配置
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
.....
}

4. WebSecurityEnablerConfiguration类

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
        SpringWebMvcImportSelector.class,
        OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    /**
     * Controls debugging support for Spring Security. Default is false.
     * @return if true, enables debug support with Spring Security
     */
    boolean debug() default false;
}

@EnableWebSecurity注解有两个作用,1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略。2: 加载了AuthenticationConfiguration, 配置了认证信息。

5. WebSecurityConfiguration类

// springSecurity过滤器链声明
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain()throws Exception{
        boolean hasConfigurers=webSecurityConfigurers!=null &&!webSecurityConfigurers.isEmpty();
        if(!hasConfigurers){
        WebSecurityConfigurerAdapter adapter=objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter(){
        });
        webSecurity.apply(adapter);
        }
        return webSecurity.build();// 构建filter
        }

2、认证流程源码

2.1、认证流程分析

在整个过滤器链中, UsernamePasswordAuthenticationFilter是来处理整个用户认证的流程的, 所以下面我们主要针对用户认证来看下源码是如何实现的?

2.2、认证流程源码跟踪?

UsernamePasswordAuthenticationFilter

public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response)throws AuthenticationException{
// 1.检查是否是post请求
        if(postOnly&&!request.getMethod().equals("POST")){
        throw new AuthenticationServiceException(
        "Authentication method not supported: "+
        request.getMethod());
        }
// 2.获取用户名和密码
        String username=obtainUsername(request);
        String password=obtainPassword(request);
        if(username==null){
        username="";
        }
        if(password==null){
        password="";
        }
        username=username.trim();
// 3.创建AuthenticationToken,此时是未认证的状态
        UsernamePasswordAuthenticationToken authRequest=new UsernamePasswordAuthenticationToken(username,password);
// Allow subclasses to set the "details" property
        setDetails(request,authRequest);
// 4.调用AuthenticationManager进行认证.
        return this.getAuthenticationManager().authenticate(authRequest);
        }

UsernamePasswordAuthenticationToken

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;//设置用户名
        this.credentials = credentials;//设置密码
        setAuthenticated(false);//设置认证状态为-未认证
        }

AuthenticationManager-->ProviderManager-->AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException{
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class,
        authentication,
        ()->messages.getMessage(
        "AbstractUserDetailsAuthenticationProvider.onlySupports",
        "Only UsernamePasswordAuthenticationToken is supported"));
// 1.获取用户名
        String username=(authentication.getPrincipal()==null)?
        "NONE_PROVIDED"
        :authentication.getName();
// 2.尝试从缓存中获取
        boolean cacheWasUsed=true;
        UserDetails user=this.userCache.getUserFromCache(username);
        if(user==null){
        cacheWasUsed=false;
        try{
// 3.检索User
        user=retrieveUser(username,
        (UsernamePasswordAuthenticationToken)  authentication);
        }
        .....
        }
        try{
// 4. 认证前检查user状态
        preAuthenticationChecks.check(user);
// 5. 附加认证证检查
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }
        .....
// 6. 认证后检查user状态
        postAuthenticationChecks.check(user);
        .....
// 7. 创建认证成功的UsernamePasswordAuthenticationToken并将认证状态设置为true
        return createSuccessAuthentication(principalToReturn, authentication,user);
        }

retrieveUser方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
//调用自定义UserDetailsService的loadUserByUsername的方法
        UserDetails loadedUser =  this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
        throw new InternalAuthenticationServiceException(
        "UserDetailsService returned null, which is an interface contract violation");
        }
            return loadedUser;
      }
        ....
     }

additionalAuthenticationChecks方法

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
        .....
// 1.获取前端密码
        String presentedPassword = authentication.getCredentials().toString();
// 2.与数据库中的密码进行比对
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");
        throw new BadCredentialsException(messages.getMessage(
        "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }

AbstractAuthenticationProcessingFilter--doFilter方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain  chain)
        throws IOException, ServletException {
        .....
        Authentication authResult;
        try {
    //1.调用子类方法
        authResult = attemptAuthentication(request, response);
        ...
        //2.session策略验证
              sessionStrategy.onAuthentication(authResult, request, response);
             }
        ....
        // 3.成功身份验证
        successfulAuthentication(request, response, chain, authResult);
    }

successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
        ....
// 1.将认证的用户放入SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
// 2.检查是不是记住我
        rememberMeServices.loginSuccess(request, response, authResult);
        ...
// 3.调用自定义MyAuthenticationService的onAuthenticationSuccess方法
        successHandler.onAuthenticationSuccess(request, response,  authResult);
     }

3、记住我流程源码

在整个过滤器链中, RememberMeAuthenticationFilter是来处理记住我用户认证的流程的, 所以下面我们主要针对记住我看下源码是如何实现的?

3.1、记住我流程分析?

3.2、记住我流程源码跟踪?

AbstractAuthenticationProcessingFilter--successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
        ....
// 1.将认证的用户放入SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
// 2.检查是不是记住我
        rememberMeServices.loginSuccess(request, response, authResult);
        ...
// 3.调用自定义MyAuthenticationService的onAuthenticationSuccess方法
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

loginSuccess方法-->onLoginSuccess

protected void onLoginSuccess(HttpServletRequest request,
        HttpServletResponse response, Authentication successfulAuthentication) {
  // 1.获取用户名
        String username = successfulAuthentication.getName();
  // 2.创建persistentToken
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
        username, generateSeriesData(), generateTokenData(), new Date());
        try {
  // 3. 插入数据库
        tokenRepository.createNewToken(persistentToken);
 // 4. 写入浏览器cookie
        addCookie(persistentToken, request, response);
       }
        catch (Exception e) {
        logger.error("Failed to save persistent token ", e);
     }
  }

RememberMeAuthenticationFilter

public void doFilter(ServletRequest req,ServletResponse res,FilterChain chain)
        throws IOException,ServletException{
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if(SecurityContextHolder.getContext().getAuthentication()==null)
        {
    // 1. 检查是否是记住我登录. 如果是完成自动登录
        Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
        if(rememberMeAuth!=null){
            try{
    // 2.调用authenticationManager再次认证
        rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
    // 3.将认证的用户在重新放入SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
        ......
      }
        ......
    }
        .....
// 4.调用下一个过滤器
        chain.doFilter(request,response);
   }
}

autoLogin方法

public final Authentication autoLogin(HttpServletRequest request,
        HttpServletResponse response) {
    // 1.获取rememberMeCookie
        String rememberMeCookie = extractRememberMeCookie(request);
    // 2.检查是否存在
        if (rememberMeCookie == null) {
              return null;
        }
        .....
        UserDetails user = null;
        try {
    // 3.解码Cookie
        String[] cookieTokens = decodeCookie(rememberMeCookie);
    // 4.根据cookie完成自动登录
        user = processAutoLoginCookie(cookieTokens, request, response);
    // 5.检查user状态
        userDetailsChecker.check(user);
        logger.debug("Remember-me cookie accepted");
    // 6.创建认证成功的RememberMeAuthenticationToken并将认证状态设置为true
             return createSuccessfulAuthentication(request, user);
        }
        .....
        return null;
  }

processAutoLoginCookie方法

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
        HttpServletRequest request, HttpServletResponse response) {
        ....
// 1.获取系列码和token
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
    // 2.根据token去数据库中查询
        PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
        .......
  // 3.在创建一个新的token
        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
        token.getUsername(), token.getSeries(), generateTokenData(),
        new Date());
        try {
// 4.修改数据库token信息
              tokenRepository.updateToken(newToken.getSeries(),
              newToken.getTokenValue(),
              newToken.getDate());
// 5.写入浏览器
        addCookie(newToken, request, response);
      }
// 6.根据用户名调用UserDetailsService查询UserDetail
        return
getUserDetailsService().loadUserByUsername(token.getUsername());

4、csrf流程源码

在整个过滤器链中, CsrfFilter是起到csrf防护的, 所以下面我们主要针对记住我看下源码是如何实现的?

4.1、csrf流程分析

4.2、csrf流程源码跟踪?

CsrfFilter

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1.取出token
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            // 2. 如果没有token,就重新生成token
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        // 3. 将csrfToken值放入request域中
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        // 4. 匹配请求是否为post请求,不是则放行
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
            }

            filterChain.doFilter(request, response);
        } else {
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                // 5.从request请求参数中取出csrfToken
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
            // 6.匹配两个token是否相等
            if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
                this.logger.debug(LogMessage.of(() -> {
                    return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request);
                }));
                AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);
                this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
            } else {
                // // 7. 如果相等则放行
                filterChain.doFilter(request, response);
            }
        }
    }

5、授权流程源码

在整个过滤器链中, FilterSecurityInterceptor是来处理整个用户授权流程的, 也是距离用户API最后一个非常重要的过滤器链,所以下面我们主要针对用户授权来看下源码是如何实现的?

5.1、授权流程分析

AffirmativeBased(基于肯定)的逻辑是: 一票通过权

ConsensusBased(基于共识)的逻辑是: 赞成票多于反对票则表示通过,反对票多于赞成票则将抛出AccessDeniedException

UnanimousBased(基于一致)的逻辑: 一票否决权?

5.2、授权流程源码跟踪

FilterSecurityInterceptor

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.access.intercept;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    private boolean observeOncePerRequest = true;

    public FilterSecurityInterceptor() {
    }

    public void init(FilterConfig arg0) {
    }

    public void destroy() {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        // 调用
        this.invoke(fi);
    }

    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
        this.securityMetadataSource = newSource;
    }

    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } else {
            if (fi.getRequest() != null && this.observeOncePerRequest) {
                fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }
            //前置调用
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.finallyInvocation(token);
            }
            // 后置调用
            super.afterInvocation(token, (Object)null);
        }

    }

    public boolean isObserveOncePerRequest() {
        return this.observeOncePerRequest;
    }

    public void setObserveOncePerRequest(boolean observeOncePerRequest) {
        this.observeOncePerRequest = observeOncePerRequest;
    }
}

AbstractSecurityInterceptor的beforeInvocation方法?

 protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        boolean debug = this.logger.isDebugEnabled();
        if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
        } else {
            // 1. 获取security的系统配置权限
            Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
            if (attributes != null && !attributes.isEmpty()) {
                if (debug) {
                    this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
                }

                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
                }
                // 2. 获取用户认证的信息
                Authentication authenticated = this.authenticateIfRequired();
                // Attempt authorization
                try {
                    // 3.调用决策管理器
                    this.accessDecisionManager.decide(authenticated, object, attributes);
                } catch (AccessDeniedException var7) {
                    this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
                    // 4.无权限则抛出异常让ExceptionTranslationFilter捕获
                    throw var7;
                }
        ........
}

AffirmativeBased的decide方法

public class AffirmativeBased extends AbstractAccessDecisionManager {
    public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
        super(decisionVoters);
    }

    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        Iterator var5 = this.getDecisionVoters().iterator();

        while(var5.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            int result = voter.vote(authentication, object, configAttributes);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Voter: " + voter + ", returned: " + result);
            }
            //一票通过,只要有一个投票器通过就允许访问
            switch (result) {
                case AccessDecisionVoter.ACCESS_DENIED:
                    ++deny;
                    break;
                case AccessDecisionVoter.ACCESS_GRANTED:
                    return;
                default:
                    break;
            }
        }

        if (deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }
}

ExceptionTranslationFilter

 private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            // 1.调用下一个过滤器即FilterSecurityInterceptor
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            // 2.捕获FilterSecurityInterceptor并判断异常类型
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }

            if (securityException == null) {
                this.rethrow(var8);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
            }
            // 3.如果是AccessDeniedException异常则处理Spring Security异常
            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }

handleSpringSecurityException方法

    private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            this.handleAuthenticationException(request, response, chain, (AuthenticationException)exception);
        } else if (exception instanceof AccessDeniedException) {
            this.handleAccessDeniedException(request, response, chain, (AccessDeniedException)exception);
        }

    }


   private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
        if (!isAnonymous && !this.authenticationTrustResolver.isRememberMe(authentication)) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception);
            }
            //如果是AccessDeniedException异常则调用AccessDeniedHandler的handle方法
            this.accessDeniedHandler.handle(request, response, exception);
        } else {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception);
            }

            this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
        }

    }

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-07-21 21:22:54  更:2022-07-21 21:24:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:15:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码