当前位置: 首页 > news >正文

etw系统provider事件较多_使用Spring Gateway和KeyCloak构建一个OIDC认证系统

bc5dc1c88dc7a31e4576f5b22e90689e.png

个人博客地址:

《如何使用Spring Gateway和KeyCloak构建一个OIDC系统》​www.chencanhao.com

这篇文章介绍一下,如何搭建一个基于 Spring Gateway 和 KeyCloak 的 OAuth2 资源保护系统,并使用 OIDC 作登录认证,这里只介绍思路和核心代码,供有一定基础的读者分享思路

首先我们需要了解这个小系统需要的组件,分别是

  • OAuth2 Server,这个我们选用的是 KeyCloak
  • Api Gateway & OAuth2 Client,使用 Spring Gateway 作为 OAuth2 的客户端
  • Resource Server (RS),就是在 Api Gateway 后面隐藏的资源服务

整体的架构可以看下图

2b3612e9d5345ee495047e2ff80d1251.png
图片出自 https://spring.io/blog/2019/08/16/securing-services-with-spring-cloud-gateway

资源认证流程是,客户端(浏览器)访问应用,此时没有认证状态,然后重定向到单点登录平台,也就是 KeyCloak,然后在 KeyCloak 上进行用户名密码认证(OIDC),成功后,KeyCloak 返回认证后的信息,然后客户端(Gateway)通过这些信息,再生成一个 Token,传到被保护的 Resource Resource Server 拿到这个 Token 再向 KeyCloak 进行权限的认证,如果认证都通过,则允许对资源进行操作。

OIDC

我们会使用 OIDC 作为用户登录认证方案

什么是 OIDC

看一下官方的介绍(http://openid.net/connect/)

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and session management, when it makes sense for them.

简单的来说,就是在 OAuth2 上多做了一个身份层,是一个基于 OAuth2 协议的身份认证标准协议。OIDC 使用 OAuth2 的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,前端 SPA ),且完全兼容 OAuth2。

所以,我们只需要使用 Spring Security 的 OAuth2 模块进行配置即可

Api Gateway 搭建

第一步我们可以先进行 Api Gateway 的搭建,然后再回过头来设置 KeyCloak

我们假设这个 Demo 应用的名字为 orange

然后我们要搭建 api gateway,使用 https://start.spring.io 来生成项目,十分方便

de4190ac36f57a00ce2a27fcdcd65787.png

我们勾选了 Gateway 和 OAuth2 Client 这两个依赖,然后下载下来,在 IDE 中打开,尝试运行一下,成功的话应该会在 8080 端口运行。

KeyCloak 搭建

搭建一个 KeyCloak demo 也是十分简单,可以直接从官方网站上下载 java 包,然后通过命令也是可以一键运行,不过这里还是推荐使用 Docker 来运行,十分方便和干净

这里给出 Docker KeyCloak 容器启动命名,我们把端口映射到 6180

docker run -p 6180:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=123456 -d jboss/keycloak
这个 Admin 密码设成 123456,在正式一点的环境肯定也是不行的,不过我们是 Demo,就不需要管那么多了

然后创建一个 Realm

d1748231d5784c2226b2a81cb3986bc8.png

然后创建一个客户端 Client

6afd76369bf8500d96fa598620f78201.png

9a047ea296a895999ea8fa95be0f36ec.png

接着创建一个用户

a868bc77beb6ff8c0748a6486e153f91.png

最后,我们需要使用到 OpenID 的一些 URI,我们可以打开这个 URL 来查到全面的 URI,这些 URI 会 OAuth 客户端中用到

http://192.168.50.251:6180/auth/realms/orange/.well-known/openid-configuration

实际操作中,需要将的 ip 和 realm 名字替换掉

a51ae7e4811a206dada3c1f1f7f0ec05.png

与 Gateway 集成

授权类型

然后我们需要在 Gateway 上集成 OAuth2,我们选择的授权类型是 Authorization Code Grant,虽然我们这个 Demo 的前端也是一个 SPA,可以直接用前端作为一个 OAuth2 客户端,然后选择 Implicit Grant 作为授权类型,但是我们还是选择了 Authorization Code Grant,这种授权类型的流程见下图

