一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 詳解SpringSecurity中的Authentication信息與登錄流程

詳解SpringSecurity中的Authentication信息與登錄流程

2020-09-09 13:48天喬巴夏丶 Java教程

這篇文章主要介紹了SpringSecurity中的Authentication信息與登錄流程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下

Authentication

詳解SpringSecurity中的Authentication信息與登錄流程

使用SpringSecurity可以在任何地方注入Authentication進而獲取到當前登錄的用戶信息,可謂十分強大。

在Authenticaiton的繼承體系中,實現類UsernamePasswordAuthenticationToken 算是比較常見的一個了,在這個類中存在兩個屬性:principal和credentials,其實分別代表著用戶和密碼。【當然其他的屬性存在于其父類中,如authoritiesdetails。】

我們需要對這個對象有一個基本地認識,它保存了用戶的基本信息。用戶在登錄的時候,進行了一系列的操作,將信息存與這個對象中,后續我們使用的時候,就可以輕松地獲取這些信息了。

那么,用戶信息如何存,又是如何取的呢?繼續往下看吧。

登錄流程

一、與認證相關的UsernamePasswordAuthenticationFilter

通過Servlet中的Filter技術進行實現,通過一系列內置的或自定義的安全Filter,實現接口的認證與授權。

比如:UsernamePasswordAuthenticationFilter

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //獲取用戶名和密碼
        String username = obtainUsername(request);
        String password = obtainPassword(request);
 
        if (username == null) {
            username = "";
        }
 
        if (password == null) {
            password = "";
        }
        username = username.trim();
        //構造UsernamePasswordAuthenticationToken對象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
 
        // 為details屬性賦值
        setDetails(request, authRequest);
        // 調用authenticate方法進行校驗
        return this.getAuthenticationManager().authenticate(authRequest);
    }

獲取用戶名和密碼

從request中提取參數,這也是SpringSecurity默認的表單登錄需要通過key/value形式傳遞參數的原因。

?
1
2
3
4
5
6
7
8
@Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

構造UsernamePasswordAuthenticationToken對象

傳入獲取到的用戶名和密碼,而用戶名對應UPAT對象中的principal屬性,而密碼對應credentials屬性。

?
1
2
3
4
5
6
7
8
9
10
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
 username, password);
 
//UsernamePasswordAuthenticationToken 的構造器
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
 super(null);
 this.principal = principal;
 this.credentials = credentials;
 setAuthenticated(false);
}

為details屬性賦值

?
1
2
3
4
5
6
7
8
9
10
11
12
// Allow subclasses to set the "details" property 允許子類去設置這個屬性
setDetails(request, authRequest);
 
