官方文檔參考,5.1.2 中文參考文檔,4.1 中文參考文檔,4.1 官方文檔中文翻譯與源碼解讀css
SpringSecurity 核心功能:html
pom 依賴前端
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.woodwhale.king</groupId> <artifactId>security-demo</artifactId> <version>1.0.0</version> <name>security-demo</name> <description>spring-security-demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
編寫一個最簡單的用戶 controllerjava
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @GetMapping public String getUsers() { return "Hello Spring Security"; } }
application.yml 配置IP 和端口mysql
server: address: 127.0.0.1 port: 8081 logging: level: org.woodwhale.king: DEBUG
瀏覽器訪問http://127.0.0.1:8081/user,瀏覽器被自動重定向到了登陸的界面:git
這個/login
訪問路徑在程序中沒有任何的顯示代碼編寫,爲何會出現這樣的界面呢,當前界面中的UI 都是哪裏來的呢?github
固然是 spring-security 進行了默認控制,從啓動日誌中,能夠看到一串用戶名默認爲user
的默認密碼:web
登陸成功以後,能夠正常訪問服務資源了。spring
在配置文件配置用戶名和密碼:sql
spring: security: user: name: "admin" password: "admin"
舊版的 spring security 關閉默認安全訪問控制,只須要在配置文件中關閉便可:
security.basic.enabled = false
新版本 Spring-Boot2.xx(Spring-security5.x) 的再也不提供上述配置了:
方法1: 將 security 包從項目依賴中去除。
方法2:將org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
不注入spring中:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @SpringBootApplication @EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class}) public class SecurityDemoApplication { public static void main(String[] args) { SpringApplication.run(SecurityDemoApplication.class, args); } }
方法3:己實現一個配置類繼承自WebSecurityConfigurerAdapter
,並重寫configure(HttpSecurity http)
方法:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").permitAll(); } /** * 配置一個userDetailsService Bean * 再也不生成默認security.user用戶 */ @Bean @Override protected UserDetailsService userDetailsService() { return super.userDetailsService(); } }
注意:WebSecurityConfigurerAdapter
是一個適配器類,因此爲了使自定義的配置類見名知義,因此寫成了WebSecurityConfig
。同時增長了@EnableWebSecurity
註解到了 spring security 中。
springsucrity 的自定義用戶認證配置的核心均在上述的WebSecurityConfigurerAdapter
類中,用戶想要個性化的用戶認證邏輯,就須要本身寫一個自定義的配置類,適配到 spring security 中:
注意:若是配置了兩個以上的自定義實現類,那麼就會報WebSecurityConfigurers
不惟一的錯誤:java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique.
@Configuration @EnableWebSecurity public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 定義當須要提交表單進行用戶登陸時候,轉到的登陸頁面。 .and() .authorizeRequests() // 定義哪些URL須要被保護、哪些不須要被保護 .anyRequest() // 任何請求,登陸後能夠訪問 .authenticated(); } }
將用戶名密碼設置到內存中,用戶登陸的時候會校驗內存中配置的用戶名和密碼:
在舊版本的 spring security 中,在上述自定義的BrowerSecurityConfig
中配置以下代碼便可:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); }
可是在新版本中,啓動運行都沒有問題,一旦用戶正確登陸的時候,會報異常:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
由於在 Spring security 5.0 中新增了多種加密方式,也改變了密碼的格式。官方文檔說明:Password Storage Format
上面這段話的意思是,如今新的 Spring Security 中對密碼的存儲格式是"{id}……"
。前面的 id
是加密方式,id 能夠是bcrypt
、sha256
等,後面緊跟着是使用這種加密類型進行加密後的密碼。
所以,程序接收到內存或者數據庫查詢到的密碼時,首先查找被{}
包括起來的id
,以肯定後面的密碼是被什麼加密類型方式進行加密的,若是找不到就認爲 id 是 null。這也就是爲何程序會報錯:There is no PasswordEncoder mapped for the id "null"
。官方文檔舉的例子中是各類加密方式針對同一密碼加密後的存儲形式,原始密碼都是"password"。
要想咱們的項目還可以正常登錄,須要將前端傳過來的密碼進行某種方式加密,官方推薦的是使用bcrypt
加密方式(不用用戶使用相同原密碼生成的密文是不一樣的),所以須要在 configure 方法裏面指定一下:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); auth.inMemoryAuthentication() .passwordEncoder(new BCryptPasswordEncoder()) .withUser("admin") .password(new BCryptPasswordEncoder().encode("admin")) .roles("ADMIN"); }
固然還有一種方法,將passwordEncoder
配置抽離出來:
@Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin") .password(new BCryptPasswordEncoder().encode("admin")) .roles("ADMIN"); }
這裏還有一種更優雅的方法,實現org.springframework.security.core.userdetails.UserDetailsService
接口,重載loadUserByUsername(String username)
方法,當用戶登陸時,會調用UserDetailsService
接口的loadUserByUsername()
來校驗用戶的合法性(密碼和權限)。
這種方法爲以後結合數據庫或者JWT動態校驗打下技術可行性基礎。
@Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ADMIN")); return new User("root", new BCryptPasswordEncoder().encode("root"), authorities); } }
固然,"自定義到內存"中的配置文件中的configure(AuthenticationManagerBuilder auth)
配置就不須要再配置一遍了。
注意:對於返回的UserDetails
實現類,可使用框架本身的 User,也能夠本身實現一個 UserDetails 實現類,其中密碼和權限都應該從數據庫中讀取出來,而不是寫死在代碼裏。
將加密類型抽離出來,實現UserDetailsService
接口,將二者注入到AuthenticationManagerBuilder
中:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } }
UserDetailsService
接口實現類:
import java.util.ArrayList; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ADMIN")); return new User("root", new BCryptPasswordEncoder().encode("root"), authorities); } }
這裏的 User 對象是框架提供的一個用戶對象,注意包名是:org.springframework.security.core.userdetails.User
,裏面的屬性中最核心的就是password
,username
和authorities
。
配置自定義的登陸頁面:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 定義當須要用戶登陸時候,轉到的登陸頁面。 .loginPage("/login") // 設置登陸頁面 .loginProcessingUrl("/user/login") // 自定義的登陸接口 .defaultSuccessUrl("/home").permitAll() // 登陸成功以後,默認跳轉的頁面 .and().authorizeRequests() // 定義哪些URL須要被保護、哪些不須要被保護 .antMatchers("/", "/index","/user/login").permitAll() // 設置全部人均可以訪問登陸頁面 .anyRequest().authenticated() // 任何請求,登陸後能夠訪問 .and().csrf().disable(); // 關閉csrf防禦 }
從上述配置中,能夠看出用能夠全部訪客都可以自由登陸/
和/index
進行資源訪問,同時配置了一個登陸的接口/lgoin
,使用mvc作了視圖映射(映射到模板文件目錄中的login.html
),controller 映射代碼太簡單就不贅述了,當用戶成功登陸以後,頁面會自動跳轉至/home
頁面。
上述圖片中的配置有點小小缺陷,當去掉
.loginProcessUrl()
的配置的時候,登陸完畢,瀏覽器會一直重定向,直至報重定向失敗。由於登陸成功的 url 沒有配置成全部人都可以訪問,所以形成了死循環的結果。所以,配置了登陸界面就須要配置任意可訪問:
.antMatchers("/user/login").permitAll()
login.html
代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登陸頁面</title> </head> <body> <h2>自定義登陸頁面</h2> <form action="/user/login" method="post"> <table> <tr> <td>用戶名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><button type="submit">登陸</button></td> </tr> </table> </form> </body> </html>
上述配置用戶認證過程當中,會發現資源文件也被安全框架擋在了外面,所以須要進行安全配置:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/webjars/**/*", "/**/*.css", "/**/*.js"); }
如今前端框架的靜態資源徹底能夠經過webjars
統一管理,所以注意配置/webjars/**/*
。
先後端分離的系統中,通常後端僅提供接口 JSON 格式的數據,以供前端自行調用。剛纔那樣,調用了被保護的接口,直接進行了頁面的跳轉,在web端還能夠接受,可是在 App 端就不行了, 因此咱們還須要作進一步的處理。
這裏作一下簡單的思路整理
這裏提供一種思路,核心在於運用安全框架的:RequestCache
和RedirectStrategy
import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import lombok.extern.slf4j.Slf4j; @Slf4j @RestController public class BrowserSecurityController { // 原請求信息的緩存及恢復 private RequestCache requestCache = new HttpSessionRequestCache(); // 用於重定向 private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); /** * 當須要身份認證的時候,跳轉過來 * @param request * @param response * @return */ @RequestMapping("/authentication/require") @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public String requireAuthenication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); log.info("引起跳轉的請求是:" + targetUrl); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { redirectStrategy.sendRedirect(request, response, "/login.html"); } } return "訪問的服務須要身份認證,請引導用戶到登陸頁"; } }
注意:這個/authentication/require
須要配置到安全認證配置:配置成默認登陸界面,並設置成任何人都可以訪問,而且這個重定向的頁面能夠設計成配置,從配置文件中讀取。
在先後端分離的狀況下,咱們登陸成功了可能須要向前端返回用戶的我的信息,而不是直接進行跳轉。登陸失敗也是一樣的道理。這裏涉及到了 Spring Security 中的兩個接口AuthenticationSuccessHandler
和AuthenticationFailureHandler
。自定義這兩個接口的實現,並進行相應的配置就能夠了。 固然框架是有默認的實現類的,咱們能夠繼承這個實現類再來自定義本身的業務:
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @Slf4j @Component("myAuthenctiationSuccessHandler") public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("登陸成功"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
成功登陸以後,經過 response 返回一個 JSON 字符串回去。這個方法中的第三個參數Authentication
,它裏面包含了登陸後的用戶信息(UserDetails),Session 的信息,登陸信息等。
登陸成功以後的響應JSON:
{ "authorities": [ { "authority": "ROLE_admin" } ], "details": { "remoteAddress": "127.0.0.1", "sessionId": "8BFA4F61A7CEA774C00F616AAE8C307C" }, "authenticated": true, "principal": { "password": null, "username": "admin", "authorities": [ { "authority": "ROLE_admin" } ], "accountNonExpired": true, "accountNonLocked": true, "credentialsNonExpired": true, "enabled": true }, "credentials": null, "name": "admin" }
這裏有個細節須要注意:
principal
中有個權限數組集合authorities
,裏面的權限值是:ROLE_admin
,而自定義的安全認證配置中配置的是:admin
,因此ROLE_
前綴是框架本身加的,後期取出權限集合的時候須要注意這個細節,以取決於判斷是否有權限是使用字符串的包含關係仍是等值關係。
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @Slf4j @Component("myAuthenctiationFailureHandler") public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陸失敗"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage())); } }
將兩個自定義的處理類配置到自定義配置文件中:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.woodwhale.king.handler.MyAuthenctiationFailureHandler; import org.woodwhale.king.handler.MyAuthenctiationSuccessHandler; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler; @Autowired private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() // 定義當須要用戶登陸時候,轉到的登陸頁面。 .loginPage("/login") // 設置登陸頁面 .loginProcessingUrl("/user/login") // 自定義的登陸接口 .successHandler(myAuthenctiationSuccessHandler) .failureHandler(myAuthenctiationFailureHandler) //.defaultSuccessUrl("/home").permitAll() // 登陸成功以後,默認跳轉的頁面 .and().authorizeRequests() // 定義哪些URL須要被保護、哪些不須要被保護 .antMatchers("/", "/index").permitAll() // 設置全部人均可以訪問登陸頁面 .anyRequest().authenticated() // 任何請求,登陸後能夠訪問 .and().csrf().disable(); // 關閉csrf防禦 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .passwordEncoder(new BCryptPasswordEncoder()).withUser("admin") .password(new BCryptPasswordEncoder().encode("admin")) .roles("admin"); } }
注意:defaultSuccessUrl
不須要再配置了,實測若是配置了,成功登陸的 handler 就不起做用了。
小結
能夠看出,經過自定義的登陸成功或者失敗類,進行登陸響應控制,能夠設計一個配置,以靈活適配響應返回的是頁面仍是 JSON 數據。
在前端使用了Thymeleaf
進行渲染,特使是結合Spring Security
在前端獲取用戶信息
依賴添加:
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
注意:
由於本項目使用了spring boot 自動管理版本號,因此引入的必定是徹底匹配的,若是是舊的 spring security 版本須要手動引入對應的版本。
引用官方版本引用說明:
thymeleaf-extras-springsecurity3 for integration with Spring Security 3.x thymeleaf-extras-springsecurity4 for integration with Spring Security 4.x thymeleaf-extras-springsecurity5 for integration with Spring Security 5.x
具體語法可查看:
https://github.com/thymeleaf/thymeleaf-extras-springsecurity
這裏爲了表述方便,引用了上小節中的"自定義處理登陸成功/失敗"的成功響應JSON數據:
{ "authorities": [ { "authority": "ROLE_admin" } ], "details": { "remoteAddress": "127.0.0.1", "sessionId": "8BFA4F61A7CEA774C00F616AAE8C307C" }, "authenticated": true, "principal": { "password": null, "username": "admin", "authorities": [ { "authority": "ROLE_admin" } ], "accountNonExpired": true, "accountNonLocked": true, "credentialsNonExpired": true, "enabled": true }, "credentials": null, "name": "admin" }
sec:authorize="isAuthenticated()
:判斷是否有認證經過
sec:authorize="hasRole('ROLE_ADMIN')"
判斷是否有ROLE_ADMIN
權限
注意:上述的hasRole()
標籤使用能成功的前提是:自定義用戶的權限字符集必須是以ROLE_
爲前綴的,不然解析不到,即自定義的UserDetailsService
實現類的返回用戶的權限數組列表的權限字段必須是ROLE_***
,同時在 html 頁面中注意引入對應的xmlns
,本例這裏引用了:
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
sec:authentication="principal.authorities"
:獲得該用戶的全部權限列表
sec:authentication="principal.username"
:獲得該用戶的用戶名
固然也能夠獲取更多的信息,只要UserDetailsService
實現類中返回的用戶中攜帶有的信息都可以獲取。
AuthenticationException 經常使用的的子類:(會被底層換掉,不推薦使用) UsernameNotFoundException 用戶找不到 BadCredentialsException 壞的憑據 AccountStatusException 用戶狀態異常它包含以下子類:(推薦使用) AccountExpiredException 帳戶過時 LockedException 帳戶鎖定 DisabledException 帳戶不可用 CredentialsExpiredException 證書過時
參考資料:
http://www.javashuo.com/article/p-rcacajeh-v.html
https://blog.csdn.net/canon_in_d_major/article/details/79675033
http://www.javashuo.com/article/p-zkzkkryg-cc.html
https://www.jianshu.com/p/6307c89fe3fa/
https://mp.weixin.qq.com/s/NKhwU6qKKU0Q0diA0hg13Q
https://mp.weixin.qq.com/s/sMi1__Rw_s75YDaIdmTWKw
https://blog.csdn.net/smd2575624555/article/details/82759863
http://www.javashuo.com/article/p-bafwqlds-kk.html
https://blog.csdn.net/coder_py/article/details/80330868
參考項目源碼:
https://github.com/whyalwaysmea/Spring-Security
https://github.com/oycyqr/SpringSecurity
https://github.com/chengjiansheng/cjs-springsecurity-example