9b3faee6d2a908ce0a31c6eb2f7f1121.png
图片出自 An OAuth 2.0 introduction for beginners

为什么要选择 Authorization Code Grant 而不是 Implicit Grant,考虑的其实是一个安全性问题。

在使用隐式许可类型时需要对它严苛的局限性有所认识。首先,使用这种许可流程的客户端无法持有客户端密钥,因为无法对浏览器隐藏密钥。但由于这种许可流程只使用授权端点而不使用令牌端点,因此这个限制不会影响其功能,因为不要求客户端在授权端点上进行身份认证。然而,由于缺少对客户端进行身份认证的手段,确实会影响这种许可类型的安全等级,因此要谨慎使用。另外,隐式许可流程不可用于获取刷新令牌。因为浏览器内的应用具有短暂运行的特点,只会在被加载到浏览器的期间保持会话,所以刷新令牌在这里的作用非常有限。而且,和其他许可类型不同,这种许可类型会假设资源拥有者一直在场,必要时可以对客户端重新授权。在这种许可类型下,授权服务器仍然可以遵循首次使用时信任(TOFU)的原则,通过允许重新授权获得无缝的用户体验。
贾斯廷·里彻,安东尼奥·桑索. OAuth 2实战 (Chinese Edition) (Kindle 位置 1940-1945). Kindle 版本.

Maven 依赖

确定 Gateway 的依赖包,可以通过 https://start.spring.io 来生成

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>

Application.yaml DSL 配置

然后现在 application.yaml 中配置 OAuth2,只需要在 Provider 下面的 KeyCloak 中配置 issuer-uri 即可,这个地址可以在 keycloak 的 Admin 控制台中找到

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://192.168.50.251:6180/auth/realms/humpback_dev
        registration:
          keycloak:
            client-id: orange
            client-secret: a1246398-4c2f-46bd-b83d-9b1313f3378d

然后我们继续通过 yaml 文件配置 gateway 的配置,其中 http://localhost:8260 是我们接下来要创建 RS 服务。

spring:
  cloud:
    gateway:
      routes:
        - id: orange
          uri: http://localhost:8260
          predicates:
            - Path=/**
          filters:
#            - TokenRelay
            - TokenRelayWithTokenRefresh
            - RemoveRequestHeader=Cookie, Set-Cookie

注意上面被注释掉的 TokenRelay,这是一个 GatewayFilterFactory,不过这个 Filter 现在还有个比较大的问题,就是如果 Access Token 过期的话,还是会把请求发到 RS 那里,导致后续请求都是 401 的状态。

这个问题可以看 https://github.com/spring-cloud/spring-cloud-security/issues/175 这个 Github Issue

这个 Issue 下面有一个现成的解决方案,就是自定义一个 TokenRelay,实现如下:

import java.time.Duration;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

/**
 * Token Relay Gateway Filter with Token Refresh. This can be removed when issue {@see https://github.com/spring-cloud/spring-cloud-security/issues/175} is closed.
 * Implementierung in Anlehnung an {@link ServerOAuth2AuthorizedClientExchangeFilterFunction}
 */
@Component
public class TokenRelayWithTokenRefreshGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;

    private static final Duration accessTokenExpiresSkew = Duration.ofSeconds(3);

    public TokenRelayWithTokenRefreshGatewayFilterFactory(ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
                                                          ReactiveClientRegistrationRepository clientRegistrationRepository) {
        super(Object.class);
        this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
    }

    private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .clientCredentials(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .password(configurer -> configurer.clockSkew(accessTokenExpiresSkew))
                        .build();
        final DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    public GatewayFilter apply() {
        return apply((Object) null);
    }

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> exchange.getPrincipal()
                // .log("token-relay-filter")
                .filter(principal -> principal instanceof OAuth2AuthenticationToken)
                .cast(OAuth2AuthenticationToken.class)
                .flatMap(this::authorizeClient)
                .map(OAuth2AuthorizedClient::getAccessToken)
                .map(token -> withBearerAuth(exchange, token))
                // TODO: adjustable behavior if empty
                .defaultIfEmpty(exchange).flatMap(chain::filter);
    }

    private ServerWebExchange withBearerAuth(ServerWebExchange exchange, OAuth2AccessToken accessToken) {
        return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue()))).build();
    }

    private Mono<OAuth2AuthorizedClient> authorizeClient(OAuth2AuthenticationToken oAuth2AuthenticationToken) {
        final String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();
        return Mono.defer(() -> authorizedClientManager.authorize(createOAuth2AuthorizeRequest(clientRegistrationId, oAuth2AuthenticationToken)));
    }

    private OAuth2AuthorizeRequest createOAuth2AuthorizeRequest(String clientRegistrationId, Authentication principal) {
        return OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build();
    }
}