protected void setDetails(HttpServletRequest request,
    UsernamePasswordAuthenticationToken authRequest) {
 authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
 
//AbstractAuthenticationToken 是UsernamePasswordAuthenticationToken的父類
public void setDetails(Object details) {
 this.details = details;
}

 

details屬性存在于父類之中,主要描述兩個信息,一個是remoteAddress 和sessionId。

?
1
2
3
4
5
6
public WebAuthenticationDetails(HttpServletRequest request) {
    this.remoteAddress = request.getRemoteAddr();
 
    HttpSession session = request.getSession(false);
    this.sessionId = (session != null) ? session.getId() : null;
}

調用authenticate方法進行校驗

?
1
this.getAuthenticationManager().authenticate(authRequest)

二、ProviderManager的校驗邏輯

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public Authentication authenticate(Authentication authentication)
 throws AuthenticationException {
 Class<? extends Authentication> toTest = authentication.getClass();
 AuthenticationException lastException = null;
 AuthenticationException parentException = null;
 Authentication result = null;
 Authentication parentResult = null;
 boolean debug = logger.isDebugEnabled();
 
 for (AuthenticationProvider provider : getProviders()) {
 //獲取Class,判斷當前provider是否支持該authentication
 if (!provider.supports(toTest)) {
  continue;
 }
 //如果支持,則調用provider的authenticate方法開始校驗
 result = provider.authenticate(authentication);
 
        //將舊的token的details屬性拷貝到新的token中。
 if (result != null) {
  copyDetails(authentication, result);
  break;
 }
 }
 //如果上一步的結果為null,調用provider的parent的authenticate方法繼續校驗。
 if (result == null && parent != null) {
 result = parentResult = parent.authenticate(authentication);
 }
 
 if (result != null) {
 if (eraseCredentialsAfterAuthentication
  && (result instanceof CredentialsContainer)) {
  //調用eraseCredentials方法擦除憑證信息
  ((CredentialsContainer) result).eraseCredentials();
 }
 if (parentResult == null) {
  //publishAuthenticationSuccess將登錄成功的事件進行廣播。
  eventPublisher.publishAuthenticationSuccess(result);
 }
 return result;
 }
}

獲取Class,判斷當前provider是否支持該authentication。

如果支持,則調用provider的authenticate方法開始校驗,校驗完成之后,返回一個新的Authentication。

將舊的token的details屬性拷貝到新的token中。

如果上一步的結果為null,調用provider的parent的authenticate方法繼續校驗。

調用eraseCredentials方法擦除憑證信息,也就是密碼,具體來說就是讓credentials為空。

publishAuthenticationSuccess將登錄成功的事件進行廣播。

三、AuthenticationProvider的authenticate

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
 //從Authenticaiton中提取登錄的用戶名。
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();
 //返回登錄對象
    user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
 //校驗user中的各個賬戶狀態屬性是否正常
    preAuthenticationChecks.check(user);
 //密碼比對
    additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
 //密碼比對
    postAuthenticationChecks.check(user);
    Object principalToReturn = user;
 //表示是否強制將Authentication中的principal屬性設置為字符串
    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
 //構建新的UsernamePasswordAuthenticationToken
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

從Authenticaiton中提取登錄的用戶名。retrieveUser方法將會調用loadUserByUsername方法,這里將會返回登錄對象。preAuthenticationChecks.check(user);校驗user中的各個賬戶狀態屬性是否正常,如賬號是否被禁用,賬戶是否被鎖定,賬戶是否過期等。additionalAuthenticationChecks用于做密碼比對,密碼加密解密校驗就在這里進行。postAuthenticationChecks.check(user);用于密碼比對。forcePrincipalAsString表示是否強制將Authentication中的principal屬性設置為字符串,默認為false,也就是說默認登錄之后獲取的用戶是對象,而不是username。構建新的UsernamePasswordAuthenticationToken

用戶信息保存

我們來到UsernamePasswordAuthenticationFilter 的父類AbstractAuthenticationProcessingFilter 中,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    Authentication authResult;
    try {
 //實際觸發了上面提到的attemptAuthentication方法
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
 //登錄失敗
    catch (InternalAuthenticationServiceException failed) {
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    catch (AuthenticationException failed) {
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
 //登錄成功
    successfulAuthentication(request, response, chain, authResult);
}

關于登錄成功調用的方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void successfulAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
 //將登陸成功的用戶信息存儲在SecurityContextHolder.getContext()中
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }
 //登錄成功的回調方法
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

我們可以通過SecurityContextHolder.getContext().setAuthentication(authResult);得到兩點結論:

  • 如果我們想要獲取用戶信息,我們只需要調用SecurityContextHolder.getContext().getAuthentication()即可。
  • 如果我們想要更新用戶信息,我們只需要調用SecurityContextHolder.getContext().setAuthentication(authResult);即可。

用戶信息的獲取

前面說到,我們可以利用Authenticaiton輕松得到用戶信息,主要有下面幾種方法:

通過上下文獲取。

?
1
SecurityContextHolder.getContext().getAuthentication();

直接在Controller注入Authentication。

?
1
2
3
4
@GetMapping("/hr/info")
public Hr getCurrentHr(Authentication authentication) {
 return ((Hr) authentication.getPrincipal());
}

為什么多次請求可以獲取同樣的信息

前面已經談到,SpringSecurity將登錄用戶信息存入SecurityContextHolder 中,本質上,其實是存在ThreadLocal中,為什么這么說呢?

原因在于,SpringSecurity采用了策略模式,在SecurityContextHolder 中定義了三種不同的策略,而如果我們不配置,默認就是MODE_THREADLOCAL模式。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
 
private static void initialize() {
 if (!StringUtils.hasText(strategyName)) {
 // Set default
 strategyName = MODE_THREADLOCAL;
 }
 if (strategyName.equals(MODE_THREADLOCAL)) {
 strategy = new ThreadLocalSecurityContextHolderStrategy();
 }
}
 
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

了解這個之后,又有一個問題拋出:ThreadLocal能夠保證同一線程的數據是一份,那進進出出之后,線程更改,又如何保證登錄的信息是正確的呢。

這里就要說到一個比較重要的過濾器:SecurityContextPersistenceFilter,它的優先級很高,僅次于WebAsyncManagerIntegrationFilter。也就是說,在進入后面的過濾器之前,將會先來到這個類的doFilter方法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
 if (request.getAttribute(FILTER_APPLIED) != null) {
            // 確保這個過濾器只應對一個請求
            chain.doFilter(request, response);
            return;
        }
 //分岔路口之后,表示應對多個請求
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
 //用戶信息在 session 中保存的 value。
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
        try {
  //將當前用戶信息存入上下文
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            chain.doFilter(holder.getRequest(), holder.getResponse());
        }
        finally {
  //收尾工作,獲取SecurityContext
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
  //清空SecurityContext
            SecurityContextHolder.clearContext();
  //重新存進session中
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
        }
    }
}
  • SecurityContextPersistenceFilter 繼承自 GenericFilterBean,而 GenericFilterBean 則是 Filter 的實現,所以 SecurityContextPersistenceFilter 作為一個過濾器,它里邊最重要的方法就是 doFilter 了。
  • doFilter 方法中,它首先會從 repo 中讀取一個 SecurityContext 出來,這里的 repo 實際上就是 HttpSessionSecurityContextRepository,讀取 SecurityContext 的操作會進入到 readSecurityContextFromSession(httpSession) 方法中。
  • 在這里我們看到了讀取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,這里的 springSecurityContextKey 對象的值就是 SPRING_SECURITY_CONTEXT,讀取出來的對象最終會被轉為一個 SecurityContext 對象。
  • SecurityContext 是一個接口,它有一個唯一的實現類 SecurityContextImpl,這個實現類其實就是用戶信息在 session 中保存的 value。
  • 在拿到 SecurityContext 之后,通過 SecurityContextHolder.setContext 方法將這個 SecurityContext 設置到 ThreadLocal 中去,這樣,在當前請求中,Spring Security 的后續操作,我們都可以直接從 SecurityContextHolder 中獲取到用戶信息了。
  • 接下來,通過 chain.doFilter 讓請求繼續向下走(這個時候就會進入到 UsernamePasswordAuthenticationFilter 過濾器中了)。
  • 在過濾器鏈走完之后,數據響應給前端之后,finally 中還有一步收尾操作,這一步很關鍵。這里從 SecurityContextHolder 中獲取到 SecurityContext,獲取到之后,會把 SecurityContextHolder 清空,然后調用 repo.saveContext 方法將獲取到的 SecurityContext 存入 session 中。

