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

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

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

前言

相信大家在網(wǎng)上沖浪都遇到過登錄時輸入圖片驗(yàn)證碼的情況,既然我們已經(jīng)學(xué)習(xí)了 Spring Security,也上手實(shí)現(xiàn)過幾個案例,那不妨來研究一下如何實(shí)現(xiàn)這一功能。

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

認(rèn)證流程簡析

在上文中,我們介紹了認(rèn)證流程,以及相關(guān)的關(guān)鍵類,可知 AuthenticationProvider 定義了 Spring Security 中的驗(yàn)證邏輯,該類的類關(guān)系圖:

 

我們來看下 AuthenticationProvider 的定義:

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

  boolean supports(Class<?> authentication);
}

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

  • authenticate 方法用來做驗(yàn)證,就是驗(yàn)證用戶身份。
  • supports 則用來判斷當(dāng)前的 AuthenticationProvider 是否支持對應(yīng)的 Authentication。

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

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

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

  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

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

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

我們在 DaoAuthenticationProvider 的基類
AbstractUserDetailsAuthenticationProvider 發(fā)現(xiàn)以下代碼:

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

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

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

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

實(shí)操

自定義認(rèn)證

我們復(fù)用之前的項(xiàng)目
springboot-security-inmemory,通過 postman 進(jìn)行測試,不需要額外構(gòu)建 html 頁面。

改動內(nèi)容包括自定義 DaoAuthenticationProvider 實(shí)現(xiàn)類,重寫
additionalAuthenticationChecks 方法,以及生成圖片驗(yàn)證碼。

項(xiàng)目增加如下依賴:

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

創(chuàng)建 VerifyService 獲取驗(yàn)證碼圖片

@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;
  }
}

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

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 實(shí)現(xiàn)類

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 驗(yàn)證碼比對
    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("驗(yàn)證碼錯誤");
    }
    // 密碼比對
    super.additionalAuthenticationChecks(userDetails, authentication);
  }
}

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

修改 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;
  }

}

測試

首先獲取圖片驗(yàn)證碼

 

輸入正確的驗(yàn)證碼和錯誤的密碼,進(jìn)行登錄:

 

如果輸入錯誤的驗(yàn)證碼

 

問題

使用AirPost測試遇到的問題

controller文件中設(shè)置了兩個api,一個方法往session中加了一個值,另一個方法從sesion中取值,結(jié)果兩次操作的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;
}

執(zhí)行結(jié)果:

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

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

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

總結(jié)

上面的例子主要是針對認(rèn)證功能做一點(diǎn)增強(qiáng),在實(shí)際應(yīng)用中,其他的登錄場景也可以考慮這種方案,例如目前廣為流行的手機(jī)號碼動態(tài)登錄,就可以使用這種方式認(rèn)證。

后續(xù)我們還會自定義認(rèn)證流程中的密碼比對,以及授權(quán)流程中的權(quán)限比對,使之更佳貼近實(shí)際應(yīng)用場景。

分享到:
標(biāo)簽:驗(yàn)證碼
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

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

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定