精通Spring Boot——第十八篇:自定義認證流程
前兩篇簡單介紹了一下使用Spring Security 使用Http Basic登入,以及Spring Security如何自定義登入邏輯。這篇文章主要介紹如何使用handler來定義認證相關的流程。 先做一些自定義的操作,如配置自定義登入頁,配置登入請求URL等。 當我們使用Spring Security時,它會為我們提供一個預設的登入頁面,這顯然沒法滿足我們的需求,那如何來自定義頁面呢?請看程式碼:
/** * @author developlee * @since 2018/11/27 21:58 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final MyLoginHandler myLoginHandler; private final MyLogoutHandler myLogoutHandler; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private MyUserDetailsService myUserDetailsService; @Autowired public SecurityConfig(MyLoginHandler myLoginHandler, MyLogoutHandler myLogoutHandler) { this.myLoginHandler = myLoginHandler; this.myLogoutHandler = myLogoutHandler; } @Override protected void configure(HttpSecurity http) throws Exception { // 自定義使用者登入頁,並允許客戶端請求 http.formLogin().loginPage("/login").permitAll() .loginProcessingUrl("/sign_in") // 配置登入成功的handler .successHandler(myLoginHandler) .and().authorizeRequests().anyRequest().authenticated(); // 配置登出的handler http.logout().addLogoutHandler(myLogoutHandler) // logout 成功,刪除 cookies .deleteCookies("web-site", "custom-token").clearAuthentication(true); // Spring Security 預設是開啟了CSRF 保護的,所以logout操作必須是用POST方式請求, // 如果非要使用GET請求來logout的話,也可以在程式碼中的實現 //.logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET")) //session管理 session失效後跳轉 http.sessionManagement().invalidSessionUrl("/login"); //只允許一個使用者登入,如果同一個賬戶兩次登入,那麼第一個賬戶將被踢下線,跳轉到登入頁面 http.sessionManagement().maximumSessions(1).expiredUrl("/login"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); auth.eraseCredentials(false); } }
接下來寫個簡單的登入頁,這個頁面我是用thymeleaf模板寫的,也是第一次使用thymeleaf,還請大家多多包涵這醜陋的畫風。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Thymeleaf-Login-Demo</title> </head> <body> <div id="header"> <h2>登入示例頁面-供參考</h2> <strong>demo login page for example</strong> </div> <div id="container"> <form th:action="@{/sign_in}" method="post"> <input name="username" type="text" placeholder="使用者名稱"/> <br/> <input name="password" type="password" placeholder="密碼"/> <br/> <input name="登入" type="submit" /> <br/> </form> </div> </body> </html>
專案啟動起來,看到的頁面效果圖如下:
結合資料庫來實現使用者登入,按照我們的思路,實現UserDetails, UserDetailsService 這兩介面。首先,讓我們自己的User實體類實現UserDetails介面. 自定義User實體類,這個類和我們的資料庫結構是對應的。
/** * @author developlee * @since 2018/11/27 21:38 */ @Entity @Table(name = "tb_users") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "age") private String age; @Column(name = "sex") private String sex; @Column(name = "isLock") private boolean isLock; @Column(name = "isEnabled") private boolean isEnabled; }
實現UserDetailsService介面,重寫loadUserByUsername方法
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從資料庫查詢使用者
User user = userRepository.findByUsername(username);
if(user == null) {
throw new UsernameNotFoundException("使用者" + username + "不存在");
}
return new MyUserDetails(user);
}
}
接下來,自定義MyUserDetails實現UserDetails介面,構造方法傳入我們從資料庫查詢出來的User物件。
/**
* @author developlee
* @since 2018/11/27 21:42
*/
public class MyUserDetails implements UserDetails {
private User user;
public MyUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return user.isLock();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
}
搞個登入成功的處理器MyLoginHandler,登入成功後,列印一行日誌,並跳轉到hello頁
@Slf4j
@Component
public class MyLoginHandler implements AuthenticationSuccessHandler {
// 登入成功處理
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
log.info("登入成功!");
httpServletResponse.sendRedirect(httpServletRequest.getContextPath().concat("/hello"));
}
}
登入試試看吧,見證奇蹟的時刻 這是資料庫中建立的使用者
生成密碼的程式碼
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void testMac() {
String password = "123456";
System.out.println("加密後密碼:" + passwordEncoder.encode(password));
}
密碼生成插入資料庫,應該在使用者註冊時進行操作。
然後,讓我們在hello頁,新增一個logout按鈕,來實現登出功能。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<p>Hello, World!</p>
<a id="logout-btn" th:href="@{/sign_out}">我要退出!</a>
</body>
</html>
點選‘我要退出!’即可跳轉到登入頁! 現在登入登出我們都已經準備就緒,接下來,為我們的登入加些料吧! 預設的登出連結是/logout,如果開啟了CSRF驗證(預設是開啟的),則該登出請求,必須設定為post請求。登出後瀏覽器跳轉路徑模式/login?logout。SecurityContextLogoutHandler 預設是作為最後的logoutHandler的。在處理登出請求中,我們可以自己新增logoutHandler或者LogoutSuccessHandler的實現。接下來請看程式碼演示: logoutHandler處理器
@Slf4j
@Component
public class MyLogoutHandler implements LogoutHandler {
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
log.info("登出成功了!!!");
authentication.setAuthenticated(false); // 設定為未授權
}
}
本文的所有程式碼我已經放在我的github.com上,感謝您的觀看,如果有什麼錯誤的地方,還請指出,共同探討!