日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

前言

相信大家在網上沖浪都遇到過登錄時輸入圖片驗證碼的情況,既然我們已經學習了 Spring Security,也上手實現過幾個案例,那不妨來研究一下如何實現這一功能。

首先需要明確的是,登錄時輸入圖片驗證碼,屬于認證功能的一部分,所以本文不涉及授權功能。

認證流程簡析

在上文中,我們介紹了認證流程,以及相關的關鍵類,可知 AuthenticationProvider 定義了 Spring Security 中的驗證邏輯,該類的類關系圖:

 

我們來看下 AuthenticationProvider 的定義:

public interface AuthenticationProvider {
  Authentication authenticate(Authentication authentication) throws AuthenticationException;

  boolean supports(Class<?> authentication);
}

可以看到,AuthenticationProvider 中就兩個方法:

  • authenticate 方法用來做驗證,就是驗證用戶身份。
  • supports 則用來判斷當前的 AuthenticationProvider 是否支持對應的 Authentication。

這里又涉及到一個東西,就是 Authentication。Authentication 本身是一個接口,從這個接口中,我們可以得到用戶身份信息,密碼,細節信息,認證信息,以及權限列表。我們來看下 Authentication 的定義:

package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
  // 獲取用戶的權限
  Collection<? extends GrantedAuthority> getAuthorities();

  //獲取用戶憑證,一般是密碼,認證之后會移出,來保證安全性
  Object getCredentials();
	//獲取用戶攜帶的詳細信息,Web應用中一般是訪問者的ip地址和sessionId
  Object getDetails();
	// 獲取當前用戶
  Object getPrincipal();
	//判斷當前用戶是否認證成功
  boolean isAuthenticated();

  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

官方文檔里說過,當用戶提交登錄信息時,會將用戶名和密碼進行組合成一個實例
UsernamePasswordAuthenticationToken,而這個類是 Authentication 的一個常用的實現類,用來進行用戶名和密碼的認證,類似的還有 RememberMeAuthenticationToken,它用于記住我功能。

Spring Security 支持多種不同的認證方式,不同的認證方式對應不同的身份類型,每個 AuthenticationProvider 需要實現supports()方法來表明自己支持的認證方式,如我們使用表單方式認證,在提交請求時 Spring Security 會生成
UsernamePasswordAuthenticationToken,它是一個 Authentication,里面封裝著用戶提交的用戶名、密碼信息。而對應的,哪個 AuthenticationProvider 來處理它?

我們在 DaoAuthenticationProvider 的基類
AbstractUserDetailsAuthenticationProvider 發現以下代碼:

    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

也就是說當web表單提交用戶名密碼時,Spring Security 由 DaoAuthenticationProvider 處理。

DaoAuthenticationProvider 的父類是
AbstractUserDetailsAuthenticationProvider, 在該類中的 authenticate()方法用于處理認證邏輯,這里就不粘貼代碼了,該方法大致邏輯如下:

  1. 首先實例化UserDetails對象,調用了retrieveUser方法獲取到了一個user對象,retrieveUser是一個抽象方法。該方法進一步會調用我們自己在登錄時候的寫的 loadUserByUsername 方法,具體在自定義的 UserDetailsService 或 InMemoryUserDetailsManager 等。
  2. 如果沒拿到信息就會拋出異常,如果查到了就會去調用preAuthenticationChecks的check(user)方法去進行預檢查。在預檢查中進行了三個檢查,因為UserDetail類中有四個布爾類型,去檢查其中的三個,用戶是否鎖定用戶是否過期,用戶是否可用。
  3. 預檢查之后緊接著去調用了additionalAuthenticationChecks方法去進行附加檢查,這個方法也是一個抽象方法,檢查密碼是否匹配,在DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法中去具體實現,在里面進行了加密解密去校驗當前的密碼是否匹配。我們想要校驗圖片驗證碼,就可以和密碼一起校驗,即我們重寫 additionalAuthenticationChecks 方法。
  4. 最后在 postAuthenticationChecks.check 方法中檢查密碼是否過期。
  5. 所有的檢查都通過,則認為用戶認證是成功的。用戶認證成功之后,會將這些認證信息和user傳遞進去,調用createSuccessAuthentication方法。

DaoAuthenticationProvider 中的
additionalAuthenticationChecks 方法用于比對密碼,邏輯比較簡單,就是將 password 加密后與事先保存好的密碼做比對。代碼如下:

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  if (authentication.getCredentials() == null) {
    this.logger.debug("Failed to authenticate since 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("Failed to authenticate since password does not match stored value");
      throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    }
  }
}

實操

自定義認證

我們復用之前的項目
springboot-security-inmemory,通過 postman 進行測試,不需要額外構建 html 頁面。

改動內容包括自定義 DaoAuthenticationProvider 實現類,重寫
additionalAuthenticationChecks 方法,以及生成圖片驗證碼。

項目增加如下依賴:

<dependency>
  <groupId>com.github.penggle</groupId>
  <artifactId>kaptcha</artifactId>
  <version>2.3.2</version>
</dependency>
復制代碼

創建 VerifyService 獲取驗證碼圖片