SecurityConfig

接下来就是 Spring Security 的设置了,具体可以看这个方法

这个是 WebFlux 的 Security 配置,跟 Spring MVC 的配置还是挺不一样的

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ReactiveClientRegistrationRepository clientRegistrationRepository) {
    http.cors();

    http.oauth2Login().authenticationSuccessHandler(myServerAuthenticationSuccessHandler);

    http.logout(logout -> logout.logoutSuccessHandler(
            new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)));
    http.logout().logoutUrl("/auth/logout");

    http.authorizeExchange().anyExchange().authenticated();

    http.headers().frameOptions().disable().xssProtection().disable();
    http.csrf().disable();
    http.httpBasic().disable();
    http.formLogin().disable();
    return http.build();
}

这个配置先设置了 OAuth2,包括 login,logout 等,然后把一些安全保护方法都去掉,不然等会在前端调用会十分麻烦

当然在生产环境还是要老老实实配置好 csrf,xss 这些参数。

处理重定向问题

最后我们还有一个问题需要考虑

因为 orange 这个应用的 OAuth2 客户端是在 Gateway,前端也有多种方法可以构造授权 Url 到 KeyCloak 进行登陆授权,但是我们选择一种比较简单的方法,那就是在页面中直接重定向到我们的 Gateway

假设我们前端运行在 http://localhost:3000 这个域名端口下,然后我们打开了 http://localhost:3000/orange_list 这页面,在这个页面进行登陆授权

然后我们 redirect 重定向到我们的 gateway 地址 http://localhost:8080,因为我们并没有授权过的 session,所以 gateway 会构造 URL 到 KeyCloak 中授权,这个 URL 大概长这样 http://192.168.50.251:6180/auth/realms/orange/protocol/openid-connect/auth?response_type=code&client_id=humpback-gateway&scope=openid%20address%20email%20microprofile-jwt%20offline_access%20phone%20profile%20roles%20user%20web-origins&state=VHp-YIiBsy9G-Kxm206bGHmm2gGRjF7D8Eu5rGpZVtM%3D&redirect_uri=http://localhost:8080/login/oauth2/code/keycloak&nonce=KzOiAXpzqrRXK67qzYdF5wK2pH_KGCUaBEHdz3pdnYI

可以看到这个 URL 中的 redirect_url 指的是 gateway 地址,因为在 keycloak 授权完成之后,keycloak 重定向到 Gateway,URL长这样 :

http://192.168.50.251:6180/auth/realms/humpback_dev/protocol/openid-connect/auth?response_type=code&client_id=humpback-gateway&scope=openid%20address%20email%20microprofile-jwt%20offline_access%20phone%20profile%20roles%20user%20web-origins&state=54Hy3lHVo1l2AMGPUWRDvBQIoLru328qr3p-5ynpp20%3D&redirect_uri=http://localhost:8080/login/oauth2/code/keycloak&nonce=frqYBfSEjaScFuYLI3KF6TE1vNVwjht0minWWSbDzZ0

然后 gateway 会再次请求 OAuth Server(也就是 KeyCloak)获取 Access Token

当然,如果 Implicit Grant 的话,就不需要这么麻烦,直接用前端作为 OAuth 客户端即可,也不需要 Server 端处理 OAuth 流程了

这个时候,作为登录这个用例来看,已经是登录成功了,那么 Gateway 就需要重定向回我们的前端页面了,不过这个时候 Gateway 并不知道之前来的 http://localhost:3000/orange_list

