SpringBoot security安全认证登录的实现方法

作者:陈鋆 时间:2021-05-30 08:09:56 

前言

本文章主要从spring security安全认证登录内部调用流程来流程分析登录过程。

一、登录时序图

SpringBoot security安全认证登录的实现方法

时序原图

二、配置与代码

1.引入库

pom.xml:

<!-- Spring框架基本的核心工具 -->
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-context-support</artifactId>
       </dependency>
       <!-- SpringWeb模块 -->
       <dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-web</artifactId>
       </dependency>
       <!-- spring security 安全认证 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
       </dependency>
       <!--常用工具类 -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
       </dependency>
       <!-- JSON工具类 -->
       <dependency>
           <groupId>com.fasterxml.jackson.core</groupId>
           <artifactId>jackson-databind</artifactId>
       </dependency>
       <!-- 阿里JSON解析器 -->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>fastjson</artifactId>
       </dependency>
       <!-- io常用工具类 -->
       <dependency>
           <groupId>commons-io</groupId>
           <artifactId>commons-io</artifactId>
       </dependency>
       <!-- yml解析器 -->
       <dependency>
           <groupId>org.yaml</groupId>
           <artifactId>snakeyaml</artifactId>
       </dependency>
       <!-- Token生成与解析-->
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
       </dependency>
       <!-- Jaxb -->
       <dependency>
           <groupId>javax.xml.bind</groupId>
           <artifactId>jaxb-api</artifactId>
       </dependency>
       <!-- redis 缓存操作 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>
       <!-- pool 对象池 -->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-pool2</artifactId>
       </dependency>
       <!-- 解析客户端操作系统、浏览器等 -->
       <dependency>
           <groupId>eu.bitwalker</groupId>
           <artifactId>UserAgentUtils</artifactId>
       </dependency>
       <!-- servlet包 -->
       <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>javax.servlet-api</artifactId>
       </dependency>
       <dependency>
           <groupId>org.apache.httpcomponents</groupId>
           <artifactId>httpclient</artifactId>
           <version>4.5.2</version>
       </dependency>

2.代码文件

代码如下(示例):

