Java中关于OAuth2.0的原理分析

 更新时间:2023年09月18日 09:08:05   作者:还没秃的小菜鸡  
这篇文章主要介绍了Java中关于OAuth2.0的原理分析,OAuth是一个关于授权的开放网络标准,允许用户授权第三 方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,需要的朋友可以参考下

授权服务器

@EnableAuthorizationServer解析

我们都知道 一个授权认证服务器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要做了什么呢?

我们看下 @EnableAuthorizationServer 源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}

我们可以看到其源码内部导入了 AuthorizationServerEndpointsConfigurationAuthorizationServerSecurityConfiguration 这2个配置类。 接下来我们分别看下这2个配置类具体做了什么。

AuthorizationServerEndpointsConfiguration

@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {
    // 省略 其他相关配置代码
  ....
    // 1、 AuthorizationEndpoint 创建
    @Bean
    public AuthorizationEndpoint authorizationEndpoint() throws Exception {
        AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
        FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
        authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
        authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
        authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
        authorizationEndpoint.setTokenGranter(tokenGranter());
        authorizationEndpoint.setClientDetailsService(clientDetailsService);
        authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
        authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
        authorizationEndpoint.setRedirectResolver(redirectResolver());
        return authorizationEndpoint;
    }
    // 2、 TokenEndpoint 创建
    @Bean
    public TokenEndpoint tokenEndpoint() throws Exception {
        TokenEndpoint tokenEndpoint = new TokenEndpoint();
        tokenEndpoint.setClientDetailsService(clientDetailsService);
        tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
        tokenEndpoint.setTokenGranter(tokenGranter());
        tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
        return tokenEndpoint;
    }
    // 省略 其他相关配置代码
   ....

通过源码我们可以很明确的知道:

  • AuthorizationEndpoint 用于服务授权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

AuthorizationServerSecurityConfiguration

  • ClientDetailsService : 内部仅有 loadClientByClientId 方法。从方法名我们就可知其是通过 clientId 来获取 Client 信息, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2个实现类,我们也可以像UserDetailsService 一样编写自己的实现类。
  • UserDetailsService : 内部仅有 loadUserByUsername 方法。这个类不用我再介绍了吧。不清楚得同学可以看下我之前得文章。
  • ClientDetailsUserDetailsService : UserDetailsService子类,内部维护了 ClientDetailsService 。其 loadUserByUsername 方法重写后调用ClientDetailsService.loadClientByClientId()。
  • ClientCredentialsTokenEndpointFilter** 作用与 UserNamePasswordAuthenticationFilter 类似,通过拦截 /oauth/token 地址,获取到 clientId 和 clientSecret 信息并创建 UsernamePasswordAuthenticationToken 作为 AuthenticationManager.authenticate() 参数 调用认证过程。整个认证过程唯一最大得区别在于 DaoAuthenticationProvider.retrieveUser() 获取认证用户信息时调用的是 ClientDetailsUserDetailsService,根据前面讲述的其内部其实是调用ClientDetailsService 获取到客户端信息

@EnableResourceServer

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}

从源码中我们可以看到其导入了 ResourceServerConfiguration 配置类,这个配置类最核心的配置是 应用了 ResourceServerSecurityConfigurer ,我这边贴出 ResourceServerSecurityConfigurer 源码 最核心的配置代码如下:

public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
    this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
    this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
    this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
    if (this.eventPublisher != null) {
        this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
    }
    if (this.tokenExtractor != null) {
        this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
    }
    this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
    this.resourcesServerFilter.setStateless(this.stateless);
    ((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}
private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
    OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
    if (this.authenticationManager != null) {
        if (!(this.authenticationManager instanceof OAuth2AuthenticationManager)) {
            return this.authenticationManager;
        }
        oauthAuthenticationManager = (OAuth2AuthenticationManager)this.authenticationManager;
    }
    oauthAuthenticationManager.setResourceId(this.resourceId);
    oauthAuthenticationManager.setTokenServices(this.resourceTokenServices(http));
    oauthAuthenticationManager.setClientDetailsService(this.clientDetails());
    return oauthAuthenticationManager;
}

源码中最核心的 就是 官方文档中介绍的 OAuth2AuthenticationProcessingFilter 过滤器, 其配置分3步:

1、 创建 OAuth2AuthenticationProcessingFilter 过滤器 对象

