Spring Security(二)實現圖片驗證碼和自動登陸(記住我)


錄:

1、實現圖片驗證碼
    1.1、創建獲取圖片驗證碼的 controller
    1.2、編寫用於校驗圖片驗證碼的過濾器
    1.3、將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前
    1.4、修改表單登陸頁
    1.5、測試
2、自動登陸(記住我)
    2.1、散列加密方案
    2.2、持久化令牌方案

1、實現圖片驗證碼    <--返回目錄

 

 1.1、創建獲取圖片驗證碼的 controller   <--返回目錄

  要想實現圖片驗證碼,首先需要一個用於獲取圖片驗證碼的 API。這里使用 kaptchar 勿用於生產)

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
View Code

  配置一個 kaptcha 實例:

@Bean
public Producer imageCode() {
    // 配置圖形驗證碼的基本參數
    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;
}
View Code

  創建 ValidateCodeController,用於生成圖片驗證碼

package com.oy.validate;

import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.google.code.kaptcha.Producer;

@Controller
public class ValidateCodeController {

    private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    @Autowired
    private Producer kaptchaProducer;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        response.setContentType("image/jpeg");
        // 創建驗證碼文本
        String codeText = kaptchaProducer.createText();
        // 將驗證碼文本設置到 session
        request.getSession().setAttribute(SESSION_KEY, codeText);
        // 根據文本創建圖片
        BufferedImage bi = kaptchaProducer.createImage(codeText);
        // 獲取響應輸出流
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bi, "jpg", out);
        // 推送並關閉響應輸出流
        try {
            out.flush();
        } finally {
            out.close();
        }
    }

}
View Code

  訪問圖片驗證碼(路徑 "/code/image")時不設置權限,在 JavaConfig 配置類中配置

antMatchers("/app/api/**", "/mylogin.html", "/code/image")
.permitAll() // 公開權限

  訪問 http://localhost:8089/BootDemo/code/image,即可看到返回一張圖片驗證碼。

1.2、編寫用於校驗圖片驗證碼的過濾器   <--返回目錄

  雖然 Spring Security 的過濾器對過濾器沒有特殊要求,只要繼承 Filter 即可,但是在 Spring 體系中,推薦使用 OncePerRequestFilter 來實現,它可以確保一次請求只會通過一次該過濾器(Filter 實際上並不能保證這一點)。

  ValidateCodeFIlter

package com.oy.validate;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

public class ValidateCodeFIlter extends OncePerRequestFilter {

    private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    
    private AuthenticationFailureHandler myAuthenticationFailureHandler;
    public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) {
        this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        System.out.println("ValidateCodeFIlter start, 請求uri:" 
            + request.getRequestURI() + ", servletPath:" + request.getServletPath());
        // 非登陸請求不校驗驗證碼
        if (!"/auth/form".equals(request.getServletPath())) {
            filterChain.doFilter(request, response);
            return;
        }
        
        try {
            doValidateCode(request);
            filterChain.doFilter(request, response);
        } catch(ValidateCodeException e) {
            myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
        }
        System.out.println("ValidateCodeFIlter end...");
    }
    
    private void doValidateCode(HttpServletRequest request) {
        String requestCode = request.getParameter("image_code");
        HttpSession session = request.getSession();
        String sessionCode = (String) session.getAttribute(SESSION_KEY);
                
        System.out.println("ValidateCodeFIlter, requestCode:" 
                + requestCode + " sessionCode:" + sessionCode);
        
        if (!StringUtils.isEmpty(sessionCode)) {
            // 隨手清除 session 中驗證碼,無論驗證成功還是失敗
            session.removeAttribute(SESSION_KEY);
        }
        // 校驗不通過,拋出異常
        if (StringUtils.isEmpty(requestCode)) {
            throw new ValidateCodeException("驗證碼輸入為空");
        }
        if (StringUtils.isEmpty(sessionCode)) {
            throw new ValidateCodeException("驗證碼為空");
        }
        if (!requestCode.equals(sessionCode)) {
            throw new ValidateCodeException("驗證碼輸入錯誤");
        }
        // 沒有異常,表示校驗通過
    }

}
View Code

  ValidateCodeException

package com.oy.validate;

import org.springframework.security.core.AuthenticationException;

public class ValidateCodeException extends AuthenticationException {

    private static final long serialVersionUID = 8369364787664640677L;

    public ValidateCodeException(String msg) {
        super(msg);
    }

}
View Code

  MyAuthenticationFailureHandler

package com.oy.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler  {

    //@Autowired
    //private ObjectMapper objectMapper;
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        
        System.out.println("登錄失敗," + exception.getMessage());
        super.onAuthenticationFailure(request, response, exception);
        
        /*
         * 根據配置項來確定返回 json 還是 按照 Spring Securiy 原來默認進行跳轉
        if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
        } else {
            super.onAuthenticationFailure(request, response, exception);
        }
        */
    }

}
View Code

  MyAuthenticationSuccessHandler

package com.oy.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    //@Autowired
    //private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {

        System.out.println("登錄成功");
        super.onAuthenticationSuccess(request, response, authentication);
        /*
        if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    */
    }

}
View Code

 

1.3、將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前   <--返回目錄

  WebSecurityConfig

package com.oy;

import java.io.IOException;
import java.util.Properties;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import com.oy.validate.ValidateCodeFIlter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
        validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
        
        // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
            .loginPage("/mylogin.html") // 指定登陸頁
            .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑
            .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯
            .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯
            .and()
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("admin")
                .antMatchers("/user/api/**").hasRole("user")
                // 登陸頁、驗證碼公開權限
                .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                .permitAll() // 公開權限
                .anyRequest().authenticated()
                .and()
            .csrf().disable();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    
    @Bean
    public Producer imageCode() {
        // 配置圖形驗證碼的基本參數
        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;
    }
}

 

