中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring?Security如何實現登錄驗證

發布時間:2022-01-10 14:23:21 來源:億速云 閱讀:350 作者:iii 欄目:開發技術

這篇文章主要講解了“Spring Security如何實現登錄驗證”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Spring Security如何實現登錄驗證”吧!

一、理論知識

我們先思考一下這個流程大致是如何的?

  • 填寫郵件號碼,獲取驗證碼

  • 輸入獲取到的驗證碼進行登錄(登錄的接口:/email/login,這里不能使用默認的/login,因為我們是擴展)

  • 在自定義的過濾器 EmailCodeAuthenticationFilter 中獲取發送過來的郵件號碼及驗證碼,判斷驗證碼是否正確,郵件賬號是否為空等

  • 封裝成一個需要認證的 Authentication ,此處我們自定義實現為 EmailCodeAuthenticationToken。

  • 將 Authentiction 傳給 AuthenticationManager 接口中 authenticate 方法進行認證處理

  • AuthenticationManager 默認是實現類為 ProviderManager ,ProviderManager 又委托給 AuthenticationProvider 進行處理

  • 我們自定義一個 EmailCodeAuthenticationProvider 實現 AuthenticationProvider ,實現身份驗證。

  • 自定義的 EmailCodeAuthenticationFilter 繼承了 AbstractAuthenticationProcessingFilter 抽象類, AbstractAuthenticationProcessingFilter 在 successfulAuthentication 方法中對登錄成功進行了處理,通過 SecurityContextHolder.getContext().setAuthentication() 方法將 Authentication 認證信息對象綁定到 SecurityContext即安全上下文中。

  • 其實對于身份驗證通過后的處理,有兩種方案,一種是直接在過濾器重寫successfulAuthentication,另外一種就是實現AuthenticationSuccessHandler來處理身份驗證通過。

  • 身份驗證失敗也是一樣,可重寫unsuccessfulAuthentication方法,也可以實現 AuthenticationFailureHandler來對身份驗證失敗進行處理。

大致流程就是如此。從這個流程中我們可以知道,需要重寫的組件有以下幾個:

  • EmailCodeAuthenticationFilter:郵件驗證登錄過濾器

  • EmailCodeAuthenticationToken:身份驗證令牌

  • EmailCodeAuthenticationProvider:郵件身份認證處理

  • AuthenticationSuccessHandler:處理登錄成功操作

  • AuthenticationFailureHandler:處理登錄失敗操作

接下來,我是模仿著源碼寫出我的代碼,建議大家可以在使用的時候,多去看看,我這里去除了一些不是和這個相關的代碼。

來吧!!

二、EmailCodeAuthenticationFilter

我們需要重寫的 EmailCodeAuthenticationFilter,實際繼承了AbstractAuthenticationProcessingFilter抽象類,我們不會寫,可以先看看它的默認實現UsernamePasswordAuthenticationFilter是怎么樣的嗎,抄作業這是大家的強項的哈。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
            "POST");
    //從前臺傳過來的參數
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    private boolean postOnly = true;
    
    //  初始化一個用戶密碼 認證過濾器  默認的登錄uri 是 /login 請求方式是POST
    public UsernamePasswordAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    /**
    執行實際身份驗證。實現應執行以下操作之一:
    1、為經過身份驗證的用戶返回填充的身份驗證令牌,表示身份驗證成功
    2、返回null,表示認證過程還在進行中。 在返回之前,實現應該執行完成流程所需的任何額外工作。
    3、如果身份驗證過程失敗,則拋出AuthenticationException
    */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        //生成 UsernamePasswordAuthenticationToken 稍后交由AuthenticationManager中的authenticate進行認證
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 可以放一些其他信息進去
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    //set、get方法
}

接下來我們就抄個作業哈:

package com.crush.security.auth.email_code;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;