当然也是做到 redirect 回之前的页面的,但是十分麻烦,思路是前端重定向到 Gateway 的时候带上 redirect_url,例如 http://localhost:8080/oauth/keycloak?redirect_url=http://localhost:3000/orange_list ,然后把 http://localhost:3000/orange_list 保存到 Session 中,登录完成后从 session 中拿到 http://localhost:3000/orange_list 进行重定向

这种方法在 server 端带来了额外的状态,而且这个逻辑会跟正常的 API 请求有冲突,所以 gateway 索性把这个重定向功能还给前端,让前端通过 session storage 或者其他方法来处理

最后,登录成功之后,通过自定义的 SuccessHandler 重定向回前端页面,下面是 ServerAuthenticationSuccessHandler 的实现代码

@Component
public class MyServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
    private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();

    @Value("${application.frontend_url}")
    private String DEFAULT_LOGIN_SUCCESS_URL; // http://localhost:3000

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        URI url = URI.create(DEFAULT_LOGIN_SUCCESS_URL);
        return this.redirectStrategy
                .sendRedirect(webFilterExchange.getExchange(), url);
    }
}

Resource Service 搭建

Resource Service 就是被保护的资源,当然也可以是其他类型的服务。

创建一个 kotlin 的 Orange data 类

data class Orange(var name: String, var queryUserId: String) {}

实现简单的 RestController,同样也是 Kotlin 代码

import org.springframework.http.MediaType
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*

@RestController
class OrangeController() {
    @RequestMapping(value = ["/oranges"], method = [RequestMethod.GET], produces = [MediaType.APPLICATION_JSON_VALUE])
    fun querySpaces(@AuthenticationPrincipal() principal: Jwt): List<Orange> {
        val orange1 = Orange("Orange1", principal.claims.get("sub") as String)
        val orange2 = Orange("Orange2", principal.claims.get("sub") as String)
        return listOf(orange1, orange2)
    }
}

然后我们配置 Spring Security 的 resource server

我们需要用到 spring-boot-starter-oauth2-resource-server 这个包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

然后在 application.yaml 中进行配置 Jwt Resource Server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://[keycloak_host]/auth/realms/orange
          jwk-set-uri: http://[keycloak_host]/auth/realms/orange/protocol/openid-connect/certs

然后是 Spring Security 的设置

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        // TODO: check role
        // https://stackoverflow.com/questions/47069345/how-to-use-spring-security-remotetokenservice-with-keycloak
        // https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide
        http
                .authorizeRequests()
                .antMatchers("/**")
                .hasAuthority("SCOPE_openid")
                // .antMatchers("/**")
                // .hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable().xssProtection().disable()
                .and()
                .formLogin().disable()
                .oauth2ResourceServer()
                .jwt()
    }
}
上面的代码中,还有一个问题还没有解决,就是 hasRole 这个方法一直不生效,也上网查阅过很多资料,包括在 keyCloak 中对 Role 进行 Mapping,但是还是不生效,在 Spring Security 的源码中看了一下,好像在 Web Reactive 的代码中没有找到 hasRole 的相关代码,也就暂时放下了,读者在参考这篇文章实现的时候要多注意一下。

最后我们在 Postman 中进行最后的测试,Get 请求 http://localhost:8080/oranges 来获取两个 Orange 资源

在 Postman 的 Authorization 选项卡中获取 OAuth2 的 Access Token,Spring Security 会自动 Decode 去再向 KeyCloak 获取一遍 Access Token,然后创建 Session

当然 Postman 的步骤比较麻烦,也可以直接通过浏览器打开 http://localhost:8080, 浏览器会重定向到 KeyCloak 进行认证,不过认证成功后会跳转到前端 http://localhost:3000,这时候再访问 http://localhost:8080/oranges 即可

c8720d176b1e45edf36da93b3d0ada91.png

可以看到我们已经获取到了两个 Orange JSON 对象

前端

前端的集成方案就比较简单,不论是服务端模版生成的前端,还是单页面 SPA 应用程序,只要没有登录,就直接重定向到 Gateway 的地址 http://localhost:8080/oauth2/authorization/keycloak,然后 Gateway 处理完 KeyCloak 的登录流程后,就会自动跳转回前端页面。

