萬字長文:詳解Spring Security基於表單登陸的認證模式

本文思惟導圖

圖1 思惟導圖css

原理探討

當咱們在項目中引入 Spring Security 的相關依賴後,默認的就是表單登陸形式;俗話說:「聽人勸,吃飽飯」,既然 Spring Security 已經給咱們安排的明明白白了,咱們就從表單登陸開始吧。html

在開始以前,咱們能夠站在 Spring Security 的角度上思考:若是我本身來實現表單登陸的功能,那麼我須要作哪些工做呢?前端

就我我的而言,我可能會考慮如下幾點:spring

  • 配置用戶信息,存儲如帳號、密碼等;密碼不能以明文傳輸,須要加密功能
  • 執行校驗
  • 認證成功或者失敗的處理方案

能夠簡單的製做成以下流程圖:數據庫

圖1-1 表單登陸簡單流程圖json

上方屬於咱們本身設想的實現方案,屬於"低配版"模式,下面咱們來看看 Spring Security 是怎麼作的。Spring Security的思路和咱們大同小異,優勢在於其提供了很好的封裝,提升了框架自己的可擴展性。安全

Spring Security 的實現步驟以下:app

  1. UsernamePasswordAuthenticationFilter攔截器攔截前端傳遞的表單登陸請求,將登陸信息(username、password)封裝成 UsernamePasswordAuthenticationToken,傳遞給 AuthenticationManager認證管理器
  2. AuthenticationManager認證管理器根據Token的類型遍歷獲取對應的Provider,也便是 DaoAuthenticationProvider,執行認證流程
  3. DaoAuthenticationProvider 依靠 PasswordEncoder 和 UserDetailsService對登陸請求進行驗證
  4. 驗證經過,由AuthenticationSuccessHandler 認證成功處理器進行處理
  5. 驗證失敗,由AuthenticationFailureHandler 認證失敗處理器進行處理

製做成流程圖如示:框架

圖1-2 Spring Security表單登陸認證流程圖ide

這時你可能會一臉懵逼:這咋和剛剛咱們本身設想的徹底不同呀~ 又是Manager又是Provider的;莫慌,且聽我慢慢道來。

上面出現了不少新的概念,咱們目前不須要十分細緻的瞭解它們是怎麼發揮做用的,只須要大概知道它們有什麼用的便可;具體的介紹會在下篇《認證(二):表單登陸認證流程源碼解析》娓娓道來。

  • UsernamePasswordAuthenticationFilter 表單登陸攔截器,用以捕獲前端傳遞的登陸信息(username、password),並將登陸信息封裝成某些Token。
  • AuthenticationManager 認證管理器,可簡單的理解爲分配工做的領導。DaoAuthenticationProvider DAO認證處理器,至關於被安排幹活的童鞋;從名字DAO也能夠簡單的推測出:它與數據庫中的用戶信息密不可分。
  • PasswordEncoder 密碼加密器,密碼不能明文傳輸,須要加密。UserDetailsService 用戶信息Service層,這個也很好理解,前端傳遞的登陸信息確定是有對應的數據庫實體存儲。
  • AuthenticationSuccessHandler 認證成功處理器 AuthenticationFailureHandler 認證失敗處理器。

通過上述的原理探討,咱們大致上能弄懂了整個表單登陸有哪幾個模塊須要處理;可簡單的總結爲3個模塊:

  1. 登陸前置處理:用戶信息的封裝、密碼加密器的設置
  2. 登陸中處理:登陸的校驗
  3. 登陸後置處理:登陸失敗、登陸成功的處理方案

小試牛刀

俗話說:「光說不練假把式」,那麼就讓咱們來實戰一番吧。

登陸前置處理

做爲一個Java Web項目,第一步固然是引入相關依賴;直接引入Spring Boot封裝好的starter便可。

Step-1 配置用戶信息

Spring Security 提供了UserDetails接口,用於獲取用戶的基本信息(帳號密碼、權限集合、是否鎖定等等),咱們只須要根據自身的業務場景,實現該接口便可。

Spring Security提供的UserDetails.class接口

自定義業務相關的用戶信息類,業務定義的UserInfo.class必須帶有username和password相關的信息,用於作用戶驗證;項目根據自身需求來判斷是否須要使用下面的幾個boolean方法,若是無相關需求則直接返回true便可。