RedisCache.java :spring redis 工具组件类,注入RedisTemplate,封装Redis缓存数据、对象操作。

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
/**
* spring redis 工具类
*
*
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
   @Autowired
   public RedisTemplate redisTemplate;
   /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    */
   public <T> void setCacheObject(final String key, final T value)
   {
       redisTemplate.opsForValue().set(key, value);
   }
   /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    * @param timeout 时间
    * @param timeUnit 时间颗粒度
    */
   public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
   {
       redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
   }
   /**
    * 设置有效时间
    *
    * @param key Redis键
    * @param timeout 超时时间
    * @return true=设置成功;false=设置失败
    */
   public boolean expire(final String key, final long timeout)
   {
       return expire(key, timeout, TimeUnit.SECONDS);
   }
   /**
    * 设置有效时间
    *
    * @param key Redis键
    * @param timeout 超时时间
    * @param unit 时间单位
    * @return true=设置成功;false=设置失败
    */
   public boolean expire(final String key, final long timeout, final TimeUnit unit)
   {
       return redisTemplate.expire(key, timeout, unit);
   }
   /**
    * 获得缓存的基本对象。
    *
    * @param key 缓存键值
    * @return 缓存键值对应的数据
    */
   public <T> T getCacheObject(final String key)
   {
       ValueOperations<String, T> operation = redisTemplate.opsForValue();
       return operation.get(key);
   }
   /**
    * 删除单个对象
    *
    * @param key
    */
   public boolean deleteObject(final String key)
   {
       return redisTemplate.delete(key);
   }
   /**
    * 删除集合对象
    *
    * @param collection 多个对象
    * @return
    */
   public long deleteObject(final Collection collection)
   {
       return redisTemplate.delete(collection);
   }
   /**
    * 缓存List数据
    *
    * @param key 缓存的键值
    * @param dataList 待缓存的List数据
    * @return 缓存的对象
    */
   public <T> long setCacheList(final String key, final List<T> dataList)
   {
       Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
       return count == null ? 0 : count;
   }
   /**
    * 获得缓存的list对象
    *
    * @param key 缓存的键值
    * @return 缓存键值对应的数据
    */
   public <T> List<T> getCacheList(final String key)
   {
       return redisTemplate.opsForList().range(key, 0, -1);
   }
   /**
    * 缓存Set
    *
    * @param key 缓存键值
    * @param dataSet 缓存的数据
    * @return 缓存数据的对象
    */
   public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
   {
       BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
       Iterator<T> it = dataSet.iterator();
       while (it.hasNext())
       {
           setOperation.add(it.next());
       }
       return setOperation;
   }
   /**
    * 获得缓存的set
    *
    * @param key
    * @return
    */
   public <T> Set<T> getCacheSet(final String key)
   {
       return redisTemplate.opsForSet().members(key);
   }
   /**
    * 缓存Map
    *
    * @param key
    * @param dataMap
    */
   public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
   {
       if (dataMap != null) {
           redisTemplate.opsForHash().putAll(key, dataMap);
       }
   }
   /**
    * 获得缓存的Map
    *
    * @param key
    * @return
    */
   public <T> Map<String, T> getCacheMap(final String key)
   {
       return redisTemplate.opsForHash().entries(key);
   }
   /**
    * 往Hash中存入数据
    *
    * @param key Redis键
    * @param hKey Hash键
    * @param value 值
    */
   public <T> void setCacheMapValue(final String key, final String hKey, final T value)
   {
       redisTemplate.opsForHash().put(key, hKey, value);
   }
   /**
    * 获取Hash中的数据
    *
    * @param key Redis键
    * @param hKey Hash键
    * @return Hash中的对象
    */
   public <T> T getCacheMapValue(final String key, final String hKey)
   {
       HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
       return opsForHash.get(key, hKey);
   }
   /**
    * 删除Hash中的数据
    *
    * @param key
    * @param mapkey
    */
   public void delCacheMapValue(final String key, final String hkey)
   {
       HashOperations hashOperations = redisTemplate.opsForHash();
       hashOperations.delete(key, hkey);
   }
   /**
    * 获取多个Hash中的数据
    *
    * @param key Redis键
    * @param hKeys Hash键集合
    * @return Hash对象集合
    */
   public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
   {
       return redisTemplate.opsForHash().multiGet(key, hKeys);
   }
   /**
    * 获得缓存的基本对象列表
    *
    * @param pattern 字符串前缀
    * @return 对象列表
    */
   public Collection<String> keys(final String pattern)
   {
       return redisTemplate.keys(pattern);
   }
}

RedisConfig:redis配置类。

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
   @Bean
   @SuppressWarnings(value = { "unchecked", "rawtypes" })
   public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
   {
       RedisTemplate<Object, Object> template = new RedisTemplate<>();
       template.setConnectionFactory(connectionFactory);
       FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
       ObjectMapper mapper = new ObjectMapper();
       mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
       mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
       serializer.setObjectMapper(mapper);
       // 使用StringRedisSerializer来序列化和反序列化redis的key值
       template.setKeySerializer(new StringRedisSerializer());
       template.setValueSerializer(serializer);
       // Hash的key也采用StringRedisSerializer的序列化方式
       template.setHashKeySerializer(new StringRedisSerializer());
       template.setHashValueSerializer(serializer);
       template.afterPropertiesSet();
       return template;
   }
}

SecurityUtils:安全服务工具类。

