springSecurity 登入以及使用者賬號密碼解析原理

你笑的像一條狗發表於2019-01-15
springSecurity 攔截器鏈

img

使用者登入基本流程處理如下:

1 SecurityContextPersistenceFilter

2 AbstractAuthenticationProcessingFilter

3 UsernamePasswordAuthenticationFilter

4 AuthenticationManager

5 AuthenticationProvider

6 userDetailsService

7 userDetails

8 認證通過

9 SecurityContext

10 SecurityContextHolder

11 AuthenticationSuccessHandler

使用者頁面登入

1 首先進入 SecurityContextPersistenceFilter 攔截器

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  。。。。。省略
		//HttpRequestResponseHolder 物件
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        // 判斷session是否存在,如果不存在則新建一個session
        SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
        boolean var13 = false;

        try {
            var13 = true;
            //將securiryContext放入SecurityContextHolder中
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            //呼叫下一個攔截器,也就是之後所有的攔截器
            chain.doFilter(holder.getRequest(), holder.getResponse());
            var13 = false;
        } finally {
            if (var13) {
                //獲取從context
                SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
                //清空SecurityContextHolder 中的contex 臨時儲存
                SecurityContextHolder.clearContext();
                //儲存後面過濾器生成的資料 到SecurityContextRepository中
                this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
                request.removeAttribute("__spring_security_scpf_applied");
                if (debug) {
                    this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
                }

            }
        }
		//從SecurityContextHolder獲取SecurityContext例項  8
        SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        //清空SecurityContextHolder中的SecurityContext
        SecurityContextHolder.clearContext();
        //將SecurityContext例項儲存到session中,以便下次請求時候用	9
        this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
        request.removeAttribute("__spring_security_scpf_applied");
        if (debug) {
            this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
        }

    }
}
複製程式碼

loadContext 判斷session是否存在 沒有新建一個

public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
    HttpServletRequest request = requestResponseHolder.getRequest();
    HttpServletResponse response = requestResponseHolder.getResponse();
    //如果session不存在則返回null
    HttpSession httpSession = request.getSession(false);
    //根據 private String springSecurityContextKey = "SPRING_SECURITY_CONTEXT"; 
    //獲取原來的session
    SecurityContext context = this.readSecurityContextFromSession(httpSession);
    if (context == null) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("No SecurityContext was available from the HttpSession: " + httpSession + ". A new one will be created.");
        }
		//如果session 為null  則新建一個
        context = this.generateNewContext();
    }
	//將session  儲存到 內部類SaveToSessionResponseWrapper 中
    HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper wrappedResponse = new HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper(response, request, httpSession != null, context);
    //儲存在 HttpRequestResponseHolder物件中
    requestResponseHolder.setResponse(wrappedResponse);
    if (this.isServlet3) {
        requestResponseHolder.setRequest(new HttpSessionSecurityContextRepository.Servlet3SaveToSessionRequestWrapper(request, wrappedResponse));
    }

    return context;
}
複製程式碼

readSecurityContextFromSession 方法中 根據判斷session是否存在 會根據 “ SPRING_SECURITY_CONTEXT ”

獲取session

使用者登入即是認證

登入的實現方式:

2 進入AbstractAuthenticationProcessingFilter類

在這裡插入圖片描述

3 UsernamePasswordAuthenticationFilter 實現了類 AbstractAuthenticationProcessingFilter 呼叫自身的attemptAuthentication方法

獲取使用者名稱和密碼,構建token

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());
    } else {
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        //構建token ,此時沒有進行驗證
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        // AuthenticationManager 將token傳遞給  的 authenticate方法 進行 token驗證
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
//獲取密碼
protected String obtainPassword(HttpServletRequest request) {
    return request.getParameter(this.passwordParameter);
}
//獲取賬號
protected String obtainUsername(HttpServletRequest request) {
    return request.getParameter(this.usernameParameter);
}
複製程式碼

呼叫authenticate方法,進行token

4 AuthenticationManager 介面

1 AuthenticationManager 介面

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}
複製程式碼

2 ProviderManager 實現了AuthenticationManager 介面

 authenticate方法中
    result = provider.authenticate(authentication);  //返回成功的認證