/**
 * @Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
    /**
     * 前端傳來的 參數名 - 用于request.getParameter 獲取
     */
    private final String DEFAULT_EMAIL_NAME="email";

    private final String DEFAULT_EMAIL_CODE="e_code";

    @Autowired
    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }
    /**
     * 是否 僅僅post方式
     */
    private boolean postOnly = true;

    /**
     * 通過 傳入的 參數 創建 匹配器
     * 即 Filter過濾的url
     */
    public EmailCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/email/login","POST"));
    }


    /**
     * filter 獲得 用戶名(郵箱) 和 密碼(驗證碼) 裝配到 token 上 ,
     * 然后把token 交給 provider 進行授權
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if(postOnly && !request.getMethod().equals("POST") ){
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }else{
            String email = getEmail(request);
            if(email == null){
                email = "";
            }
            email = email.trim();
            //如果 驗證碼不相等 故意讓token出錯 然后走springsecurity 錯誤的流程
            boolean flag = checkCode(request);
            //封裝 token
            EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,new ArrayList<>());
            this.setDetails(request,token);
            //交給 manager 發證
            return this.getAuthenticationManager().authenticate(token);
        }
    }

    /**
     * 獲取 頭部信息 讓合適的provider 來驗證他
     */
    public void setDetails(HttpServletRequest request , EmailCodeAuthenticationToken token ){
        token.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    /**
     * 獲取 傳來 的Email信息
     */
    public String getEmail(HttpServletRequest request ){
        String result=  request.getParameter(DEFAULT_EMAIL_NAME);
        return result;
    }

    /**
     * 判斷 傳來的 驗證碼信息 以及 session 中的驗證碼信息
     */
    public boolean checkCode(HttpServletRequest request ){
        String code1 = request.getParameter(DEFAULT_EMAIL_CODE);
        System.out.println("code1**********"+code1);
        // TODO 另外再寫一個鏈接 生成 驗證碼 那個驗證碼 在生成的時候  存進redis 中去
        //TODO  這里的驗證碼 寫在Redis中, 到時候取出來判斷即可 驗證之后 刪除驗證碼
        if(code1.equals("123456")){
            return true;
        }
        return false;
    }
	// set、get方法...
}

三、EmailCodeAuthenticationToken

我們EmailCodeAuthenticationToken是繼承AbstractAuthenticationToken的,按照同樣的方式,我們接著去看看AbstractAuthenticationToken的默認實現是什么樣的就行了。

/**

 */
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 這里指的賬號密碼哈
    private final Object principal;

    private Object credentials;

    /**
    沒經過身份驗證時,初始化權限為空,setAuthenticated(false)設置為不可信令牌
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
    經過身份驗證后,將權限放進去,setAuthenticated(true)設置為可信令牌
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }

}

日常抄作業哈:

/**
 * @Author: crush
 * @Date: 2021-09-08 21:13
 * version 1.0
 */
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {


    /**
     * 這里的 principal 指的是 email 地址(未認證的時候)
     */
    private final Object principal;

    public EmailCodeAuthenticationToken(Object principal) {
        super((Collection) null);
        this.principal = principal;
        setAuthenticated(false);
    }

    public EmailCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

}

這個很簡單的哈

四、EmailCodeAuthenticationProvider

自定義的EmailCodeAuthenticationProvider是實現了AuthenticationProvider接口,抄作業就得學會看看源碼。我們接著來。

4.1、先看看AbstractUserDetailsAuthenticationProvider,我們再來模仿

AuthenticationProvider 接口有很多實現類,不一一說明了,直接看我們需要看的AbstractUserDetailsAuthenticationProvider, 該類旨在響應 UsernamePasswordAuthenticationToken 身份驗證請求。但是它是一個抽象類,但其實就一個步驟在它的實現類中實現的,很簡單,稍后會講到。

在這個源碼中我把和檢查相關的一些操作都給刪除,只留下幾個重點,我們一起來看一看哈。

//該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求。
public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    protected final Log logger = LogFactory.getLog(getClass());

    private UserCache userCache = new NullUserCache();

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        //獲取用戶名
        String username = determineUsername(authentication);
        //判斷緩存中是否存在
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                // 緩存中沒有 通過字類實現的retrieveUser 從數據庫進行檢索,返回一個 UserDetails 對象
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException ex) {
                this.logger.debug("Failed to find user '" + username + "'");
                if (!this.hideUserNotFoundExceptions) {
                    throw ex;
                }
                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            //進行相關檢查  因為可能是從緩存中取出來的 并非是最新的
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException ex) {
            if (!cacheWasUsed) {
                throw ex;
            }
            // 沒有通過檢查, 重新檢索最新的數據
            cacheWasUsed = false;
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        // 再次進行檢查
        this.postAuthenticationChecks.check(user);
        // 存進緩存中去
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //創建一個可信的身份令牌返回
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    private String determineUsername(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }

    /**
簡而言之就是創建了一個通過身份驗證的UsernamePasswordAuthenticationToken
     */
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }


    /**
允許子類從特定于實現的位置實際檢索UserDetails ,如果提供的憑據不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個UserDetails )
     */
    protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;
    //...
    

    //簡而言之:當然有時候我們有多個不同的 `AuthenticationProvider`,它們分別支持不同的 `Authentication`對象,那么當一個具體的 `AuthenticationProvier`傳進入 `ProviderManager`的內部時,就會在 `AuthenticationProvider`列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證
    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }

}