@Service
public class VerifyService {

  public Producer getProducer() {
    Properties properties = new Properties();
    properties.setProperty("kaptcha.image.width", "150");
    properties.setProperty("kaptcha.image.height", "50");
    properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
    properties.setProperty("kaptcha.textproducer.char.length", "4");
    Config config = new Config(properties);
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
  }
}

這段配置很簡單,我們就是提供了驗證碼圖片的寬高、字符庫以及生成的驗證碼字符長度。

VerifyCodeController 文件中增加圖片返回接口:

@RestController
@Slf4j
public class VerifyCodeController {

  @Autowired
  VerifyService verifyService;

  @GetMApping("/verify-code")
  public void getVerifyCodePng(HttpServletRequest request, HttpServletResponse resp)
      throws IOException {
    resp.setDateHeader("Expires", 0);
    resp.setHeader("Cache-Control",
        "no-store, no-cache, must-revalidate");
    resp.addHeader("Cache-Control", "post-check=0, pre-check=0");
    resp.setHeader("Pragma", "no-cache");
    resp.setContentType("image/jpeg");

    Producer producer = verifyService.getProducer();
    String text = producer.createText();
    HttpSession session = request.getSession();
    session.setAttribute("verify_code", text);
    BufferedImage image = producer.createImage(text);
    try (ServletOutputStream out = resp.getOutputStream()) {
      ImageIO.write(image, "jpg", out);
    }
  }

}

自定義 DaoAuthenticationProvider 實現類

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 驗證碼比對
    HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder
        .getRequestAttributes()).getRequest();
    String code = req.getParameter("code");
    HttpSession session = req.getSession(false);
    String verify_code = (String) session.getAttribute("verify_code");
    if (code == null || verify_code == null || !code.equals(verify_code)) {
      throw new AuthenticationServiceException("驗證碼錯誤");
    }
    // 密碼比對
    super.additionalAuthenticationChecks(userDetails, authentication);
  }
}

案例比較簡單,生成驗證碼圖片時,順便存放到 session 中,登錄驗證時從 session 中獲取驗證碼字符串,然后與傳來的驗證碼進行比對。

修改 SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
  PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

  @Override
  @Bean
  protected AuthenticationManager authenticationManager() throws Exception {
    ProviderManager manager = new ProviderManager(Arrays.asList(myAuthenticationProvider()));
    return manager;
  }

  @Bean
  @Override
  protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("hresh").password("123").roles("admin").build());
    return manager;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .authorizeRequests()
        .antMatchers("/verify-code").permitAll()
        .antMatchers("/code").permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .successHandler((req, resp, auth) -> {
          resp.setContentType("application/json;charset=utf-8");
          PrintWriter out = resp.getWriter();
          out.write(new ObjectMapper().writeValueAsString(Result.ok(auth.getPrincipal())));
          out.flush();
          out.close();
        })
        .failureHandler((req, resp, e) -> {
          resp.setContentType("application/json;charset=utf-8");
          PrintWriter out = resp.getWriter();
          out.write(new ObjectMapper().writeValueAsString(Result.failed(e.getMessage())));
          out.flush();
          out.close();
        })
        .permitAll();
  }

  @Bean
  MyAuthenticationProvider myAuthenticationProvider() {
    MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider();
    myAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    myAuthenticationProvider.setUserDetailsService(userDetailsService());
    return myAuthenticationProvider;
  }

}

測試

首先獲取圖片驗證碼

 

輸入正確的驗證碼和錯誤的密碼,進行登錄:

 

如果輸入錯誤的驗證碼

 

問題

使用AirPost測試遇到的問題

controller文件中設置了兩個api,一個方法往session中加了一個值,另一個方法從sesion中取值,結果兩次操作的sessionId不同。

代碼如下所示:

@GetMapping("/verify-code")
public void getVerifyCodePng(HttpServletRequest request) {
  Producer producer = verifyService.getProducer();
  String text = producer.createText();
  HttpSession session = request.getSession();
  session.setAttribute("verify_code", text);
  session.setAttribute("user", "hresh");
  log.info("code is " + text + " session id is " + session.getId());
}

@GetMapping("/code")
public String getVerifyCode(HttpServletRequest request) {
  HttpSession session = request.getSession();
  String verify_code = (String) session.getAttribute("verify_code");
  log.info("input code is " + verify_code + " session id is " + session.getId());
  return verify_code;
}

執行結果:

input code is 8045 session id is 77EBBF046128BC3618C825F62C0A2099
input code is null session id is A69A7D10EAFB0471B5D658489522739D

網上有類似的問題,可以參考這篇文章:blog.csdn.NET/weixin_4164…

相關問題還可以看這篇文章:跨域訪問sessionid不一致問題

總結

上面的例子主要是針對認證功能做一點增強,在實際應用中,其他的登錄場景也可以考慮這種方案,例如目前廣為流行的手機號碼動態登錄,就可以使用這種方式認證。

后續我們還會自定義認證流程中的密碼比對,以及授權流程中的權限比對,使之更佳貼近實際應用場景。

分享到:
標簽:驗證碼
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定