@Setter
public class UserInfo implements UserDetails {
    private String username;
    private String password;
    /**
     * UserDetails的接口
     * 用戶權限集,默認須要添加ROLE_做爲前綴
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(1);
        simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        return simpleGrantedAuthorities;
    }
    /**
     * 獲取用戶密碼
     */
    @Override
    public String getPassword() {
        return this.password;
    }
    /**
     * 獲取用戶名
     */
    @Override
    public String getUsername() {
        return this.username;
    }
    /**
     * 帳戶是否未過時  --true則爲未過時
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 帳戶是否未被鎖定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 帳戶憑證是否未過時
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 帳戶是否可用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

在定義完用戶實體UserInfo後,咱們同時也須要提供對應的Service層的API方法,用以進行一些基本的操做,諸如:新增用戶、刪除用戶等。

Spring Security 也提供了對應的Service層接口,UserDetailsService,接口只有一個方法:UserDetails loadUserByUsername(String username);根據用戶名加載用戶信息.

UserDetailsService.class

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

所以咱們能夠自定義業務相關的UserInfoServiceImpl類,實現Spring Security提供的 UserDetailsService接口

UserInfoServiceImpl.class

/**
 * 用戶信息service模塊
 *
 * UserDetailsService接口爲SpringSecurity內置接口,內部有方法:
 * UserDetails loadUserByUsername(String username):如名所得 根據用戶名加載用戶
 * 該方法主要是在:DaoAuthenticationProvider中被調用,獲取用戶的信息
 *
 * @author 小奇
 */
@Slf4j
@Service
public class UserInfoServiceImpl implements UserDetailsService, UserInfoService {
    private final UserInfoDAO userInfoDAO;
    @Autowired
    public UserInfoServiceImpl(UserInfoDAO userInfoDAO) {
        this.userInfoDAO = userInfoDAO;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<UserInfo> userInfoOpt = Optional.ofNullable(userInfoDAO.loadUserByUsername(username));
        UserInfo user = userInfoOpt.orElseThrow(() -> new UsernameNotFoundException("can't not load user by username"));
        log.info("根據用戶名:{}查詢用戶成功", user.getUsername());
        return user;
    }
}

Step-2 配置密碼加密器

衆所周知,密碼是不能以明文的方式存儲的,貼心的Spring Security天然不會忘記提供加密的功能。PasswordEncoder接口,主要提供2個方法;String encode(CharSequence rawPassword)方法用於加密,由咱們在註冊用戶的時候調用;boolean matches(CharSequence rawPassword, String encodedPassword) 方法用於匹配,登陸驗證時由Spring Security框架調用。

PasswordEncoder.class

若是項目有本身的加解密方式,只須要實現該接口便可,若是沒有能夠嘗試使用Spring提供的BCryptPasswordEncoder密碼加密器。

登陸中處理

在這一塊上,咱們能夠自定義與自身業務有關的登陸邏輯判斷,目前沒有這種需求就使用Spring Security提供的默認實現便可。

登陸後置處理

登陸的後置處理分兩種狀況,第一種是登陸成功的處理,一種是登陸失敗的處理。

Step-03 配置登陸成功處理器

Spring Security提供了認證成功處理器接口AuthenticationSuccessHandler,當咱們有一些自定義的業務邏輯,諸如:用戶登陸成功後贈送積分,或者登陸成功後自動跳轉……就能夠經過提供該接口的自定義實現。

AuthenticationSuccessHandler.class

public interface AuthenticationSuccessHandler {
     /**
      * 默認方法
     */
    default void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authentication)
        throws IOException, ServletException{
            onAuthenticationSuccess(request, response, authentication);
            chain.doFilter(request, response);
    }
    /**
    * 成功後會被調用
     */
    void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response, Authentication authentication)
        throws IOException, ServletException;
}

自定義成功處理器 WebAuthenticationSuccessHandler.class

/**
 * 自定義驗證成功處理器
 * @author 小奇
 */
@Slf4j
@Component
public class WebAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        log.info("登陸成功~~");
        // 返回json 可添加自身業務邏輯  如:登陸成功後添加用戶積分等……
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

Step-04 配置登陸失敗處理器

AuthenticationFailureHandler失敗處理器和成功處理器相似,不作過多的解析,上代碼。

public interface AuthenticationFailureHandler {
    /**
     * 失敗後調用
     */
    void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException;
}

自定義失敗處理器WebAuthenticationFailureHandler.class

/**
 * 自定義驗證失敗處理器
 * @author 小奇
 */
@Slf4j
@Component
public class WebAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        log.error("登陸失敗");
        // 把exception返回給前臺
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
        // 可作其餘業務邏輯,諸如限制天天登陸失敗的次數
    }
}

Step-05 配置SecurityConfig

還記得以前咱們提過的Spring Security爲人廣爲詬病的繁瑣配置嗎?自從搭上Spring Boot的列車以後,有了翻天覆地的改變。

下面就來簡單配置一下咱們在上面自定義的一些模塊吧。

/**
 * @author kylin
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private WebAuthenticationSuccessHandler successHandler;
    @Autowired
    private WebAuthenticationFailureHandler failureHandler;
    /**
     * 密碼加密器,使用spring提供的BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
     /**
     * http請求安全配置
     *
     * @param http
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
           .authorizeRequests()
                .antMatchers("/resources/", "/css/", "/about", "/test").permitAll()
                .anyRequest().authenticated()
                .and()
           .formLogin()
                .loginPage("/login.html")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .permitAll()
                .and()
           .csrf().disable();
    }
}

整個配置就基本完成了,也比較簡單易懂;對一些配置進行基礎的講解

  1. .antMatchers("/resources/", "/css/", "/about", "/test").permitAll()是指對於這些正則路徑進行放行
  2. loginPage("/login.html")指的是自定義了一個前端的登陸頁面,固然也可使用默認的頁面(只是相對比較簡陋了些)
  3. 最後的csrf記得關閉,這一塊後面會專門介紹。

總結

以上內容則爲本文的全內容,文章經過原理探討、動手嘗試逐一展開。若有錯誤之處,請多多指正

image

相關文章
相關標籤/搜索