import com.ems.mgr.common.constant.HttpStatus;
import com.ems.mgr.common.exception.ServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.ems.mgr.common.core.domain.model.LoginUser;
/**
* 安全服务工具类
*/
public class SecurityUtils
{
   /**
    * 用户ID
    **/
   public static Long getUserId()
   {
       try
       {
           return getLoginUser().getUserId();
       }
       catch (Exception e)
       {
           throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED);
       }
   }
   /**
    * 获取部门ID
    **/
   public static Long getDeptId()
   {
       try
       {
           return getLoginUser().getDeptId();
       }
       catch (Exception e)
       {
           throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED);
       }
   }
   /**
    * 获取用户账户
    **/
   public static String getUsername()
   {
       try
       {
           return getLoginUser().getUsername();
       }
       catch (Exception e)
       {
           throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED);
       }
   }
   /**
    * 获取用户
    **/
   public static LoginUser getLoginUser()
   {
       try
       {
           return (LoginUser) getAuthentication().getPrincipal();
       }
       catch (Exception e)
       {
           throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
       }
   }
   /**
    * 获取Authentication
    */
   public static Authentication getAuthentication()
   {
       return SecurityContextHolder.getContext().getAuthentication();
   }
   /**
    * 生成BCryptPasswordEncoder密码
    *
    * @param password 密码
    * @return 加密字符串
    */
   public static String encryptPassword(String password)
   {
       BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
       return passwordEncoder.encode(password);
   }
   /**
    * 判断密码是否相同
    *
    * @param rawPassword 真实密码
    * @param encodedPassword 加密后字符
    * @return 结果
    */
   public static boolean matchesPassword(String rawPassword, String encodedPassword)
   {
       BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
       return passwordEncoder.matches(rawPassword, encodedPassword);
   }
   /**
    * 是否为管理员
    *
    * @param userId 用户ID
    * @return 结果
    */
   public static boolean isAdmin(Long userId)
   {
       return userId != null && 1L == userId;
   }
}

SecurityConfig:spring security配置。

import com.ems.mgr.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ems.mgr.framework.security.handle.AuthenticationEntryPointImpl;
import com.ems.mgr.framework.security.handle.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
/**
* spring security配置
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
   /**
    * 自定义用户认证逻辑
    */
   @Autowired
   private UserDetailsService userDetailsService;
   /**
    * 认证失败处理类
    */
   @Autowired
   private AuthenticationEntryPointImpl unauthorizedHandler;
   /**
    * 退出处理类
    */
   @Autowired
   private LogoutSuccessHandlerImpl logoutSuccessHandler;
   /**
    * token认证过滤器
    */
   @Autowired
   private JwtAuthenticationTokenFilter authenticationTokenFilter;
   /**
    * 跨域过滤器
    */
   @Autowired
   private CorsFilter corsFilter;
   /**
    * 解决 无法直接注入 AuthenticationManager
    *
    * @return
    * @throws Exception
    */
   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception
   {
       return super.authenticationManagerBean();
   }
   /**
    * 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
   {
       httpSecurity
               // CSRF禁用,因为不使用session
               .csrf().disable()
               // 认证失败处理类
               .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
               // 基于token,所以不需要session
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
               // 过滤请求
               .authorizeRequests()
               // 对于登录login  验证码captchaImage 允许匿名访问
               .antMatchers("/ems/login", "/ems/captchaImage","/ems/login/sms","/ems/robot/**/**").anonymous()
               .antMatchers("/tool/gen/**/**").anonymous()
               .antMatchers("/message/sendOne").anonymous()
               .antMatchers(
                       HttpMethod.GET,
                       "/",
                       "/*.html",
                       "/**/*.html",
                       "/**/*.css",
                       "/**/*.js",
                       "/profile/**"
               ).permitAll()
               .antMatchers("/swagger-ui.html").anonymous()
               .antMatchers("/swagger-resources/**").anonymous()
               .antMatchers("/webjars/**").anonymous()
               .antMatchers("/*/api-docs").anonymous()
               .antMatchers("/druid/**").anonymous()
               .antMatchers("/merakWs/**").anonymous()
               //.antMatchers("/tool/**").anonymous()
               // 除上面外的所有请求全部需要鉴权认证
               .anyRequest().authenticated()
               .and()
               .headers().frameOptions().disable();
       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
   {
   //设置DaoAuthenticationProvider类PasswordEncoder passwordEncoder
   auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
   }
}

spring security配置类配置token认证过滤器(JwtAuthenticationTokenFilter)、

认证失败处理类(AuthenticationEntryPointImpl)、退出处理类(LogoutSuccessHandlerImpl)

1-JwtAuthenticationTokenFilter:

