1. 程式人生 > >精通Spring Boot——第十八篇:自定義認證流程

精通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上,感謝您的觀看,如果有什麼錯誤的地方,還請指出,共同探討!