SpringBoot淺析安全管理之Spring Security配置
在 Java 開(kāi)發(fā)領(lǐng)域常見(jiàn)的安全框架有 Shiro 和 Spring Security。Shiro 是一個(gè)輕量級(jí)的安全管理框架,提供了認(rèn)證、授權(quán)、會(huì)話管理、密碼管理、緩存管理等功能。Spring Security 是一個(gè)相對(duì)復(fù)雜的安全管理框架,功能比 Shiro 更加強(qiáng)大,權(quán)限控制細(xì)粒度更高,對(duì) OAuth 2 的支持也很友好,又因?yàn)?Spring Security 源自 Spring 家族,因此可以和 Spring 框架無(wú)縫整合,特別是 Spring Boot 中提供的自動(dòng)化配置方案,可以讓 Spring Security 的使用更加便捷。
Spring Security 的基本配置
基本用法
1. 創(chuàng)建項(xiàng)目添加依賴
創(chuàng)建一個(gè) Spring Boot 項(xiàng)目,然后添加 spring-boot-starter-security 依賴即可
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2. 添加 hello 接口
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }
3. 啟動(dòng)項(xiàng)目測(cè)試
啟動(dòng)成功后,訪問(wèn) /hello 接口就會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,這個(gè)登錄頁(yè)面是由 Spring Security 提供的
默認(rèn)的用戶名是 user ,默認(rèn)的登錄密碼則在每次啟動(dòng)項(xiàng)目時(shí)隨機(jī)生成,查看項(xiàng)目啟動(dòng)日志
Using generated security password: 4f845a17-7b09-479c-8701-48000e89d364
登錄成功后,用戶就可以訪問(wèn) /hello 接口了
配置用戶名和密碼
如果開(kāi)發(fā)者對(duì)默認(rèn)的用戶名和密碼不滿意,可以在 application.properties 中配置默認(rèn)的用戶名、密碼以及用戶角色
spring.security.user.name=tangsan
spring.security.user.password=tangsan
spring.security.user.roles=admin
基于內(nèi)存的認(rèn)證
開(kāi)發(fā)者也可以自定義類繼承自 WebSecurityConfigurer,進(jìn)而實(shí)現(xiàn)對(duì) Spring Security 更多的自定義配置,例如基于內(nèi)存的認(rèn)證,配置方式如下:
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("123123").roles("ADMIN", "USER") .and() .withUser("tangsan").password("123123").roles("USER"); } }
代碼解釋:
- 自定義 MyWebSecurityConfig 繼承自 WebSecurityConfigurerAdapter ,并重寫(xiě) configure(AuthenticationManagerBuilder auth) 方法,在該方法中配置兩個(gè)用戶,一個(gè)用戶是 admin ,具備兩個(gè)角色 ADMIN、USER;另一個(gè)用戶是 tangsan ,具備一個(gè)角色 USER
- 此處使用的 Spring Security 版本是 5.0.6 ,在 Spring Security 5.x 中引入了多種密碼加密方式,開(kāi)發(fā)者必須指定一種,此處使用 NoOpPasswordEncoder ,即不對(duì)密碼進(jìn)行加密
注意:基于內(nèi)存的用戶配置,在配置角色時(shí)不需要添加 “ROLE_” 前綴,這點(diǎn)和后面 10.2 節(jié)中基于數(shù)據(jù)庫(kù)的認(rèn)證有差別。
配置完成后,重啟項(xiàng)目,就可以使用這里配置的兩個(gè)用戶進(jìn)行登錄了。
HttpSecurity
雖然現(xiàn)在可以實(shí)現(xiàn)認(rèn)證功能,但是受保護(hù)的資源都是默認(rèn)的,而且不能根據(jù)實(shí)際情況進(jìn)行角色管理,如果要實(shí)現(xiàn)這些功能,就需要重寫(xiě) WebSecurityConfigurerAdapter 中的另一個(gè)方法
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("root").password("123123").roles("ADMIN", "DBA") .and() .withUser("admin").password("123123").roles("ADMIN", "USER") .and() .withUser("tangsan").password("123123").roles("USER"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .access("hasAnyRole('ADMIN','USER')") .antMatchers("/db/**") .access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginProcessingUrl("/login") .permitAll() .and() .csrf() .disable(); } }
代碼解釋:
- 首先配置了三個(gè)用戶,root 用戶具備 ADMIN 和 DBA 的角色,admin 用戶具備 ADMIN 和 USER 角色,tangsan 用于具備 USER 角色
- 調(diào)用 authorizeRequests() 方法開(kāi)啟 HttpSecurity 的配置,antMatchers() ,hasRole() ,access() 方法配置訪問(wèn)不同的路徑需要不同的用戶及角色
- anyRequest(),authenticated() 表示出了前面定義的之外,用戶訪問(wèn)其他的 URL 都必須認(rèn)證后訪問(wèn)
- formLogin(),loginProcessingUrl(“/login”),permitAll(),表示開(kāi)啟表單登錄,前面看到的登錄頁(yè)面,同時(shí)配置了登錄接口為 /login 即可以直接調(diào)用 /login 接口,發(fā)起一個(gè) POST 請(qǐng)求進(jìn)行登錄,登錄參數(shù)中用戶名必須命名為 username ,密碼必須命名為 password,配置 loginProcessingUrl 接口主要是方便 Ajax 或者移動(dòng)端調(diào)用登錄接口。最后還配置了 permitAll,表示和登錄相關(guān)的接口都不需要認(rèn)證即可訪問(wèn)。
配置完成后,在 Controller 中添加如下接口進(jìn)行測(cè)試:
@RestController public class HelloController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/db/hello") public String dba() { return "hello dba"; } @GetMapping("/hello") public String hello() { return "hello"; } }
根據(jù)上文配置,“/admin/hello” 接口 root 和 admin 用戶具有訪問(wèn)權(quán)限;“/user/hello” 接口 admin 和 tangsan 用戶具有訪問(wèn)權(quán)限;“/db/hello” 只有 root 用戶有訪問(wèn)權(quán)限。瀏覽器中的測(cè)試很容易,這里不再贅述。
登錄表單詳細(xì)配置
目前為止,登錄表單一直使用 Spring Security 提供的頁(yè)面,登錄成功后也是默認(rèn)的頁(yè)面跳轉(zhuǎn),但是,前后端分離已經(jīng)成為企業(yè)級(jí)應(yīng)用開(kāi)發(fā)的主流,在前后端分離的開(kāi)發(fā)方式中,前后端的數(shù)據(jù)交互通過(guò) JSON 進(jìn)行,這時(shí),登錄成功后就不是頁(yè)面跳轉(zhuǎn)了,而是一段 JSON 提示。要實(shí)現(xiàn)這些功能,只需要繼續(xù)完善上文的配置
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .access("hasAnyRole('ADMIN','USER')") .antMatchers("/db/**") .access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login_page") .loginProcessingUrl("/login") .usernameParameter("name") .passwordParameter("passwd") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException { Object principal = auth.getPrincipal(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(200); Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", principal); ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(401); Map<String, Object> map = new HashMap<>(); map.put("status", 401); if (e instanceof LockedException) { map.put("msg", "賬戶被鎖定,登錄失敗!"); } else if (e instanceof BadCredentialsException) { map.put("msg", "賬戶名或密碼輸入錯(cuò)誤,登錄失敗!"); } else if (e instanceof DisabledException) { map.put("msg", "賬戶被禁用,登錄失敗!"); } else if (e instanceof AccountExpiredException) { map.put("msg", "賬戶已過(guò)期,登錄失敗!"); } else if (e instanceof CredentialsExpiredException) { map.put("msg", "密碼已過(guò)期,登錄失敗!"); } else { map.put("msg", "登錄失敗!"); } ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .permitAll() .and() .csrf() .disable(); }
代碼解釋:
- loginPage(“/login_page”) 表示如果用戶未獲授權(quán)就訪問(wèn)一個(gè)需要授權(quán)才能訪問(wèn)的接口,就會(huì)自動(dòng)跳轉(zhuǎn)到 login_page 頁(yè)面讓用戶登錄,這個(gè) login_page 就是開(kāi)發(fā)者自定義的登錄頁(yè)面,而不再是 Spring Security 提供的默認(rèn)登錄頁(yè)
- loginProcessingUrl(“/login”) 表示登錄請(qǐng)求處理接口,無(wú)論是自定義登錄頁(yè)面還是移動(dòng)端登錄,都需要使用該接口
- usernameParameter(“name”),passwordParameter(“passwd”) 定義了認(rèn)證所需要的用戶名和密碼的參數(shù),默認(rèn)用戶名參數(shù)是 username,密碼參數(shù)是 password,可以在這里定義
- successHandler() 方法定義了登錄成功的處理邏輯。用戶登錄成功后可以跳轉(zhuǎn)到某一個(gè)頁(yè)面,也可以返回一段 JSON ,這個(gè)要看具體業(yè)務(wù)邏輯,此處假設(shè)是第二種,用戶登錄成功后,返回一段登錄成功的 JSON 。onAuthenticationSuccess 方法的第三個(gè)參數(shù)一般用來(lái)獲取當(dāng)前登錄用戶的信息,在登錄后,可以獲取當(dāng)前登錄用戶的信息一起返回給客戶端
- failureHandler 方法定義了登錄失敗的處理邏輯,和登錄成功類似,不同的是,登錄失敗的回調(diào)方法里有一個(gè) AuthenticationException 參數(shù),通過(guò)這個(gè)異常參數(shù)可以獲取登錄失敗的原因,進(jìn)而給用戶一個(gè)明確的提示
配置完成后,使用 Postman 進(jìn)行測(cè)試
如果登錄失敗也會(huì)有相應(yīng)的提示
注銷登錄配置
如果想要注銷登錄,也只需要提供簡(jiǎn)單的配置即可
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**") .hasRole("ADMIN") .antMatchers("/user/**") .access("hasAnyRole('ADMIN','USER')") .antMatchers("/db/**") .access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login_page") .loginProcessingUrl("/login") .usernameParameter("name") .passwordParameter("passwd") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException { Object principal = auth.getPrincipal(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(200); Map<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", principal); ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); resp.setStatus(401); Map<String, Object> map = new HashMap<>(); map.put("status", 401); if (e instanceof LockedException) { map.put("msg", "賬戶被鎖定,登錄失敗!"); } else if (e instanceof BadCredentialsException) { map.put("msg", "賬戶名或密碼輸入錯(cuò)誤,登錄失敗!"); } else if (e instanceof DisabledException) { map.put("msg", "賬戶被禁用,登錄失敗!"); } else if (e instanceof AccountExpiredException) { map.put("msg", "賬戶已過(guò)期,登錄失敗!"); } else if (e instanceof CredentialsExpiredException) { map.put("msg", "密碼已過(guò)期,登錄失敗!"); } else { map.put("msg", "登錄失敗!"); } ObjectMapper om = new ObjectMapper(); out.write(om.writeValueAsString(map)); out.flush(); out.close(); } }) .permitAll() .and() .logout() .logoutUrl("/logout") .clearAuthentication(true) .invalidateHttpSession(true) .addLogoutHandler(new LogoutHandler() { @Override public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) { } }) .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException { resp.sendRedirect("/login_page"); } }) .and() .csrf() .disable(); }
代碼解釋:
- logout() 表示開(kāi)啟注銷登錄的配置
- logoutUrl(“/logout”) 表示注銷登錄請(qǐng)求 URL 為 /logout ,默認(rèn)也是 /logout
- clearAuthentication(true) 表示是否清楚身份認(rèn)證信息,默認(rèn)為 true
- invalidateHttpSession(true) 表示是否使 Session 失效,默認(rèn)為 true
- addLogoutHandler 方法中完成一些數(shù)據(jù)清楚工作,例如 Cookie 的清楚
- logoutSuccessHandler 方法用于處理注銷成功后的業(yè)務(wù)邏輯,例如返回一段 JSON 提示或者跳轉(zhuǎn)到登錄頁(yè)面等
多個(gè) HttpSecurity
如果業(yè)務(wù)比較復(fù)雜,也可以配置多個(gè) HttpSecurity ,實(shí)現(xiàn)對(duì) WebSecurityConfigurerAdapter 的多次擴(kuò)展
@Configuration public class MultiHttpSecurityConfig { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Autowired protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password("123123").roles("ADMIN", "USER") .and() .withUser("tangsan").password("123123").roles("USER"); } @Configuration @Order(1) public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/admin/**").authorizeRequests() .anyRequest().hasRole("ADMIN"); } } @Configuration public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login") .permitAll() .and() .csrf() .disable(); } } }
代碼解釋:
- 配置多個(gè) HttpSecurity 時(shí),MultiHttpSecurityConfig 不需要繼承 WebSecurityConfigurerAdapter ,在 MultiHttpSecurityConfig 中創(chuàng)建靜態(tài)內(nèi)部類繼承 WebSecurityConfigurerAdapter 即可,靜態(tài)內(nèi)部類上添加 @Configuration 注解和 @Order注解,數(shù)字越大優(yōu)先級(jí)越高,未加 @Order 注解的配置優(yōu)先級(jí)最低
- AdminSecurityConfig 類表示該類主要用來(lái)處理 “/admin/**” 模式的 URL ,其它 URL 將在 OtherSecurityConfig 類中處理
密碼加密
1. 為什么要加密
略
2. 加密方案
Spring Security 提供了多種密碼加密方案,官方推薦使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 強(qiáng)哈希函數(shù),開(kāi)發(fā)者在使用時(shí)可以選擇提供 strength 和 SecureRandom 實(shí)例。strength 越大,密碼的迭代次數(shù)越多,密鑰迭代次數(shù)為 2^strength 。strength 取值在 4~31 之間,默認(rèn)為 10 。
3. 實(shí)踐
只需要修改上文配置的 PasswordEncoder 這個(gè) Bean 的實(shí)現(xiàn)即可
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); }
參數(shù) 10 就是 strength ,即密鑰的迭代次數(shù)(也可以不配置,默認(rèn)為 10)。
使用以下方式獲取加密后的密碼。
public static void main(String[] args) { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10); String encode = bCryptPasswordEncoder.encode("123123"); System.out.println(encode); }
修改配置的內(nèi)存用戶的密碼
auth.inMemoryAuthentication() .withUser("admin") .password("$2a$10$.hZESNfpLSDUnuqnbnVaF..Xb2KsAqwvzN7hN65Gd9K0VADuUbUzy") .roles("ADMIN", "USER") .and() .withUser("tangsan") .password("$2a$10$4LJ/xgqxSnBqyuRjoB8QJeqxmUeL2ynD7Q.r8uWtzOGs8oFMyLZn2") .roles("USER");
雖然 admin 和 tangsan 加密后的密碼不一樣,但是明文都是 123123 配置完成后,使用 admin/123123,或 tangsan/123123 就可以實(shí)現(xiàn)登錄,一般情況下,用戶信息是存儲(chǔ)在數(shù)據(jù)庫(kù)中的,因此需要用戶注冊(cè)時(shí)對(duì)密碼進(jìn)行加密處理
@Service public class RegService { public int reg(String username, String password) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); String encodePasswod = encoder.encode(password); return saveToDb(username, encodePasswod); } private int saveToDb(String username, String encodePasswod) { // 業(yè)務(wù)處理 return 0; } }
用戶將密碼從前端傳來(lái)之后,通過(guò) BCryptPasswordEncoder 實(shí)例中的 encode 方法對(duì)密碼進(jìn)行加密處理,加密完成后將密文存入數(shù)據(jù)庫(kù)。
方法安全
上文介紹的認(rèn)證和授權(quán)都是基于 URL 的,開(kāi)發(fā)者也可通過(guò)注解來(lái)靈活配置方法安全,使用相關(guān)注解,首先要通過(guò) @EnableGlobalMethodSecurity 注解開(kāi)啟基于注解的安全配置
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) public class MultiHttpSecurityConfig{ }
代碼解釋:
- prePostEnabled = true 會(huì)解鎖 @PreAuthorize 和 @PostAuthorize 兩個(gè)注解, @PreAuthorize 注解會(huì)在方法執(zhí)行前進(jìn)行驗(yàn)證,而 @PostAuthorize 注解在方法執(zhí)行后進(jìn)行驗(yàn)證
- securedEnabled = true 會(huì)解鎖 @Secured 注解
開(kāi)啟注解安全后,創(chuàng)建一個(gè) MethodService 進(jìn)行測(cè)試
@Service public class MethodService { @Secured("ROLE_ADMIN") public String admin() { return "hello admin"; } @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')") public String dba() { return "hello dba"; } @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')") public String user() { return "user"; } }
代碼解釋:
- @Secured(“ROLE_ADMIN”) 注解表示訪問(wèn)該方法需要 ADMIN 角色,注意這里需要在角色前加一個(gè)前綴 ROLE_
- @PreAuthorize(“hasRole(‘ADMIN’) and hasRole(‘DBA’)”) 注解表示訪問(wèn)該方法既需要 ADMIN 角色又需要 DBA 角色
- @PreAuthorize(“hasAnyRole(‘ADMIN’,‘DBA’,‘USER’)”) 表示訪問(wèn)該方法需要 ADMIN 、DBA 或 USER 角色中至少一個(gè)
- @PostAuthorize 和 @PreAuthorize 中都可以使用基于表達(dá)式的語(yǔ)法
最后在 Controller 中注入 Service 并調(diào)用 Service 中的方法進(jìn)行測(cè)試
@RestController public class HelloController { @Autowired MethodService methodService; @GetMapping("/hello") public String hello() { String user = methodService.user(); return user; } @GetMapping("/hello2") public String hello2() { String admin = methodService.admin(); return admin; } @GetMapping("/hello3") public String hello3() { String dba = methodService.dba(); return dba; } }
admin 訪問(wèn) hello
admin 訪問(wèn) hello2
admin 訪問(wèn) hello3
到此這篇關(guān)于SpringBoot淺析安全管理之Spring Security配置的文章就介紹到這了,更多相關(guān)SpringBoot Spring Security內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring中@Autowired和@Resource注解的使用區(qū)別詳解
這篇文章主要介紹了Spring中@Autowired和@Resource注解的使用區(qū)別詳解,@Autowired默認(rèn)根據(jù)type進(jìn)行注入,找到與指定類型兼容的?Bean?并進(jìn)行注入,如果無(wú)法通過(guò)type匹配到對(duì)應(yīng)的?Bean?的話,會(huì)根據(jù)name進(jìn)行匹配,如果都匹配不到則拋出異常,需要的朋友可以參考下2023-11-11Java可以如何實(shí)現(xiàn)文件變動(dòng)的監(jiān)聽(tīng)的示例
本篇文章主要介紹了Java可以如何實(shí)現(xiàn)文件變動(dòng)的監(jiān)聽(tīng)的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02Spring中如何使用@Value注解實(shí)現(xiàn)給Bean屬性賦值
這篇文章主要介紹了Spring中如何使用@Value注解實(shí)現(xiàn)給Bean屬性賦值的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08使用filter實(shí)現(xiàn)url級(jí)別內(nèi)存緩存示例
這篇文章主要介紹了使用filter實(shí)現(xiàn)url級(jí)別內(nèi)存緩存示例,只需要一個(gè)靜態(tài)類,在filter中調(diào)用,也可以全部寫(xiě)到filt里面。可以根據(jù)查詢參數(shù)分別緩存,需要的朋友可以參考下2014-03-03用Java實(shí)現(xiàn)簡(jiǎn)單ATM機(jī)功能
這篇文章主要為大家詳細(xì)介紹了用Java實(shí)現(xiàn)簡(jiǎn)單ATM機(jī)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01Java數(shù)據(jù)結(jié)構(gòu)之棧的基本定義與實(shí)現(xiàn)方法示例
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之棧的基本定義與實(shí)現(xiàn)方法,簡(jiǎn)單描述了數(shù)據(jù)結(jié)構(gòu)中棧的功能、原理,并結(jié)合java實(shí)例形式分析了棧的基本定義與使用方法,需要的朋友可以參考下2017-10-10關(guān)于@Component注解下的類無(wú)法@Autowired問(wèn)題
這篇文章主要介紹了關(guān)于@Component注解下的類無(wú)法@Autowired問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03java中javamail收發(fā)郵件實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了java中javamail收發(fā)郵件實(shí)現(xiàn)方法,實(shí)例分析了javamail的使用方法與相關(guān)注意事項(xiàng),非常具有實(shí)用價(jià)值,感興趣的小伙伴們可以參考一下2016-02-02