總結:

每個請求到達服務端的時候,首先從session中找出SecurityContext ,為了本次請求之后都能夠使用,設置到SecurityContextHolder 中。

當請求離開的時候,SecurityContextHolder 會被清空,且SecurityContext 會被放回session中,方便下一個請求來獲取。

資源放行的兩種方式

用戶登錄的流程只有走過濾器鏈,才能夠將信息存入session中,因此我們配置登錄請求的時候需要使用configure(HttpSecurity http),因為這個配置會走過濾器鏈。

?
1
2
3
http.authorizeRequests()
 .antMatchers("/hello").permitAll()
 .anyRequest().authenticated()

而 configure(WebSecurity web)不會走過濾器鏈,適用于靜態資源的放行。

?
1
2
3
4
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/index.html","/img/**","/fonts/**","/favicon.ico");
}

到此這篇關于SpringSecurity中的Authentication信息與登錄流程的文章就介紹到這了,更多相關SpringSecurity登錄流程內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://www.cnblogs.com/summerday152/p/13636285.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 香蕉精品视频 | 9re视频这里只有精品 | 午夜AV内射一区二区三区红桃视 | 国产一级精品高清一级毛片 | 午夜第九达达兔鲁鲁 | 91手机看片国产永久免费 | 日本精品一卡二卡≡卡四卡 | 亚洲精品国产一区二区三区在 | 欧美国产日本精品一区二区三区 | 爱豆传媒最新视频国产 | 日产乱码卡一卡2卡三卡四福利 | 国产精品久久久久久影院 | 极品妖艳许清赵丽全文免费阅读 | 97porm自拍视频区原创 | 日韩视频在线免费 | 青青国产成人久久激情91麻豆 | 亚洲日日做天天做日日谢 | 极品主播的慰在线播放 | 欧洲喷浆乌克兰 | 欧美成人免费观看国产 | 69日本xxxhd| 好大好长好紧爽免费 | 青春草在线观看视频 | 亚洲丰满模特裸做爰 | 草β好视频 | 手机在线免费观看视频 | ipx-177绝对领域在线观看 | 亚洲精品www久久久久久久软件 | 久久夜色噜噜噜亚洲AV0000 | 色婷婷综合久久久 | 韩国甜性涩爱免费观看 | 久久精品国产免费 | 美女扒开屁股让男人进去 | 亚洲国产日韩欧美在线vip1区 | 日本卡1卡2卡4卡免费 | 国内精品视频一区二区三区八戒 | 国产青草亚洲香蕉精品久久 | 国产老村长足疗店对白 | 女攻双性 | 草莓在线 | 日本一片免费观看高清完整 |