1.4、修改表單登陸頁   <--返回目錄

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>自定義表單登陸頁</h2>
<form action="auth/form" method="post">
用戶名:<input type="text" name="username" /><br/>&nbsp;&nbsp;&nbsp;碼:<input type="text" name="password" /><br/>

驗證碼:<input type="text" name="image_code" /><br/>
<img src="code/image" alt="imagecode" height="50px" width="150px" /><br/>

<input type="submit" value="提交" />
</form>
</body>
</html>

 

1.5、測試   <--返回目錄

  啟動項目,訪問 http://localhost:8089/BootDemo/admin/api/1, 控制台打印結果

// 訪問:http://localhost:8089/BootDemo/admin/api/1
ValidateCodeFIlter start, 請求uri:/BootDemo/admin/api/1, servletPath:/admin/api/1
ValidateCodeFIlter start, 請求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html
ValidateCodeFIlter start, 請求uri:/BootDemo/code/image, servletPath:/code/image

// 使用 admin/123 登陸,驗證碼輸入錯誤
ValidateCodeFIlter start, 請求uri:/BootDemo/auth/form, servletPath:/auth/form
ValidateCodeFIlter, requestCode:ff sessionCode:9657
登錄失敗,驗證碼輸入錯誤
ValidateCodeFIlter end...

// 登陸失敗后,Spring Security 默認行為:跳轉到登陸頁面
ValidateCodeFIlter start, 請求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html
ValidateCodeFIlter start, 請求uri:/BootDemo/code/image, servletPath:/code/image

 

2、自動登陸(記住我)   <--返回目錄

   自動登陸時將用戶的登陸信息保存在客戶端瀏覽器的 cookie 中,當用戶下次訪問時,自動實現校驗並建立登陸狀態的一種機制。

  Spring Security 提供了兩種令牌:

  1)用散列算法加密用戶必要的登陸信息並生成令牌;

  2)數據庫等持久化數據存儲機制用的持久化令牌;

 

2.1、散列加密方案   <--返回目錄

    如下紅色字體的配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private UserDetailsService myUserDetailsService;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
        validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
        
        // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
                .loginPage("/mylogin.html") // 指定登陸頁
                .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑
                .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯
                .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯
                .and()
            .rememberMe() .userDetailsService(myUserDetailsService) .key("rem_key")
                .and()
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("admin")
                .antMatchers("/user/api/**").hasRole("user")
                // 登陸頁、驗證碼公開權限
                .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                .permitAll() // 公開權限
                .anyRequest().authenticated()
                .and()
            .csrf().disable();
    }
    
  // 省略
}

  表單登陸頁,添加 <input type="checkbox" name="remember-me"/> 進行測試。啟動項目,訪問 http://localhost:8089/BootDemo/admin/api/1, 跳轉到登陸頁,使用 admin/123 登陸,勾選 “remember me” 復選框。登陸成功后,查看 cookie, 默認過期時間 2 星期。

 

 

   將該 cookie 的 value 值進行 base64 解碼:

YWRtaW46MTU4ODEzMTE3ODE5MzplY2RlYWQxOGNhNzcxM2NjZTk2ZmRhZjM4NzI5YTk4YQ==
=== base64 解碼 ===>
admin:1588131178193:ecdead18ca7713cce96fdaf38729a98a

  驗證最后那串 hash 字符串,可以看到打印結果符合預期(注:DigestUtils 是 commons-codec.commons-codec.1.14 提供)

public void demo() {
    String hash = DigestUtils.md5Hex("admin:1588131178193:123:rem_key");
    System.out.println(hash);//ecdead18ca7713cce96fdaf38729a98a
}

  那么,remember-me 這個 cookie 的 value 值是根據什么規則生成的呢?

hashInfo = md5Hex(username + ":" + expirationTime + ":" + password + ":" + key)
rememberCookie = base64(username + ":" + expirationTime  + ":" + hashInfo)

  其中,expirationTime 是過期時間;key 是散列鹽值,用於防止令牌被修改(防止用戶自行修改,因為用戶是知道自己的用戶和密碼的,如果沒有這個 key,用戶可以自行修改 expirationTime 的值)。

  通過這中方式生成 cookie 后,在下次登陸時,Spring Security 首先用 base64 解碼,得到用戶名、過期時間和加密散列值;然后使用用戶名得到密碼;接着重新以上面的散列算法正向計算,並將計算結果與從瀏覽器獲取的加密散列值進行對比,從而確定該令牌是否有效。

 

2.2、持久化令牌方案   <--返回目錄

  持久化令牌方案的原理

 

   Remember Me 過濾器位置

 

 

  配置:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired private DataSource dataSource;
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        //tokenRepository.setCreateTableOnStartup(true);// 啟動時創建表,第二次啟動項目注釋掉
        return tokenRepository;
    }
    @Autowired
    private UserDetailsService myUserDetailsService;
    
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
        validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
        
        // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
                .loginPage("/mylogin.html") // 指定登陸頁
                .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑
                .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯
                .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯
                .and()
            .rememberMe()
                .tokenRepository(persistentTokenRepository()) // 持久化 token
                .tokenValiditySeconds(3600 * 24 * 7) // 過期時間, 單位秒
                .userDetailsService(myUserDetailsService) // 使用該 UserDetailsService 校驗用戶
                .key("rem_key")
                .and()
            .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("admin")
                .antMatchers("/user/api/**").hasRole("user")
                // 登陸頁、驗證碼公開權限
                .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                .permitAll() // 公開權限
                .anyRequest().authenticated()
                .and()
            .csrf().disable();
    }
    
    // 省略
}

 

參考:

  1)《Spring Security 實戰》-- 陳木鑫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM