從零入門(mén) SpringSecurity
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)??

來(lái)源:juejin.cn/post/7026734817853210661
今天來(lái)一篇 Spring Security 精講,相信你看過(guò)之后能徹底搞懂 Spring Security。
Spring Security簡(jiǎn)介
Spring Security 是一種高度自定義的安全框架,利用(基于)SpringIOC/DI和AOP功能,為系統(tǒng)提供了聲明式安全訪(fǎng)問(wèn)控制功能,「減少了為系統(tǒng)安全而編寫(xiě)大量重復(fù)代碼的工作」。
「核心功能:認(rèn)證和授權(quán)」
Spring Security 認(rèn)證流程

Spring Security 項(xiàng)目搭建
導(dǎo)入依賴(lài)
Spring Security已經(jīng)被Spring boot進(jìn)行集成,使用時(shí)直接引入啟動(dòng)器即可
<dependency>
????<groupId>org.springframework.bootgroupId>
????<artifactId>spring-boot-starter-securityartifactId>
dependency>
訪(fǎng)問(wèn)頁(yè)面
導(dǎo)入spring-boot-starter-security啟動(dòng)器后,Spring Security已經(jīng)生效,默認(rèn)攔截全部請(qǐng)求,如果用戶(hù)沒(méi)有登錄,跳轉(zhuǎn)到內(nèi)置登錄頁(yè)面。
在瀏覽器輸入:http://localhost:8080/ 進(jìn)入Spring Security內(nèi)置登錄頁(yè)面
用戶(hù)名:user。
密碼:項(xiàng)目啟動(dòng),打印在控制臺(tái)中。
自定義用戶(hù)名和密碼
修改「application.yml」 文件
#?靜態(tài)用戶(hù),一般只在內(nèi)部網(wǎng)絡(luò)認(rèn)證中使用,如:內(nèi)部服務(wù)器1,訪(fǎng)問(wèn)服務(wù)器2
spring:
??security:
????user:
??????name:?test??#?通過(guò)配置文件,設(shè)置靜態(tài)用戶(hù)名
??????password:?test?#?配置文件,設(shè)置靜態(tài)登錄密碼
UserDetailsService詳解
什么也沒(méi)有配置的時(shí)候,賬號(hào)和密碼是由Spring Security定義生成的。而在實(shí)際項(xiàng)目中賬號(hào)和密碼都是從數(shù)據(jù)庫(kù)中查詢(xún)出來(lái)的。所以我們要通過(guò)「自定義邏輯控制認(rèn)證邏輯」。如果需要自定義邏輯時(shí),只需要實(shí)現(xiàn)UserDetailsService接口
@Component
public?class?UserSecurity?implements?UserDetailsService?{
????@Autowired
????private?UserService?userService;
????@Override
????public?UserDetails?loadUserByUsername(String?userName)?throws?UsernameNotFoundException?{
????????User?user?=?userService.login(userName);
????????System.out.println(user);
????????if?(null==user){
????????????throw?new?UsernameNotFoundException("用戶(hù)名錯(cuò)誤");
????????}
????????org.springframework.security.core.userdetails.User?result?=
????????????????new?org.springframework.security.core.userdetails.User(
????????????????????????userName,user.getPassword(),?AuthorityUtils.createAuthorityList()
????????????????);
????????return?result;
????}
}
PasswordEncoder密碼解析器詳解
PasswordEncoder
「PasswordEncoder」 是SpringSecurity 的密碼解析器,用戶(hù)密碼校驗(yàn)、加密 。自定義登錄邏輯時(shí)要求必須給容器注入PaswordEncoder的bean對(duì)象
SpringSecurity 定義了很多實(shí)現(xiàn)接口「PasswordEncoder」 滿(mǎn)足我們密碼加密、密碼校驗(yàn) 使用需求。

自定義密碼解析器
編寫(xiě)類(lèi),實(shí)現(xiàn)PasswordEncoder 接口
/**
?*?憑證匹配器,用于做認(rèn)證流程的憑證校驗(yàn)使用的類(lèi)型
?*?其中有2個(gè)核心方法
?*?1.?encode?-?把明文密碼,加密成密文密碼
?*?2.?matches?-?校驗(yàn)明文和密文是否匹配
?*?*/
public?class?MyMD5PasswordEncoder?implements?PasswordEncoder?{
????/**
?????*?加密
?????*?@param?charSequence??明文字符串
?????*?@return
?????*/
????@Override
????public?String?encode(CharSequence?charSequence)?{
????????try?{
????????????MessageDigest?digest?=?MessageDigest.getInstance("MD5");
????????????return?toHexString(digest.digest(charSequence.toString().getBytes()));
????????}?catch?(NoSuchAlgorithmException?e)?{
????????????e.printStackTrace();
????????????return?"";
????????}
????}
????/**
?????*?密碼校驗(yàn)
?????*?@param?charSequence?明文,頁(yè)面收集密碼
?????*?@param?s?密文?,數(shù)據(jù)庫(kù)中存放密碼
?????*?@return
?????*/
????@Override
????public?boolean?matches(CharSequence?charSequence,?String?s)?{
????????return?s.equals(encode(charSequence));
????}
?????/**
?????*?@param?tmp?轉(zhuǎn)16進(jìn)制字節(jié)數(shù)組
?????*?@return?飯回16進(jìn)制字符串
?????*/
????private?String?toHexString(byte?[]?tmp){
????????StringBuilder?builder?=?new?StringBuilder();
????????for?(byte?b?:tmp){
????????????String?s?=?Integer.toHexString(b?&?0xFF);
????????????if?(s.length()==1){
????????????????builder.append("0");
????????????}
????????????builder.append(s);
????????}
????????return?builder.toString();
????}
}
2.在配置類(lèi)中指定自定義密碼憑證匹配器
/**
??*?加密
??*?@return?加密對(duì)象
??*?如需使用自定義密碼憑證匹配器?返回自定義加密對(duì)象
??*?例如:?return?new?MD5PasswordEncoder();?
??*/
@Bean
public?PasswordEncoder?passwordEncoder()?{
????return?new?BCryptPasswordEncoder();?//Spring?Security?自帶
}
登錄配置
方式一 轉(zhuǎn)發(fā)
http.formLogin()
????.usernameParameter("name")?//?設(shè)置請(qǐng)求參數(shù)中,用戶(hù)名參數(shù)名稱(chēng)。?默認(rèn)username
????.passwordParameter("pswd")?//?設(shè)置請(qǐng)求參數(shù)中,密碼參數(shù)名稱(chēng)。?默認(rèn)password
????.loginPage("/toLogin")?//?當(dāng)用戶(hù)未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁(yè)面地址是什么??默認(rèn)?/login
????.loginProcessingUrl("/login")?//?用戶(hù)登錄邏輯請(qǐng)求地址是什么。?默認(rèn)是?/login
????.failureForwardUrl("/failure");?//?登錄失敗后,請(qǐng)求轉(zhuǎn)發(fā)的位置。Security請(qǐng)求轉(zhuǎn)發(fā)使用Post請(qǐng)求。默認(rèn)轉(zhuǎn)發(fā)到:loginPage?error
????.successForwardUrl("/toMain");?//?用戶(hù)登錄成功后,請(qǐng)求轉(zhuǎn)發(fā)到的位置。Security請(qǐng)求轉(zhuǎn)發(fā)使用POST請(qǐng)求。
方式二 :重定向
http.formLogin()
????.usernameParameter("name")?//?設(shè)置請(qǐng)求參數(shù)中,用戶(hù)名參數(shù)名稱(chēng)。?默認(rèn)username
????.passwordParameter("pswd")?//?設(shè)置請(qǐng)求參數(shù)中,密碼參數(shù)名稱(chēng)。?默認(rèn)password
????.loginPage("/toLogin")?//?當(dāng)用戶(hù)未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁(yè)面地址是什么??默認(rèn)?/login
????.loginProcessingUrl("/login")?//?用戶(hù)登錄邏輯請(qǐng)求地址是什么。?默認(rèn)是?/login
?.defaultSuccessUrl("/toMain",true);?//用戶(hù)登錄成功后,響應(yīng)重定向到的位置。GET請(qǐng)求。必須配置絕對(duì)地址。
??.failureUrl("/failure");?//?登錄失敗后,重定向的位置。
方式三:自定義登錄處理器
自定義登錄失敗邏輯處理器
/*自定義登錄失敗處理器*/
public?class?MyAuthenticationFailureHandler?implements?AuthenticationFailureHandler?{
????private??String?url;
????private?boolean?isRedirect;
????public?MyAuthenticationFailureHandler(String?url,?boolean?isRedirect)?{
????????this.url?=?url;
????????this.isRedirect?=?isRedirect;
????}
????@Override
????public?void?onAuthenticationFailure(HttpServletRequest?httpServletRequest,?HttpServletResponse?httpServletResponse,?AuthenticationException?e)?throws?IOException,?ServletException?{
????????if?(isRedirect){
????????????httpServletResponse.sendRedirect(url);
????????}else?{
????????????httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
????????}
????}
//get?set?方法?省略
自定義登錄成功邏輯處理器
/**
?*?自定義登錄成功后處理器
?*?轉(zhuǎn)發(fā)重定向,有代碼邏輯實(shí)現(xiàn)
?*?*/
public?class?MyAuthenticationSuccessHandler?implements?AuthenticationSuccessHandler?{
????private?String?url;
????private?boolean?isRedirect;
????public?MyAuthenticationSuccessHandler(String?url,?boolean?isRedirect)?{
????????this.url?=?url;
????????this.isRedirect?=?isRedirect;
????}
????/**
?????*?@param?request?請(qǐng)求對(duì)象?request.getRequestDispatcher.forward()
?????*?@param?response?響應(yīng)對(duì)象?response.sendRedirect()
?????*?@param?authentication 用戶(hù)認(rèn)證成功后的對(duì)象。其中報(bào)換用戶(hù)名權(quán)限結(jié)合,內(nèi)容是
?????*???????????????????????自定義UserDetailsService
?????*?*/
????@Override
????public?void?onAuthenticationSuccess(HttpServletRequest?request,?HttpServletResponse?response,?Authentication?authentication)?throws?IOException,?ServletException?{
????????if?(isRedirect){
????????????response.sendRedirect(url);
????????}else?{
????????????request.getRequestDispatcher(url).forward(request,response);
????????}
????}
//get?set?方法?省略???
http.formLogin()
????.usernameParameter("name")?//?設(shè)置請(qǐng)求參數(shù)中,用戶(hù)名參數(shù)名稱(chēng)。?默認(rèn)username
????.passwordParameter("pswd")?//?設(shè)置請(qǐng)求參數(shù)中,密碼參數(shù)名稱(chēng)。?默認(rèn)password
????.loginPage("/toLogin")?//?當(dāng)用戶(hù)未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁(yè)面地址是什么??默認(rèn)?/login
????.loginProcessingUrl("/login")?//?用戶(hù)登錄邏輯請(qǐng)求地址是什么。?默認(rèn)是?/login
登錄相關(guān)配置類(lèi)
@Configuration
@EnableWebSecurity
public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{
????@Autowired
????private??UserSecurity?userSecurity;
????@Autowired
????private?PersistentTokenRepository?persistentTokenRepository;
????/**
?????*?加密
?????*?@return?加密對(duì)象
?????*?如需使用自定義加密邏輯?返回自定義加密對(duì)象
?????*?return?new?MD5PasswordEncoder();?return?new?SimplePasswordEncoder();
?????*/
????@Bean
????public?PasswordEncoder?passwordEncoder()?{
????????return?new?BCryptPasswordEncoder();?//Spring?Security?自帶
????}
????@Override
????protected?void?configure(HttpSecurity?http)?throws?Exception?{
????????//?配置登錄請(qǐng)求相關(guān)內(nèi)容。
????????http.formLogin()
????????????.loginPage("/toLogin")?//?當(dāng)用戶(hù)未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁(yè)面地址是什么??默認(rèn)?/login
????????????.usernameParameter("name")?//?設(shè)置請(qǐng)求參數(shù)中,用戶(hù)名參數(shù)名稱(chēng)。?默認(rèn)username
????????????.passwordParameter("pswd")?//?設(shè)置請(qǐng)求參數(shù)中,密碼參數(shù)名稱(chēng)。?默認(rèn)password
????????????.loginProcessingUrl("/login")?//設(shè)置登錄?提交表單數(shù)據(jù)訪(fǎng)問(wèn)請(qǐng)求地址
????????????.defaultSuccessUrl("/toMain")???
????????????.failureUrl("/toLogin");
?????????//.successForwardUrl("/toMain")
?????????//.failureForwardUrl("/toLogin");
????????????//.successHandler(new?LoginSuccessHandler("/toMain",?true))?//自定義登錄成功處理器
????????????????//.failureHandler(new?LoginErrorHandler("/toLogin",?true));
????????http.authorizeRequests()
????????????//.antMatchers("/toLogin").anonymous()?//只能匿名用戶(hù)訪(fǎng)問(wèn)
????????????.antMatchers("/toLogin",?"/register",?"/login",?"/favicon.ico").permitAll()?//?/toLogin請(qǐng)求地址,可以隨便訪(fǎng)問(wèn)。
????????????.antMatchers("/**/*.js").permitAll()?//?授予所有目錄下的所有.js文件可訪(fǎng)問(wèn)權(quán)限
????????????.regexMatchers(".*[.]css").permitAll()?//?授予所有目錄下的所有.css文件可訪(fǎng)問(wèn)權(quán)限
????????????.anyRequest().authenticated();?//?任意的請(qǐng)求,都必須認(rèn)證后才能訪(fǎng)問(wèn)。
????????//?配置退出登錄
????????http.logout()
????????????????.invalidateHttpSession(true)?//?回收HttpSession對(duì)象。退出之前調(diào)用HttpSession.invalidate()?默認(rèn) true
????????????????.clearAuthentication(true)?//?退出之前,清空Security記錄的用戶(hù)登錄標(biāo)記。?默認(rèn) true
????????????????// .addLogoutHandler()?//?增加退出處理器。
????????????????.logoutSuccessUrl("/")?//?配置退出后,進(jìn)入的請(qǐng)求地址。?默認(rèn)是loginPage?logout
????????????????.logoutUrl("/logout");?//?配置退出登錄的路徑地址。和頁(yè)面請(qǐng)求地址一致即可。
????????//?關(guān)閉CSRF安全協(xié)議。
????????//?關(guān)閉是為了保證完整流程的可用。
????????http.csrf().disable();
????}
???@Bean
???public?PersistentTokenRepository?persistentTokenRepository(DataSource?dataSource){
????????JdbcTokenRepositoryImpl?jdbcTokenRepository?=?new?JdbcTokenRepositoryImpl();
????????jdbcTokenRepository.setDataSource(dataSource);
????????//jdbcTokenRepository.setCreateTableOnStartup(true);
????????return?jdbcTokenRepository;
????}
}
角色權(quán)限
?「hasAuthority(String)」 判斷角色是否具有特定權(quán)限
?
http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")
?「hasAnyAuthority(String ...)」 如果用戶(hù)具備給定權(quán)限中某一個(gè),就允許訪(fǎng)問(wèn)
?
http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx")?
?「hasRole(String)」 如果用戶(hù)具備給定角色就允許訪(fǎng)問(wèn)。否則出現(xiàn)403
?
//請(qǐng)求地址為/admin/read的請(qǐng)求,必須登錄用戶(hù)擁有'管理員'角色才可訪(fǎng)問(wèn)
http.authorizeRequests().antMatchers("/admin/read").hasRole("管理員")?
?「hasAnyRole(String ...)」 如果用戶(hù)具備給定角色的任意一個(gè),就允許被訪(fǎng)問(wèn)
?
//用戶(hù)擁有角色是管理員?或?訪(fǎng)客?可以訪(fǎng)問(wèn)?/guest/read
http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理員",?"訪(fǎng)客")
?「hasIpAddress(String)」 請(qǐng)求是指定的IP就運(yùn)行訪(fǎng)問(wèn)
?
//ip?是127.0.0.1?的請(qǐng)求?可以訪(fǎng)問(wèn)/ip
http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")
403 權(quán)限不足頁(yè)面處理
1.編寫(xiě)類(lèi)實(shí)現(xiàn)接口「AccessDeniedHandler」
/**
?*?@describe??403?權(quán)限不足
?*?@author:?AnyWhere
?*?@date?2021/4/18?20:57
?*/
@Component
public?class?MyAccessDeniedHandler?implements?AccessDeniedHandler?{
????@Override
????public?void?handle(HttpServletRequest?request,?HttpServletResponse?response,?AccessDeniedException?e)?
????????????throws?IOException,?ServletException?{
????????response.setStatus(HttpServletResponse.SC_OK);
????????response.setContentType("text/html;charset=UTF-8");
????????response.getWriter().write(
????????????????""?+
????????????????????????""?+
????????????????????????""?+
????????????????????????"權(quán)限不足,請(qǐng)聯(lián)系管理員"?+
????????????????????????""?+
????????????????????????""?+
????????????????????????""
????????);
????????response.getWriter().flush();//刷新緩沖區(qū)
????}
}
2.配置類(lèi)中配置exceptionHandling
//?配置403訪(fǎng)問(wèn)錯(cuò)誤處理器。
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/
RememberMe(記住我)
@Configuration
@EnableWebSecurity
public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{
??@Override
??protected?void?configure(HttpSecurity?http)?throws?Exception?{
????//配置記住密碼
????http.rememberMe()
????????.rememberMeParameter("remember-me")?//?修改請(qǐng)求參數(shù)名。?默認(rèn)是remember-me
????????.tokenValiditySeconds(14*24*60*60)?//?設(shè)置記住我有效時(shí)間。單位是秒。默認(rèn)是14天
????????.rememberMeCookieName("remember-me")?//?修改remember me的cookie名稱(chēng)。默認(rèn)是remember-me
????????.tokenRepository(persistentTokenRepository)?//?配置用戶(hù)登錄標(biāo)記的持久化工具對(duì)象。
????????.userDetailsService(userSecurity);?//?配置自定義的UserDetailsService接口實(shí)現(xiàn)類(lèi)對(duì)象
??}
??@Bean
??public?PersistentTokenRepository?persistentTokenRepository(DataSource?dataSource){
?????JdbcTokenRepositoryImpl?jdbcTokenRepository?=?new?JdbcTokenRepositoryImpl();
?????jdbcTokenRepository.setDataSource(dataSource);
?????//jdbcTokenRepository.setCreateTableOnStartup(true);
?????return?jdbcTokenRepository;
??}
}???
Spring Security 注解
@Secured
?角色校驗(yàn) ,請(qǐng)求到來(lái)訪(fǎng)問(wèn)控制單元方法時(shí)必須包含XX角色才能訪(fǎng)問(wèn)
角色必須添加ROLE_前綴
?
??@Secured({"ROLE_管理員","ROLE_訪(fǎng)客"})
??@RequestMapping("/toMain")
??public?String?toMain(){
??????return?"main";
??}
使用注解@Secured需要在配置類(lèi)中添加注解 使@Secured注解生效
@EnableGlobalMethodSecurity(securedEnabled?=?true)
@PreAuthorize
?權(quán)限檢驗(yàn),請(qǐng)求到來(lái)訪(fǎng)問(wèn)控制單元之前必須包含xx權(quán)限才能訪(fǎng)問(wèn),控制單元方法執(zhí)行前進(jìn)行角色校驗(yàn)
?
???/**
?????*?[ROLE_管理員,?admin:read,?admin:write,?all:login,?all:logout,?all:error,?all:toMain]
?????*?@PreAuthorize???角色?、權(quán)限?校驗(yàn)?方法執(zhí)行前進(jìn)行角色校驗(yàn)
?????*
?????*??hasAnyAuthority()?
?????*??hasAuthority()
?????*
?????*??hasPermission()
?????*
?????*
?????*??hasRole()???
?????*??hasAnyRole()
?????*?*/
????@PreAuthorize("hasAnyRole('ROLE_管理員','ROLE_訪(fǎng)客')")
????@RequestMapping("/toMain")
????@PreAuthorize("hasAuthority('admin:write')")
????public?String?toMain(){
????????return?"main";
????}
使用@PreAuthorize和@PostAuthorize 需要在配置類(lèi)中配置注解@EnableGlobalMethodSecurity 才能生效
@EnableGlobalMethodSecurity(prePostEnabled?=?true)
@PostAuthorize
?權(quán)限檢驗(yàn),請(qǐng)求到來(lái)訪(fǎng)問(wèn)控制單元之后必須包含xx權(quán)限才能訪(fǎng)問(wèn) ,控制單元方法執(zhí)行完后進(jìn)行角色校驗(yàn)
?
???/**
?????*?[ROLE_管理員,?admin:read,?admin:write,?all:login,?all:logout,?all:error,?all:toMain]
?????*?@PostAuthorize??角色?、權(quán)限?校驗(yàn)?方法執(zhí)行后進(jìn)行角色校驗(yàn)
?????*
?????*??hasAnyAuthority()
?????*??hasAuthority()
?????*??hasPermission()
?????*??hasRole()
?????*??hasAnyRole()
?????*?*/
????@PostAuthorize("hasRole('ROLE_管理員')")
????@RequestMapping("/toMain")
????@PreAuthorize("hasAuthority('admin:write')")
????public?String?toMain(){
????????return?"main";
????}
Spring Security 整合Thymeleaf 進(jìn)行權(quán)限校驗(yàn)
<dependency>
??????<groupId>org.springframework.bootgroupId>
??????<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
?????<groupId>org.thymeleaf.extrasgroupId>
?????<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
Spring Security中CSRF
什么是CSRF?
CSRF(Cross-site request forgery)跨站請(qǐng)求偽造,也被稱(chēng)為“One Click Attack” 或者Session Riding。通過(guò)偽造用戶(hù)請(qǐng)求訪(fǎng)問(wèn)受信任站點(diǎn)的非法請(qǐng)求訪(fǎng)問(wèn)。
跨域:只要網(wǎng)絡(luò)協(xié)議,ip地址,端口中任何一個(gè)不相同就是跨域請(qǐng)求。
客戶(hù)端與服務(wù)進(jìn)行交互時(shí),由于http協(xié)議本身是無(wú)狀態(tài)協(xié)議,所以引入了cookie進(jìn)行記錄客戶(hù)端身份。在cookie中會(huì)存放session id用來(lái)識(shí)別客戶(hù)端身份的。在跨域的情況下,session id可能被第三方惡意劫持,通過(guò)這個(gè)session id向服務(wù)端發(fā)起請(qǐng)求時(shí),服務(wù)端會(huì)認(rèn)為這個(gè)請(qǐng)求是合法的,可能發(fā)生很多意想不到的事情。
通俗解釋?zhuān)?/p>
CSRF就是別的網(wǎng)站非法獲取我們網(wǎng)站Cookie值,我們項(xiàng)目服務(wù)器是無(wú)法區(qū)分到底是不是我們的客戶(hù)端,只有請(qǐng)求中有Cookie,認(rèn)為是自己的客戶(hù)端,所以這個(gè)時(shí)候就出現(xiàn)了CSRF。

解決的辦法整體來(lái)說(shuō),就是兩個(gè)思路:
生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
請(qǐng)求到來(lái)時(shí),從請(qǐng)求中提取出來(lái) csrfToken,和保存的 csrfToken 做比較,進(jìn)而判斷出當(dāng)前請(qǐng)求是否合法。
1.?2021 全球程序員收入報(bào)告出爐,看完我真的拖后腿了。。
2.?Caffine Cache 在 SpringBoot 中的使用
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號(hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過(guò)內(nèi)容,記得讀完點(diǎn)一下“在看”,加個(gè)“星標(biāo)”,這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。
點(diǎn)“在看”支持小哈呀,謝謝啦??!

