在 Spring Boot 集成 Spring Security 這篇文章中,咱們介紹瞭如何在 Spring Boot 項目中快速集成 Spring Security,同時也介紹瞭如何更改系統默認生成的用戶名和密碼。接下來本文將基於 Spring Boot 集成 Spring Security 這篇文章中所建立的項目,進一步介紹在 Spring Security 中如何實現自定義用戶認證。html
閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 —— 全棧修仙之路
本項目所使用的開發環境及主要框架版本:前端
<?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 https://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.2.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.semlinker</groupId> <artifactId>custom-user-authentication</artifactId> <version>0.0.1-SNAPSHOT</version> <name>custom-user-authentication</name> <description>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.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 省略spring-boot-starter-test、spring-security-test及spring-boot-devtools --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
首先建立一個 MyUser 類,用於存儲模擬的用戶信息(實際開發中通常從數據庫中獲取真實的用戶信息):java
// com/semlinker/domain/MyUser.java @Data public class MyUser implements Serializable { private static final long serialVersionUID = -1090551705063344205L; private String userName; private String password; private boolean accountNonExpired = true; // 表示帳號是否未過時 private boolean accountNonLocked = true; // 表示帳號是否未鎖定 private boolean credentialsNonExpired = true; // 表示用戶憑證未過時,好比用戶密碼 private boolean enabled = true; // 表示用戶是否啓用 }
接着配置 PasswordEncoder 對象,顧名思義該對象用於密碼加密。在下面的 UserDetailsService 服務中須要用到此對象,所以這裏咱們須要提早作好配置。PasswordEncoder
是一個密碼加密接口,在 Spring Security 中有許多實現類,好比 BCryptPasswordEncoder、Pbkdf2PasswordEncoder 和 LdapShaPasswordEncoder 等。git
固然咱們也能夠自定義 PasswordEncoder,但 Spring Security 中實現的 BCryptPasswordEncoder 功能已經足夠強大,它對相同的密碼進行加密後能夠生成不一樣的結果,這樣就大大提升了系統的安全性。即儘管系統中使用相同密碼的某些用戶不當心泄露了密碼,也不會致使其餘用戶密碼泄露。既然 BCryptPasswordEncoder 功能那麼強大,咱們確定直接使用它,具體的配置方式以下:github
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
自定義 UserDetailsService 服務,須要實現 UserDetailsService 接口,該接口只包含一個 loadUserByUsername 方法,用於經過 username 來加載匹配的用戶。當找不到 username 對應用戶時,會拋出 UsernameNotFoundException 異常。UserDetailsService 接口的定義以下:web
// org/springframework/security/core/userdetails/UserDetailsService.java public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
loadUserByUsername 方法返回 UserDetails 對象,這裏的 UserDetails 也是一個接口,它的定義以下:spring
// org/springframework/security/core/userdetails/UserDetails.java public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
顧名思義,UserDetails 表示詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。以上方法的具體做用以下:數據庫
介紹完上述內容,下面咱們來建立一個 MyUserDetailsService 類並實現 UserDetailsService 接口,具體以下:apache
// com/semlinker/service/MyUserDetailsService.java @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { MyUser myUser = new MyUser(); myUser.setUserName(username); myUser.setPassword(this.passwordEncoder.encode("hello")); // 使用Spring Security內部UserDetails的實現類User,來建立User對象 return new User(username, myUser.getPassword(), myUser.isEnabled(), myUser.isAccountNonExpired(), myUser.isCredentialsNonExpired(), myUser.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
在 Spring Security 中使用咱們自定義的 MyUserDetailsService,還須要在 WebSecurityConfig 類中進行配置:json
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean UserDetailsService myUserDetailService() { return new MyUserDetailsService(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder()); } }
在以上 configure 方法中,咱們配置了自定義的 MyUserDetailsService 和 PasswordEncoder 對象。
在 Spring Security 中 DefaultLoginPageGeneratingFilter 過濾器會爲咱們生成默認登陸界面:
相信不少小夥伴都 「看不慣」 這個頁面,下面咱們就來對這個頁面進行 「整容」。
// com/semlinker/controller/HomeController.java @Controller public class HomeController { @GetMapping("/") public String index() { return "index"; } }
// com/semlinker/controller/UserController.java @Controller public class UserController { @GetMapping("/login") public String login() { return "login"; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Semlinker修仙之路首頁 </title> </head> <body> <h3>歡迎您來到Semlinker修仙之路首頁</h3> </body> </html>
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Semlinker修仙之路登陸頁</title> </head> <body> <form class="login-form" method="post" action="/login"> <h1>Login</h1> <div class="form-field"> <i class="fas fa-user"></i> <input type="text" name="username" id="username" class="form-field" placeholder=" " required> <label for="username">Username</label> </div> <div class="form-field"> <i class="fas fa-lock"></i> <input type="password" name="password" id="password" class="form-field" placeholder=" " required> <label for="password">Password</label> </div> <button type="submit" value="Login" class="btn">Login</button> </form> </body> </html>
在建立完登陸頁以後,還須要在 WebSecurityConfig 類中進行配置才能生效,對應的配置方式以下:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 省略前面已設置的內容 protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login"); } }
完成上述配置後,咱們來測試一下效果,首先啓動 Spring Boot 應用,待啓動完成後在瀏覽器中打開 http://localhost:8080/login 地址,若一切順利的話,你將看到如下界面:
(頁面來源於 https://codepen.io/alphardex/...)
接下來咱們來執行登陸操做,這裏的用戶名能夠是任意的,密碼是前面咱們所設置的 hello。但當咱們輸入正確的用戶名和密碼點擊登陸以後,映入眼簾的倒是如下的異常頁面:
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon Oct 28 14:27:25 CST 2019 There was an unexpected error (type=Forbidden, status=403). Forbidden
這是什麼緣由呢?爲啥被禁止訪問了,小夥伴們先別急,首先打開當前項目 src/main/resources/
目錄下的 application.properties 文件,而後輸入如下配置信息:
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
待完成配置以後,重啓一下應用,而後從新執行一次上述的登陸操做。若是沒猜錯的話,你從新執行登陸,輸入的用戶名和密碼也沒有錯,但仍看見 Whitelabel Error Page 頁面。其實剛纔咱們已經啓用的 Security FilterChainProxy 的 DEBUG 調試模式,因此咱們來看一下控制檯輸出的異常信息:
經過上圖能夠發現 /login
請求,通過 CsrfFilter 過濾器就再也不往下繼續執行了。這裏的 CsrfFilter 過濾器是用來處理跨站請求僞造攻擊的過濾器,跨站請求僞造(英語:Cross-site request forgery),也被稱爲 one-click attack 或者 session riding,一般縮寫爲 CSRF 或者 XSRF, 是一種挾制用戶在當前已登陸的 Web 應用程序上執行非本意的操做的攻擊方法。
如今咱們已經大體知道緣由了,因爲咱們的登陸頁暫不須要開啓 Csrf 防護,因此咱們先把 Csrf 過濾器禁用掉:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") .and().csrf().disable(); } }
更新完 WebSecurityConfig 配置類,再從新跑一次前面的登陸流程,此次當你點擊登陸以後,你將會在當前頁面看到歡迎您來到Semlinker修仙之路首頁這行內容。
默認狀況下,當用戶經過瀏覽器訪問被保護的資源時,會默認自動重定向到預設的登陸地址。這對於傳統的 Web 項目來講,是沒有多大問題,但這種方式就不適用於先後端分離的項目。對於先後端分離的項目,服務端通常只須要對外提供返回 JSON 格式的 API 接口。
針對上述的問題,有以下一種方案可供參考。即根據請求是否以 .html
爲結尾來對應不一樣的處理方法。若是是以 .html
結尾,那麼重定向到登陸頁面,不然返回 」訪問的資源須要身份認證!」 信息,而且 HTTP 狀態碼爲401(HttpStatus.UNAUTHORIZED
)。
要實現上述的功能,咱們先來定義一個 WebSecurityController 類,具體實現以下:
// com/semlinker/controller/WebSecurityController.java @Slf4j @RestController public class WebSecurityController { // 原請求信息的緩存及恢復 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 Exception { 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 "訪問的服務須要身份認證,請引導用戶到登陸頁"; } }
接着將 formLogin 的默認登陸頁,修改成 /authentication/require
,並經過 antMatchers 方法設置免攔截:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .and() .authorizeRequests() .antMatchers("/authentication/require", "/login.html").permitAll() .anyRequest().authenticated() .and().csrf().disable() ; } }
同時也要修改一下前面定義的 UserController 類,讓其支持 /login.html
路徑映射:
// com/semlinker/controller/UserController.java @Controller public class UserController { @GetMapping({"login", "/login.html"}) public String login() { return "login"; } }
完成上述調整後,到咱們訪問 http://localhost:8080/index 的時候,頁面會自動跳轉到 http://localhost:8080/authentication/require,而且輸出 "訪問的服務須要身份認證,請引導用戶到登陸頁"。而當咱們訪問 http://localhost:8080/index.html 的時候,頁面會跳轉到登陸頁面。
在先後端分離項目中,當用戶登陸成功或登陸失敗時,須要向前端返回相應的信息,而不是直接進行頁面跳轉。針對先後端分離的場景,能夠利用 Spring Security 中的 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
這兩個接口或繼承 SimpleUrlAuthenticationSuccessHandler
或 SimpleUrlAuthenticationFailureHandler
類來實現自定義登陸成功和登陸失敗的處理邏輯。
這裏咱們選用繼承 SimpleUrlAuthenticationSuccessHandler
類,來實現自定義登陸成功處理邏輯:
// com/semlinker/handler/MyAuthenctiationSuccessHandler.java @Slf4j @Component 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)); } }
一樣咱們也選用繼承 SimpleUrlAuthenticationFailureHandler
類,來實現自定義登陸失敗處理邏輯:
// com/semlinker/handler/MyAuthenctiationFailureHandler.java @Slf4j @Component 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())); } }
最後要讓自定義處理登陸成功和失敗邏輯生效,還須要在 WebSecurityConfig 類中配置 FormLoginConfigurer 對象的 successHandler 和 failureHandler 屬性,到目前爲止 WebSecurityConfig 類的完整配置以下:
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler; @Autowired private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean UserDetailsService myUserDetailService() { return new MyUserDetailsService(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder()); } protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") .successHandler(myAuthenctiationSuccessHandler) .failureHandler(myAuthenctiationFailureHandler) .and() .authorizeRequests() .antMatchers("/authentication/require", "/login").permitAll() .anyRequest().authenticated() .and().csrf().disable() ; } }
前面本文已經介紹了在 Spring Security 中實現自定義用戶認證的流程,在學習過程當中若是小夥伴們遇到其它問題的話,建議能夠開啓 FilterChainProxy
的 DEBUG 模式進行日誌排查。
本文項目地址: Github - custom-user-authentication