複製程式碼

重寫authenticate方法 來獲取使用者驗證資訊

5 AuthenticationProvider 介面中方法

Authentication authenticate(Authentication authentication)
      throws AuthenticationException;
複製程式碼

AbstractUserDetailsAuthenticationProvider 實現了AuthenticationProvider 介面

public Authentication authenticate(Authentication authentication) //authentication 傳遞過來token
      throws AuthenticationException {
   Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
         () -> messages.getMessage(
               "AbstractUserDetailsAuthenticationProvider.onlySupports",
               "Only UsernamePasswordAuthenticationToken is supported"));

   // Determine username    獲取密碼
   String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
         : authentication.getName();

   boolean cacheWasUsed = true;
   //從快取中獲取
   UserDetails user = this.userCache.getUserFromCache(username);
	//沒有快取
   if (user == null) {
      cacheWasUsed = false;

      try {
      //查詢資料庫  獲取使用者賬號密碼
         user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException notFound) {
         logger.debug("User '" + username + "' not found");

//此處 不能丟擲異常 UsernameNotFoundException 
         if (hideUserNotFoundExceptions) {
            throw new BadCredentialsException(messages.getMessage(
                  "AbstractUserDetailsAuthenticationProvider.badCredentials",
                  "Bad credentials"));
         }
         else {
            throw notFound;
         }
      }

      Assert.notNull(user,
            "retrieveUser returned null - a violation of the interface contract");
   }

   try {
   //檢查賬號是否過期等操作
      preAuthenticationChecks.check(user);
      //
      additionalAuthenticationChecks(user,
            (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException exception) {
      if (cacheWasUsed) {
         // There was a problem, so try again after checking
         // we're using latest data (i.e. not from the cache)
         cacheWasUsed = false;
         user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
         preAuthenticationChecks.check(user);
         additionalAuthenticationChecks(user,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      else {
         throw exception;
      }
   }

   postAuthenticationChecks.check(user);

   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }

   Object principalToReturn = user;

   if (forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
	//建立成功的認證Authentication
   return createSuccessAuthentication(principalToReturn, authentication, user);
}
複製程式碼

retrieveUser 方法查詢使用者資料:

呼叫自身的抽象方法

protected abstract UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException;
複製程式碼

DaoAuthenticationProvider 實現類

DaoAuthenticationProvider 繼承 AbstractUserDetailsAuthenticationProvider 重寫 retrieveUser 方法

用來根據使用者名稱查詢使用者資料

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
     // 根據使用者名稱查詢資料
      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);
   }
}
複製程式碼

6 UserServiceDatils 介面 呼叫 loadUserByUsername 查詢資料

7 userDetails物件返回資料

UserServiceDatils 介面需要自己實現(6.7一起進行)

public class MUserDetailsService implements UserDetailsService {

     Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private UsersMapper usersMapper;