如前文所说,这种方案的 Url 重定向需要由前端来处理,假如你在 http://localhost:3000/welcome 页面,如果登录完之后,最好还是跳转回 /welcome,但是 Gateway 重定向到 http://localhost:3000

而判断用户是否已经登录,已经重定向到 Gateway 进行登录这一个动作也是前端页面完成的,那么可以在前端重定向之前,把当前的 Url 记录到 SessionStorage 中,然后从 Gateway 登录回来之后再读取 SessionStorage 中的内容,进行重定向。

一些总结

最后,总的来说,现在 Spring 的 WebFlux 技术栈虽然说已经发展挺久的,但是相对来说资料还是比较少,而且看上起问题还不少,特别是 WebFlux + Spring Security OAuth 这种技术栈,所以没有特殊要求还是选择 Zuul 作为 Gateway 比较省心。

其次,如果后端对登录这一块没有更强的安全要求,或者对登录态有控制要求的话,前端可以直接使用 Implicit Grant 这种授权模式来获取 Access Token,Gateway 就只需要做转发即可,也能省下不少工作量。

参考资料

  • Securing Services with Spring Cloud Gateway
  • An OAuth 2.0 introduction for beginners
  • Spring Security 5 – OAuth2 Login
  • Spring Cloud Gateway with OpenID Connect and Token Relay
  • 《OAuth 2实战》

相关文章:

  • @data注解_一枚 架构师 也不会用的Lombok注解,相见恨晚
  • java中int和integer的区别_Java中关于强、软、弱、虚引用的区别
  • android 投屏_[Android] 虫洞手机投屏电脑(支持键盘映射和传声音)
  • 云丁智能锁说明书_真硬核!行业爆发前夜,这把锁登上航母
  • python调用node_node-python:在nodejs中调用python代码
  • python多线程读取文件内容_python多线程读取logcat内容,导致其他线程阻塞
  • python闭包满足的三个条件_Python中的闭包
  • javascript等待异步线程完成_程序员修神之路--问世间异步为何物?
  • python取随机数画图_python3测试工具开发快速入门教程1turtle绘图-4选择与随机数...
  • python中如何输入多行字符_python中怎么输入多行字符串
  • python饼图显示百分比_解决echarts饼图显示百分比,和显示内容字体及大小
  • java 二维数组定义长方体_47.二维数组的定义
  • zap 自定义日志格式_Go 每日一库之 zap
  • python接管已经打开ie浏览器_Python Webdriver 重新使用已经打开的浏览器实例
  • python 单例 多线程_python 单例模式
  • -------------------- 第二讲-------- 第一节------在此给出链表的基本操作
  • @jsonView过滤属性
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • 【Linux系统编程】快速查找errno错误码信息
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • 2017年终总结、随想
  • IDEA常用插件整理
  • Laravel 菜鸟晋级之路
  • Linux gpio口使用方法
  • Linux编程学习笔记 | Linux IO学习[1] - 文件IO
  • Lucene解析 - 基本概念
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • npx命令介绍
  • Python学习笔记 字符串拼接
  • storm drpc实例
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 聊聊hikari连接池的leakDetectionThreshold
  • 聊聊redis的数据结构的应用
  • 容器服务kubernetes弹性伸缩高级用法
  • 入口文件开始,分析Vue源码实现
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • # centos7下FFmpeg环境部署记录
  • (2015)JS ES6 必知的十个 特性
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (笔试题)分解质因式
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (转)http-server应用
  • (转)linux 命令大全
  • *p=a是把a的值赋给p,p=a是把a的地址赋给p。
  • .Net 高效开发之不可错过的实用工具
  • .NET 使用配置文件
  • .Net中间语言BeforeFieldInit
  • .Net组件程序设计之线程、并发管理(一)
  • /proc/vmstat 详解
  • /var/log/cvslog 太大
  • ?
  • @ 代码随想录算法训练营第8周(C语言)|Day53(动态规划)
  • @RequestMapping 的作用是什么?
  • @WebService和@WebMethod注解的用法
  • [2]十道算法题【Java实现】