2、 创建 OAuth2AuthenticationManager 对象 对将其作为参数设置到 OAuth2AuthenticationProcessingFilter 中

3、 将 OAuth2AuthenticationProcessingFilter 过滤器添加到过滤器链上

AuthorizationEndpoint生成授权码

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
                              SessionStatus sessionStatus, Principal principal) {
    //  1、 通过 OAuth2RequestFactory 从 参数中获取信息创建 AuthorizationRequest 授权请求对象
    AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
    Set<String> responseTypes = authorizationRequest.getResponseTypes();
    if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
        throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
    }
    if (authorizationRequest.getClientId() == null) {
        throw new InvalidClientException("A client id must be provided");
    }
    try {
        // 2、 判断  principal 是否 已授权 : /oauth/authorize 设置为无权限访问 ,所以要判断,如果 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登录页面,这也是为什么我们第一次去请求获取 授权码时会跳转到登陆界面的原因
        if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
            throw new InsufficientAuthenticationException(
                    "User must be authenticated with Spring Security before authorization can be completed.");
        }
        // 3、 通过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息
        ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
        // 4、 获取参数中的回调地址并且与系统配置的回调地址对比
        String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
        String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
        if (!StringUtils.hasText(resolvedRedirect)) {
            throw new RedirectMismatchException(
                    "A redirectUri must be either supplied or preconfigured in the ClientDetails");
        }
        authorizationRequest.setRedirectUri(resolvedRedirect);
        //  5、 验证 scope 
        oauth2RequestValidator.validateScope(authorizationRequest, client);
        //  6、 检测该客户端是否设置自动 授权(即 我们配置客户端时配置的 autoApprove(true)  )
        authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                (Authentication) principal);
        boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
        authorizationRequest.setApproved(approved);
        if (authorizationRequest.isApproved()) {
            if (responseTypes.contains("token")) {
                return getImplicitGrantResponse(authorizationRequest);
            }
            if (responseTypes.contains("code")) {
                // 7 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址
                return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                        (Authentication) principal));
            }
        }
        model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
        model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
        return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
    }
    catch (RuntimeException e) {
        sessionStatus.setComplete();
        throw e;
    }
}

1、 通过 OAuth2RequestFactory 从 参数中获取信息创建 AuthorizationRequest 授权请求对象

2、 判断 principal 是否 已授权 : /oauth/authorize 设置为无权限访问 ,所以要判断,如果 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登录页面,这也是为什么我们第一次去请求获取 授权码时会跳转到登陆界面的原因

3、 通过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息

4、 获取参数中的回调地址并且与系统配置的回调地址(步骤3获取到的client信息)对比

5、 与步骤4一样 验证 scope

6、 检测该客户端是否设置自动 授权(即 我们配置客户端时配置的 autoApprove(true))

7、 由于我们设置 autoApprove(true) 则 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址

8、 真实生成Code 的方法时 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其内部是authorizationCodeServices.createAuthorizationCode()方法生成code的

TokenEndpoint 生成token

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
        Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
    // 1、 验证 用户信息 (正常情况下会经过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
    if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
                "There is no client authentication. Try adding an appropriate authentication filter.");
    }
    // 2、 通过 ClientDetailsService().loadClientByClientId() 获取系统配置客户端信息
    String clientId = getClientId(principal);
    ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
    // 3、 通过客户端信息生成 TokenRequest 对象
    TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
 ......
    // 4、 调用 TokenGranter.grant()方法生成 OAuth2AccessToken 对象(即token)
    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }
    // 5、 返回token
    return getResponse(token);
}

1、 验证 用户信息 (正常情况下会经过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )

2、 通过 ClientDetailsService().loadClientByClientId() 获取系统配置的客户端信息

3、 通过客户端信息生成 TokenRequest 对象

4、 将步骤3获取到的 TokenRequest 作为TokenGranter.grant() 方法参照 生成 OAuth2AccessToken 对象(即token)

5、 返回 token

TokenGranter

TokenGranter的设计思路是使用CompositeTokenGranter管理一个List列表,每一种grantType对应一个具体的真正授权者,CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的 grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。

五种类型分别是:

  • ResourceOwnerPasswordTokenGranter ==> password密码模式
  • AuthorizationCodeTokenGranter ==> authorization_code授权码模式
  • ClientCredentialsTokenGranter ==> client_credentials客户端模式
  • ImplicitTokenGranter ==> implicit简化模式
  • RefreshTokenGranter ==>refresh_token 刷新token专用