    /**
     *@description:  需要從資料庫中通過使用者名稱來查詢使用者的資訊和使用者所屬的角色
     *@author: wangl
     *@time:  2019/1/8 10:44
     *@version 1.0
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        logger.info("===========授權============");

        UserDetails userDeatils = null;

        //通過使用者名稱 查詢密碼
        Users users = usersMapper.selectUserByUserName(username);

        Set<MGrantedAuthority> authorities = new HashSet<>();
        //查詢使用者角色
        if(null != users){
           logger.info("使用者 = " + users.getUsername() + "----" + users.getPassword());
            //查詢使用者許可權
            List<Users> usersList = usersMapper.selectRolesAndResourceByUserId(users.getId());
            if(null != usersList && usersList.size()>0){
                usersList.forEach(user->{
                     //存放role name  或者 許可權名字
                    authorities.add(new MGrantedAuthority(user.getResourceName())); 
                });
            }else{
                System.out.println("使用者無許可權。。");

                throw new BadCredentialsException("not found ... ");
            }

7 返回資料
            userDeatils = new Users(users.getUsername(),users.getPassword(),authorities);

        }else{
        
            throw new BadCredentialsException("使用者名稱不存在");
        }

        return userDeatils;
    }
複製程式碼

查詢使用者資料之後

返回查詢到的loadUser物件
然後使用者賬號是否被鎖定,過期等驗證
this.preAuthenticationChecks.check(user);

複製程式碼

密碼驗證

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        this.logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    } else {
        //獲取密碼
        String presentedPassword = authentication.getCredentials().toString();
    	//匹配密碼
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}
複製程式碼

自定義密碼驗證器

@Slf4j
@Component
public class PasswordEncorder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        log.info("============ charSequence.toString()  ============== " + charSequence.toString());
        return MD5Util.encode(charSequence.toString());
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {

        String pwd = charSequence.toString();
        log.info("前端傳過來密碼為: " + pwd);
        log.info("加密後密碼為: " + MD5Util.encode(charSequence.toString()));
        //s 應在資料庫中加密
        if( MD5Util.encode(charSequence.toString()).equals(MD5Util.encode(s))){
            return true;
        }

        throw new DisabledException("--密碼錯誤--");
    }
    
}
複製程式碼

11 AuthenticationSuccessHandler

/**
 *@description: 自定義登陸成功處理類
 *@author: wangl
 *@time:  2019/1/14 17:46
 *@version 1.0
 */

@Component
public class MyAuthenctiationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        logger.info("登陸成功。。");
        String name = authentication.getName();
        HttpSession session = request.getSession();
        session.setAttribute("user",name);
        response.sendRedirect("/success");
        //super.onAuthenticationSuccess(request, response, authentication);
    }
}
複製程式碼

配置類

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MUserDetailsService mUserDetailsService; //userDetails 

    @Autowired
    MyFilterSecurityInterceptor myFilterSecurityInterceptor; //自定義攔截器

    @Autowired
    PasswordEncoder passwordEncoder; //密碼驗證器

    @Autowired
    private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;//失敗處理
    @Autowired
    private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;//成功處理

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers("/static/**"); //過濾靜態資源
    }


    /**定義認證使用者資訊獲取來源,密碼校驗規則等**/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //從記憶體中獲取
        //明文方式提交
         /*   auth.inMemoryAuthentication().withUser("zs").password("1234").roles("USER")
            .and().withUser("admin").password("1234").roles("ADMIN"); //從記憶體中獲取
           */

        auth.userDetailsService(mUserDetailsService) .passwordEncoder(passwordEncoder);  //密碼加密方式
        /*auth.authenticationProvider(customAuthenticationProvider)
                .authenticationProvider(authenticationProvider()) //增加
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());*/


        /*
            auth.inMemoryAuthentication().passwordEncoder(new PasswordEncorder()).withUser("zs").password("1234").roles("USER")
            .and().withUser("admin").password("1234").roles("ADMIN")
            ;*/
    }

    /**定義安全策略**/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      

        http.authorizeRequests() ////配置安全策略
                .antMatchers("/","/login.html","/loginPage").permitAll()   // 定義請求不需要驗證
                //.antMatchers("/admin/next").hasRole("ADMIN")  // 設定只有管理員才能訪問的url
                //.antMatchers("/admin/**").hasAnyRole("ADMIN")  // 設定多個角色訪問的url
                .anyRequest().authenticated()  //其餘所有請求都需要驗證

                .and()

                .formLogin()  //配置登入頁面

                .loginPage("/loginPage")  //登入頁面訪問路徑
                .loginProcessingUrl("/login") //登入頁面提交表單路徑
                .successHandler(myAuthenctiationSuccessHandler)//成功頁面
                .failureHandler(myAuthenctiationFailureHandler) //失敗後跳轉路徑

                .and()
                .logout()   //登出不需要驗證
                .logoutUrl("/logout").permitAll()

               // .and()

                //.authorizeRequests()
                //.antMatchers("/admin/next").hasRole("ADMIN")
                //.and()
                //.rememberMe() //記住我功能
                //.tokenValiditySeconds(10000);


                //自定義的攔截器 , 在適當的地方加入
                http.addFilterAt(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);

                http.csrf().disable(); ///關閉預設的csrf認證
    }


}
複製程式碼

相關文章