16.SpringSecurity-web权限方案-自动登录(原理分析)

实现原理

在这里插入图片描述
  1.首次登录游览器保存cookie加密串Token,数据库保存cookie加密串及用户信息;

  2.再次访问,通过游览器中保存的cookie加密串获取数据库中的用户信息,并进行认证登录;

具体流程

在这里插入图片描述

源码

  之前有讲过UsernamePasswordAuthenticationFilter是过滤器链中的其中之一,其作用是对/login 的 POST 请求做拦截,校验表单中用户名,密码。

  attemptAuthentication方法就是其具体的实现,重点不是这里,是校验之后的逻辑,所以要看它的父类AbstractAuthenticationProcessingFilter的过滤方法doFilter中的源码:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {

   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);

      return;
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Request is to process authentication");
   }

   Authentication authResult;

   try {
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   }
   catch (InternalAuthenticationServiceException failed) {
      logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
      unsuccessfulAuthentication(request, response, failed);

      return;
   }
   catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);

      return;
   }

   // Authentication success
   if (continueChainBeforeSuccessfulAuthentication) {
      chain.doFilter(request, response);
   }

   successfulAuthentication(request, response, chain, authResult);
}

  认证成功后会调用successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, Authentication authResult)
      throws IOException, ServletException {

   if (logger.isDebugEnabled()) {
      logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
            + authResult);
   }
    //获取当前用户的权限和信息
   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);
}

  其中rememberMeServices.loginSuccess就是“记住我”的具体实现:

@Override
public final void loginSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication successfulAuthentication) {

   if (!rememberMeRequested(request, parameter)) {
      logger.debug("Remember-me login not requested.");
      return;
   }
    //生成Token,添加Cookie的方法
   onLoginSuccess(request, response, successfulAuthentication);
}

  onLoginSuccess方法:

protected void onLoginSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication successfulAuthentication) {
   String username = successfulAuthentication.getName();

   logger.debug("Creating new persistent login for user " + username);

   PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
         username, generateSeriesData(), generateTokenData(), new Date());
   try {
       //生成Token
      tokenRepository.createNewToken(persistentToken);
      //将生成的Token存入到Cookie中
      addCookie(persistentToken, request, response);
   }
   catch (Exception e) {
      logger.error("Failed to save persistent token ", e);
   }
}

  createNewToken实现:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
      PersistentTokenRepository {
   // ~ Static fields/initializers
   // =====================================================================================

   /** Default SQL for creating the database table to store the tokens */
   public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
         + "token varchar(64) not null, last_used timestamp not null)";
   /** The default SQL used by the <tt>getTokenBySeries</tt> query */
   public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
   /** The default SQL used by <tt>createNewToken</tt> */
   public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
   /** The default SQL used by <tt>updateToken</tt> */
   public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
   /** The default SQL used by <tt>removeUserTokens</tt> */
   public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";

   // ~ Instance fields
   // ================================================================================================

   private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
   private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
   private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
   private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
   private boolean createTableOnStartup;

   protected void initDao() {
      if (createTableOnStartup) {
         getJdbcTemplate().execute(CREATE_TABLE_SQL);
      }
   }
    //创建Token
   public void createNewToken(PersistentRememberMeToken token) {
      getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
            token.getTokenValue(), token.getDate());
   }
    //将Token加入到数据库中
   public void updateToken(String series, String tokenValue, Date lastUsed) {
      getJdbcTemplate().update(updateTokenSql, tokenValue, lastUsed, series);
   }

   /**
    * Loads the token data for the supplied series identifier.
    *
    * If an error occurs, it will be reported and null will be returned (since the result
    * should just be a failed persistent login).
    *
    * @param seriesId
    * @return the token matching the series, or null if no match found or an exception
    * occurred.
    */
   public PersistentRememberMeToken getTokenForSeries(String seriesId) {
      try {
         return getJdbcTemplate().queryForObject(tokensBySeriesSql,
               (rs, rowNum) -> new PersistentRememberMeToken(rs.getString(1), rs
                     .getString(2), rs.getString(3), rs.getTimestamp(4)), seriesId);
      }
      catch (EmptyResultDataAccessException zeroResults) {
         if (logger.isDebugEnabled()) {
            logger.debug("Querying token for series '" + seriesId
                  + "' returned no results.", zeroResults);
         }
      }
      catch (IncorrectResultSizeDataAccessException moreThanOne) {
         logger.error("Querying token for series '" + seriesId
               + "' returned more than one value. Series" + " should be unique");
      }
      catch (DataAccessException e) {
         logger.error("Failed to load token for series " + seriesId, e);
      }

      return null;
   }

   public void removeUserTokens(String username) {
      getJdbcTemplate().update(removeUserTokensSql, username);
   }

   /**
    * Intended for convenience in debugging. Will create the persistent_tokens database
    * table when the class is initialized during the initDao method.
    *
    * @param createTableOnStartup set to true to execute the
    */
   public void setCreateTableOnStartup(boolean createTableOnStartup) {
      this.createTableOnStartup = createTableOnStartup;
   }

  以上就是1-4步骤的源码实现。

  再次登录时,通过RememberMeAuthenticationFilter过滤器用来处理自动登录:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   if (SecurityContextHolder.getContext().getAuthentication() == null) {
       //自动登录处理
      Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
            response);

      if (rememberMeAuth != null) {
         // Attempt authenticaton via AuthenticationManager
         try {
            rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

            // Store to SecurityContextHolder
            SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

            onSuccessfulAuthentication(request, response, rememberMeAuth);

            if (logger.isDebugEnabled()) {
               logger.debug("SecurityContextHolder populated with remember-me token: '"
                     + SecurityContextHolder.getContext().getAuthentication()
                     + "'");
            }

            // Fire event
            if (this.eventPublisher != null) {
               eventPublisher
                     .publishEvent(new InteractiveAuthenticationSuccessEvent(
                           SecurityContextHolder.getContext()
                                 .getAuthentication(), this.getClass()));
            }

            if (successHandler != null) {
               successHandler.onAuthenticationSuccess(request, response,
                     rememberMeAuth);

               return;
            }

         }
         catch (AuthenticationException authenticationException) {
            if (logger.isDebugEnabled()) {
               logger.debug(
                     "SecurityContextHolder not populated with remember-me token, as "
                           + "AuthenticationManager rejected Authentication returned by RememberMeServices: '"
                           + rememberMeAuth
                           + "'; invalidating remember-me token",
                     authenticationException);
            }

            rememberMeServices.loginFail(request, response);

            onUnsuccessfulAuthentication(request, response,
                  authenticationException);
         }
      }

      chain.doFilter(request, response);
   }
   else {
      if (logger.isDebugEnabled()) {
         logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
               + SecurityContextHolder.getContext().getAuthentication() + "'");
      }

      chain.doFilter(request, response);
   }
}

  autoLogin实现:

@Override
public final Authentication autoLogin(HttpServletRequest request,
      HttpServletResponse response) {
   //从Cookie中获取数据       
   String rememberMeCookie = extractRememberMeCookie(request);

   if (rememberMeCookie == null) {
      return null;
   }

   logger.debug("Remember-me cookie detected");

   if (rememberMeCookie.length() == 0) {
      logger.debug("Cookie was empty");
      cancelCookie(request, response);
      return null;
   }

   UserDetails user = null;

   try {
       //对Cookie进行解密
      String[] cookieTokens = decodeCookie(rememberMeCookie);
      user = processAutoLoginCookie(cookieTokens, request, response);
      //判断解密的数据与数据库中的数据是否一致
      userDetailsChecker.check(user);

      logger.debug("Remember-me cookie accepted");
        //若一致,则进行自动登录
      return createSuccessfulAuthentication(request, user);
   }
   catch (CookieTheftException cte) {
      cancelCookie(request, response);
      throw cte;
   }
   catch (UsernameNotFoundException noUser) {
      logger.debug("Remember-me login was valid but corresponding user not found.",
            noUser);
   }
   catch (InvalidCookieException invalidCookie) {
      logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
   }
   catch (AccountStatusException statusInvalid) {
      logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
   }
   catch (RememberMeAuthenticationException e) {
      logger.debug(e.getMessage());
   }

   cancelCookie(request, response);
   return null;
}

  以上就是11-14步骤的源码实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
org.springframework.security.authentication.InternalAuthenticationServiceException: null at org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(DaoAuthenticationProvider.java:123) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:144) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:95) ~[spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.9.RELEASE.jar:5.2.9.
07-20

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值