關于 protected abstract UserDetails retrieveUser 的實現,AbstractUserDetailsAuthenticationProvider實現是DaoAuthenticationProvider.

DaoAuthenticationProvider主要操作是兩個,第一個是從數據庫中檢索出相關信息,第二個是給檢索出的用戶信息進行密碼的加密操作。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    private UserDetailsService userDetailsService;
    
    @Override
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            // 檢索用戶,一般我們都會實現 UserDetailsService接口,改為從數據庫中檢索用戶信息 返回安全核心類 UserDetails
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    @Override
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        // 判斷是否用了密碼加密 針對這個點 沒有深入 大家好奇可以去查一查這個知識點
        boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }

}

4.2、抄作業啦

看完源碼,其實我們如果要重寫的話,主要要做到以下幾個事情:

重寫public boolean supports(Class<?> authentication)方法。

有時候我們有多個不同的 AuthenticationProvider,它們分別支持不同的 Authentication對象,那么當一個具體的 AuthenticationProvier 傳進入 ProviderManager的內部時,就會在 AuthenticationProvider列表中挑選其對應支持的 provider 對相應的 Authentication對象進行驗證

簡單說就是指定AuthenticationProvider驗證哪個 Authentication 對象。如指定DaoAuthenticationProvider認證UsernamePasswordAuthenticationToken,

所以我們指定EmailCodeAuthenticationProvider認證EmailCodeAuthenticationToken。

檢索數據庫,返回一個安全核心類UserDetail。

創建一個經過身份驗證的Authentication對象

了解要做什么事情了,我們就可以動手看看代碼啦。

/**
 * @Author: crush
 * @Date: 2021-09-08 21:14
 * version 1.0
 */
@Slf4j
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {

    ITbUserService userService;

    public EmailCodeAuthenticationProvider(ITbUserService userService) {
        this.userService = userService;
    }


    /**
     * 認證
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!supports(authentication.getClass())) {
            return null;
        }
        log.info("EmailCodeAuthentication authentication request: %s", authentication);
        EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;

        UserDetails user = userService.getByEmail((String) token.getPrincipal());

        System.out.println(token.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("無法獲取用戶信息");
        }
        System.out.println(user.getAuthorities());
        EmailCodeAuthenticationToken result =
                new EmailCodeAuthenticationToken(user, user.getAuthorities());
                /*
                Details 中包含了 ip地址、 sessionId 等等屬性 也可以存儲一些自己想要放進去的內容
                */
        result.setDetails(token.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return EmailCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

五、在配置類中進行配置

主要就是做下面幾件事:將過濾器、認證器注入到spring中
將登錄成功處理、登錄失敗處理器注入到Spring中,或者在自定義過濾器中對登錄成功和失敗進行處理。
添加到過濾鏈中

    @Bean
    public EmailCodeAuthenticationFilter emailCodeAuthenticationFilter() {
        EmailCodeAuthenticationFilter emailCodeAuthenticationFilter = new EmailCodeAuthenticationFilter();
        emailCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        emailCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        return emailCodeAuthenticationFilter;
    }

    @Bean
    public EmailCodeAuthenticationProvider emailCodeAuthenticationProvider() {
        return new EmailCodeAuthenticationProvider(userService);
    }

    /**
     * 因為使用了BCryptPasswordEncoder來進行密碼的加密,所以身份驗證的時候也的用他來判斷哈、,
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        //authenticationProvider 根據傳入的自定義AuthenticationProvider添加身份AuthenticationProvider 。
        auth.authenticationProvider(emailCodeAuthenticationProvider());
    }
.and()
    .authenticationProvider(emailCodeAuthenticationProvider())
    .addFilterBefore(emailCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

    .authenticationProvider(mobileCodeAuthenticationProvider())
    .addFilterBefore(mobileCodeAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)

感謝各位的閱讀,以上就是“Spring Security如何實現登錄驗證”的內容了,經過本文的學習后,相信大家對Spring Security如何實現登錄驗證這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

赞皇县| 斗六市| 益阳市| 遵化市| 张家港市| 吉首市| 林芝县| 和硕县| 吉林市| 迭部县| 云浮市| 利辛县| 福海县| 溆浦县| 富锦市| 六枝特区| 安阳市| 元氏县| 定陶县| 邯郸县| 绩溪县| 台前县| 元阳县| 伽师县| 阳原县| 仙居县| 福州市| 瑞昌市| 藁城市| 漳州市| 柳河县| 汽车| 泾源县| 西宁市| 东乡族自治县| 丹东市| 区。| 汉寿县| 成安县| 德钦县| 辉县市|