Java中關(guān)于OAuth2.0的原理分析
授權(quán)服務(wù)器
@EnableAuthorizationServer解析
我們都知道 一個授權(quán)認(rèn)證服務(wù)器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要做了什么呢?
我們看下 @EnableAuthorizationServer 源碼:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class}) public @interface EnableAuthorizationServer { }
我們可以看到其源碼內(nèi)部導(dǎo)入了 AuthorizationServerEndpointsConfiguration 和 AuthorizationServerSecurityConfiguration 這2個配置類。 接下來我們分別看下這2個配置類具體做了什么。
AuthorizationServerEndpointsConfiguration
@Configuration @Import(TokenKeyEndpointRegistrar.class) public class AuthorizationServerEndpointsConfiguration { // 省略 其他相關(guān)配置代碼 .... // 1、 AuthorizationEndpoint 創(chuàng)建 @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 創(chuàng)建 @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; } // 省略 其他相關(guān)配置代碼 ....
通過源碼我們可以很明確的知道:
- AuthorizationEndpoint 用于服務(wù)授權(quán)請求。預(yù)設(shè)地址:/oauth/authorize。
- TokenEndpoint 用于服務(wù)訪問令牌的請求。預(yù)設(shè)地址:/oauth/token。
AuthorizationServerSecurityConfiguration
- ClientDetailsService : 內(nèi)部僅有 loadClientByClientId 方法。從方法名我們就可知其是通過 clientId 來獲取 Client 信息, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2個實現(xiàn)類,我們也可以像UserDetailsService 一樣編寫自己的實現(xiàn)類。
- UserDetailsService : 內(nèi)部僅有 loadUserByUsername 方法。這個類不用我再介紹了吧。不清楚得同學(xué)可以看下我之前得文章。
- ClientDetailsUserDetailsService : UserDetailsService子類,內(nèi)部維護了 ClientDetailsService 。其 loadUserByUsername 方法重寫后調(diào)用ClientDetailsService.loadClientByClientId()。
- ClientCredentialsTokenEndpointFilter** 作用與 UserNamePasswordAuthenticationFilter 類似,通過攔截 /oauth/token 地址,獲取到 clientId 和 clientSecret 信息并創(chuàng)建 UsernamePasswordAuthenticationToken 作為 AuthenticationManager.authenticate() 參數(shù) 調(diào)用認(rèn)證過程。整個認(rèn)證過程唯一最大得區(qū)別在于 DaoAuthenticationProvider.retrieveUser() 獲取認(rèn)證用戶信息時調(diào)用的是 ClientDetailsUserDetailsService,根據(jù)前面講述的其內(nèi)部其實是調(diào)用ClientDetailsService 獲取到客戶端信息。
@EnableResourceServer
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ResourceServerConfiguration.class}) public @interface EnableResourceServer { }
從源碼中我們可以看到其導(dǎo)入了 ResourceServerConfiguration 配置類,這個配置類最核心的配置是 應(yīng)用了 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、 創(chuàng)建 OAuth2AuthenticationProcessingFilter 過濾器 對象
2、 創(chuàng)建 OAuth2AuthenticationManager 對象 對將其作為參數(shù)設(shè)置到 OAuth2AuthenticationProcessingFilter 中
3、 將 OAuth2AuthenticationProcessingFilter 過濾器添加到過濾器鏈上
AuthorizationEndpoint生成授權(quán)碼
@RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) { // 1、 通過 OAuth2RequestFactory 從 參數(shù)中獲取信息創(chuàng)建 AuthorizationRequest 授權(quán)請求對象 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 是否 已授權(quán) : /oauth/authorize 設(shè)置為無權(quán)限訪問 ,所以要判斷,如果 判斷失敗則拋出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉(zhuǎn)到 登錄頁面,這也是為什么我們第一次去請求獲取 授權(quán)碼時會跳轉(zhuǎn)到登陸界面的原因 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、 獲取參數(shù)中的回調(diào)地址并且與系統(tǒng)配置的回調(diào)地址對比 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、 檢測該客戶端是否設(shè)置自動 授權(quán)(即 我們配置客戶端時配置的 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 調(diào)用 getAuthorizationCodeResponse() 方法生成code碼并回調(diào)到設(shè)置的回調(diào)地址 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 從 參數(shù)中獲取信息創(chuàng)建 AuthorizationRequest 授權(quán)請求對象
2、 判斷 principal 是否 已授權(quán) : /oauth/authorize 設(shè)置為無權(quán)限訪問 ,所以要判斷,如果 判斷失敗則拋出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉(zhuǎn)到 登錄頁面,這也是為什么我們第一次去請求獲取 授權(quán)碼時會跳轉(zhuǎn)到登陸界面的原因
3、 通過 ClientDetailsService.loadClientByClientId() 獲取到 ClientDetails 客戶端信息
4、 獲取參數(shù)中的回調(diào)地址并且與系統(tǒng)配置的回調(diào)地址(步驟3獲取到的client信息)對比
5、 與步驟4一樣 驗證 scope
6、 檢測該客戶端是否設(shè)置自動 授權(quán)(即 我們配置客戶端時配置的 autoApprove(true))
7、 由于我們設(shè)置 autoApprove(true) 則 調(diào)用 getAuthorizationCodeResponse() 方法生成code碼并回調(diào)到設(shè)置的回調(diào)地址
8、 真實生成Code 的方法時 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其內(nèi)部是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、 驗證 用戶信息 (正常情況下會經(jīng)過 ClientCredentialsTokenEndpointFilter 過濾器認(rèn)證后獲取到用戶信息 ) if (!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException( "There is no client authentication. Try adding an appropriate authentication filter."); } // 2、 通過 ClientDetailsService().loadClientByClientId() 獲取系統(tǒng)配置客戶端信息 String clientId = getClientId(principal); ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); // 3、 通過客戶端信息生成 TokenRequest 對象 TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); ...... // 4、 調(diào)用 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、 驗證 用戶信息 (正常情況下會經(jīng)過 ClientCredentialsTokenEndpointFilter 過濾器認(rèn)證后獲取到用戶信息 )
2、 通過 ClientDetailsService().loadClientByClientId() 獲取系統(tǒng)配置的客戶端信息
3、 通過客戶端信息生成 TokenRequest 對象
4、 將步驟3獲取到的 TokenRequest 作為TokenGranter.grant() 方法參照 生成 OAuth2AccessToken 對象(即token)
5、 返回 token
TokenGranter
TokenGranter的設(shè)計思路是使用CompositeTokenGranter管理一個List列表,每一種grantType對應(yīng)一個具體的真正授權(quán)者,CompositeTokenGranter 內(nèi)部就是在循環(huán)調(diào)用五種TokenGranter實現(xiàn)類的 grant方法,而granter內(nèi)部則是通過grantType來區(qū)分是否是各自的授權(quán)類型。
五種類型分別是:
- ResourceOwnerPasswordTokenGranter ==> password密碼模式
- AuthorizationCodeTokenGranter ==> authorization_code授權(quán)碼模式
- 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 { //創(chuàng)建 OAuth2AccessToken createAccessToken(OAuth2Authentication var1) throws AuthenticationException; //刷新 OAuth2AccessToken refreshAccessToken(String var1, TokenRequest var2) throws AuthenticationException; //獲取 OAuth2AccessToken getAccessToken(OAuth2Authentication var1); }
流程
資源服務(wù)器
@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); //創(chuàng)建OAuth2核心過濾器 this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter(); this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint); //設(shè)置OAuth2的身份認(rèn)證處理器,沒有交給spring管理(避免影響非普通的認(rèn)證流程) this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager); if (this.eventPublisher != null) { this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher); } if (this.tokenExtractor != null) { //設(shè)置TokenExtractor默認(rèn)的實現(xiàn)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)); } //認(rèn)證身份 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、 調(diào)用 tokenExtractor.extract() 方法從請求中解析出token信息并存放到 authentication 的 principal 字段 中
2、 調(diào)用 authenticationManager.authenticate() 認(rèn)證過程: 注意此時的 authenticationManager 是 OAuth2AuthenticationManager
在解析@EnableResourceServer 時我們講過 OAuth2AuthenticationManager 與 OAuth2AuthenticationProcessingFilter 的關(guān)系,這里不再重述,我們直接看下 OAuth2AuthenticationManager 的 authenticate() 方法實現(xiàn):
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、 調(diào)用 tokenServices.loadAuthentication() 方法 通過 token 參數(shù)獲取到 OAuth2Authentication 對象 ,這里的tokenServices 就是我們資源服務(wù)器配置的。 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、 檢測客戶端信息,由于我們采用授權(quán)服務(wù)器和資源服務(wù)器分離的設(shè)計,所以這個檢測方法實際沒有檢測 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、 設(shè)置認(rèn)證成功標(biāo)識并返回 auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; }
整個 認(rèn)證邏輯分4步:
1、 從 authentication 中獲取 token
2、 調(diào)用 tokenServices.loadAuthentication() 方法 通過 token 參數(shù)獲取到 OAuth2Authentication 對象 ,這里的tokenServices 就是我們資源服務(wù)器配置的。
3、 檢測客戶端信息,由于我們采用授權(quán)服務(wù)器和資源服務(wù)器分離的設(shè)計,所以這個檢測方法實際沒有檢測
4、 設(shè)置認(rèn)證成功標(biāo)識并返回 ,注意返回的是 OAuth2Authentication (Authentication 子類)。
到此這篇關(guān)于Java中關(guān)于OAuth2.0的原理分析的文章就介紹到這了,更多相關(guān)OAuth2.0原理 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javaweb servlet中使用請求轉(zhuǎn)發(fā)亂碼的實現(xiàn)
下面小編就為大家?guī)硪黄猨avaweb servlet中使用請求轉(zhuǎn)發(fā)亂碼的實現(xiàn)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08java實現(xiàn)學(xué)生教師管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)學(xué)生教師管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-10-10Spring Boot 2.7.6整合redis與低版本的區(qū)別
這篇文章主要介紹了Spring Boot 2.7.6整合redis與低版本的區(qū)別,文中補充介紹了SpringBoot各個版本使用Redis之間的區(qū)別實例講解,需要的朋友可以參考下2023-02-02Java中BufferedReader與Scanner讀入的區(qū)別詳解
這篇文章主要介紹了Java中BufferedReader與Scanner讀入的區(qū)別詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10IDEA提示:Boolean method ‘xxx‘ is always&nb
這篇文章主要介紹了IDEA提示:Boolean method ‘xxx‘ is always inverted問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08springBoot下實現(xiàn)java自動創(chuàng)建數(shù)據(jù)庫表
這篇文章主要介紹了springBoot下實現(xiàn)java自動創(chuàng)建數(shù)據(jù)庫表的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07spring?cloud?gateway限流常見算法實現(xiàn)
本文主要介紹了spring?cloud?gateway限流常見算法實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02elasticsearch源碼分析index?action實現(xiàn)方式
這篇文章主要為大家介紹了elasticsearch源碼分析index?action實現(xiàn)方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04