OAuth2AccessToken

@JsonSerialize(
    using = OAuth2AccessTokenJackson1Serializer.class
)
@JsonDeserialize(
    using = OAuth2AccessTokenJackson1Deserializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(
    using = OAuth2AccessTokenJackson2Serializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
    using = OAuth2AccessTokenJackson2Deserializer.class
)
public interface OAuth2AccessToken {
    String BEARER_TYPE = "Bearer";
    String OAUTH2_TYPE = "OAuth2";
    String ACCESS_TOKEN = "access_token";
    String TOKEN_TYPE = "token_type";
    String EXPIRES_IN = "expires_in";
    String REFRESH_TOKEN = "refresh_token";
    String SCOPE = "scope";
}

AuthorizationServerTokenServices

public interface AuthorizationServerTokenServices {
    //创建
    OAuth2AccessToken createAccessToken(OAuth2Authentication var1) throws AuthenticationException;
	//刷新
    OAuth2AccessToken refreshAccessToken(String var1, TokenRequest var2) throws AuthenticationException;
	//获取
    OAuth2AccessToken getAccessToken(OAuth2Authentication var1);
}

流程

在这里插入图片描述

资源服务器

@EnableResourceServer

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}

ResourceServerConfiguration

protected void configure(HttpSecurity http) throws Exception {
    ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
    ResourceServerTokenServices services = this.resolveTokenServices();
    if (services != null) {
        resources.tokenServices(services);
    } else if (this.tokenStore != null) {
        resources.tokenStore(this.tokenStore);
    } else if (this.endpoints != null) {
        resources.tokenStore(this.endpoints.getEndpointsConfigurer().getTokenStore());
    }
    if (this.eventPublisher != null) {
        resources.eventPublisher(this.eventPublisher);
    }
    Iterator var4 = this.configurers.iterator();
    ResourceServerConfigurer configurer;
    while(var4.hasNext()) {
        configurer = (ResourceServerConfigurer)var4.next();
        configurer.configure(resources);
    }
    ((HttpSecurity)((HttpSecurity)http.authenticationProvider(new AnonymousAuthenticationProvider("default")).exceptionHandling().accessDeniedHandler(resources.getAccessDeniedHandler()).and()).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()).csrf().disable();
    http.apply(resources);
    if (this.endpoints != null) {
        http.requestMatcher(new ResourceServerConfiguration.NotOAuthRequestMatcher(this.endpoints.oauth2EndpointHandlerMapping()));
    }
    var4 = this.configurers.iterator();
    while(var4.hasNext()) {
        configurer = (ResourceServerConfigurer)var4.next();
        configurer.configure(http);
    }
    if (this.configurers.isEmpty()) {
        ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
    }
}

ResourceServerSecurityConfigurer

public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
    //创建OAuth2核心过滤器
    this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
    this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
    //设置OAuth2的身份认证处理器,没有交给spring管理(避免影响非普通的认证流程)
    this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
    if (this.eventPublisher != null) {
        this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
    }
    if (this.tokenExtractor != null) {
        //设置TokenExtractor默认的实现BearerTokenExtractor
        this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
    }
    this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
    this.resourcesServerFilter.setStateless(this.stateless);
    // @formatter:off
    ((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}

OAuth2AuthenticationProcessingFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;
    try {
        //从请求中取出身份信息,即access_token,封装到 PreAuthenticatedAuthenticationToken
        Authentication authentication = this.tokenExtractor.extract(request);
        if (authentication == null) {
            .....
        } else {
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
            }
			//认证身份
            Authentication authResult = this.authenticationManager.authenticate(authentication);
            if (debug) {
                logger.debug("Authentication success: " + authResult);
            }
            this.eventPublisher.publishAuthenticationSuccess(authResult);
            //将身份信息绑定到SecurityContextHolder中
            SecurityContextHolder.getContext().setAuthentication(authResult);
        }
    } catch (OAuth2Exception var9) {
        SecurityContextHolder.clearContext();
        if (debug) {
            logger.debug("Authentication request failed: " + var9);
        }
        this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
        this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
        return;
    }
    chain.doFilter(request, response);
}

整个filter步骤最核心的是下面2个:

1、 调用 tokenExtractor.extract() 方法从请求中解析出token信息并存放到 authentication 的 principal 字段 中

2、 调用 authenticationManager.authenticate() 认证过程: 注意此时的 authenticationManager 是 OAuth2AuthenticationManager

在解析@EnableResourceServer 时我们讲过 OAuth2AuthenticationManager 与 OAuth2AuthenticationProcessingFilter 的关系,这里不再重述,我们直接看下 OAuth2AuthenticationManager 的 authenticate() 方法实现:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    if (authentication == null) {
        throw new InvalidTokenException("Invalid token (token not found)");
    }
    // 1、 从 authentication 中获取 token
    String token = (String) authentication.getPrincipal();
    // 2、 调用 tokenServices.loadAuthentication() 方法  通过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是我们资源服务器配置的。
    OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    if (auth == null) {
        throw new InvalidTokenException("Invalid token: " + token);
    }
    Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
    if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
        throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
    }
    // 3、 检测客户端信息,由于我们采用授权服务器和资源服务器分离的设计,所以这个检测方法实际没有检测
    checkClientDetails(auth);
    if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        // Guard against a cached copy of the same details
        if (!details.equals(auth.getDetails())) {
            // Preserve the authentication details from the one loaded by token services
            details.setDecodedDetails(auth.getDetails());
        }
    }
    // 4、 设置认证成功标识并返回
    auth.setDetails(authentication.getDetails());
    auth.setAuthenticated(true);
    return auth;
}

整个 认证逻辑分4步:

1、 从 authentication 中获取 token

2、 调用 tokenServices.loadAuthentication() 方法 通过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是我们资源服务器配置的。

3、 检测客户端信息,由于我们采用授权服务器和资源服务器分离的设计,所以这个检测方法实际没有检测

4、 设置认证成功标识并返回 ,注意返回的是 OAuth2Authentication (Authentication 子类)。

到此这篇关于Java中关于OAuth2.0的原理分析的文章就介绍到这了,更多相关OAuth2.0原理 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 关于@JSONField和@JsonFormat的使用区别说明

    关于@JSONField和@JsonFormat的使用区别说明

    这篇文章主要介绍了关于@JSONField 和 @JsonFormat的区别说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java版的7种单例模式写法示例

    Java版的7种单例模式写法示例

    这篇文章主要给大家介绍了关于Java版的7种单例模式写法,文中通过示例代码介绍的非常详细,对大家学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-10-10
  • 详解Java中的流程控制

    详解Java中的流程控制

    今天带大家复习Java基础知识,文中对Java流程控制作了非常详细的介绍及代码示例,对正在学习Java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-05-05
  • java利用socket通信实现Modbus-RTU通信协议的示例代码

    java利用socket通信实现Modbus-RTU通信协议的示例代码

    这篇文章主要介绍了java利用socket通信实现Modbus-RTU通信协议的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Java递归寻路实现,你真的理解了吗

    Java递归寻路实现,你真的理解了吗

    递归就是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,需要的朋友可以参考下
    2021-08-08
  • Mybatis中一条SQL使用两个foreach的问题及解决

    Mybatis中一条SQL使用两个foreach的问题及解决

    这篇文章主要介绍了Mybatis中一条SQL使用两个foreach的问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Java解析XML文件开源库DOM4J

    Java解析XML文件开源库DOM4J

    dom4j是一个Java的XML API,是jdom的升级品,用来读写XML文件的。dom4j是一个十分优秀的JavaXML API,具有性能优异、功能强大和极其易使用的特点,它的性能超过sun公司官方的dom技术,同时它也是一个开放源代码的软件
    2023-01-01
  • 解决SecureRandom.getInstanceStrong()引发的线程阻塞问题

    解决SecureRandom.getInstanceStrong()引发的线程阻塞问题

    这篇文章主要介绍了解决SecureRandom.getInstanceStrong()引发的线程阻塞问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • springboot通过jar包启动中文日志乱码问题及解决

    springboot通过jar包启动中文日志乱码问题及解决

    这篇文章主要介绍了springboot通过jar包启动中文日志乱码问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • 使用Spring处理x-www-form-urlencoded方式

    使用Spring处理x-www-form-urlencoded方式

    这篇文章主要介绍了使用Spring处理x-www-form-urlencoded方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11

最新评论