package com.ems.mgr.framework.security.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.ems.mgr.common.core.domain.model.LoginUser;
import com.ems.mgr.common.utils.SecurityUtils;
import com.ems.mgr.common.utils.StringUtils;
import com.ems.mgr.framework.web.service.TokenService;
/**
* token过滤器 验证token有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;
   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
           throws ServletException, IOException
   {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
       {
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
       }
       chain.doFilter(request, response);
   }
}

2-AuthenticationEntryPointImpl:认证失败处理类 返回未授权

package com.ems.mgr.framework.security.handle;
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ems.mgr.common.core.domain.AjaxResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.ems.mgr.common.constant.HttpStatus;
import com.ems.mgr.common.utils.ServletUtils;
import com.ems.mgr.common.utils.StringUtils;
/**
* 认证失败处理类 返回未授权
*
*
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
   private static final long serialVersionUID = -8970718410437077606L;
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
           throws IOException
   {
       int code = HttpStatus.UNAUTHORIZED;
       String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
   }
}

3-LogoutSuccessHandlerImpl:自定义退出处理类

package com.ems.mgr.framework.security.handle;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ems.mgr.common.constant.Constants;
import com.ems.mgr.common.core.domain.AjaxResult;
import com.ems.mgr.framework.manager.factory.AsyncFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import com.alibaba.fastjson.JSON;
import com.ems.mgr.common.constant.HttpStatus;
import com.ems.mgr.common.core.domain.model.LoginUser;
import com.ems.mgr.common.utils.ServletUtils;
import com.ems.mgr.common.utils.StringUtils;
import com.ems.mgr.framework.manager.AsyncManager;
import com.ems.mgr.framework.web.service.TokenService;
/**
* 自定义退出处理类 返回成功
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
   @Autowired
   private TokenService tokenService;
   /**
    * 退出处理
    *
    * @return
    */
   @Override
   public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
           throws IOException, ServletException
   {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser))
       {
           String userName = loginUser.getUsername();
           // 删除用户缓存记录
           tokenService.delLoginUser(loginUser.getToken());
           // 记录用户退出日志
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
       }
       ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
   }
}

参考文档

Spring Security 入门原理及实战

来源:https://blog.csdn.net/jun55xiu/article/details/127984500

标签:SpringBoot,security,安全,认证,登录
0
投稿

猜你喜欢

  • Quarkus的Spring扩展快速改造Spring项目

    2023-11-26 03:01:58
  • Android开发之android_gps定位服务简单实现

    2023-07-31 20:02:25
  • Springboot之idea之pom文件图标不对问题

    2021-12-31 07:27:32
  • java模拟实现双向链表

    2022-06-05 09:16:16
  • Android基础教程数据存储之文件存储

    2023-08-05 18:18:10
  • Java多线程-线程的同步与锁的问题

    2023-11-29 01:40:12
  • 微信小程序微信登录的实现方法详解(JAVA后台)

    2023-08-29 12:37:25
  • Java项目中获取路径的绝对路径问题和相对路径问题

    2023-07-09 13:25:01
  • java异常处理执行顺序详解try catch finally

    2022-10-01 04:10:10
  • Java集合之Map接口的实现类精解

    2023-10-07 15:10:37
  • Spring深入刨析声明式事务注解的源码

    2023-10-23 09:41:48
  • 详解spring cloud eureka注册中心

    2023-11-10 17:54:10
  • JavaMail实现带附件的邮件发送

    2021-10-21 15:00:09
  • springboot实现定时任务的四种方式小结

    2021-10-20 20:38:06
  • Java生成压缩文件的实例代码

    2023-02-04 21:11:09
  • springboot自动扫描添加的BeanDefinition源码实例详解

    2023-11-24 15:15:22
  • 深入了解Spring中最常用的11个扩展点

    2023-07-05 17:46:40
  • SpringCloud Feign转发请求头(防止session失效)的解决方案

    2022-08-29 12:25:59
  • C语言程序设计50例(经典收藏)

    2023-07-10 08:33:19
  • spring boot微服务场景下apollo加载过程解析

    2022-05-20 13:55:32
  • asp之家 软件编程 m.aspxhome.com