Spring Boot2(十五):Shiro記住我rememberMe、驗證碼Kaptcha


接着上次學習的《Spring Boot2(十二):手摸手教你搭建Shiro安全框架》,實現了Shiro的認證和授權。今天繼續在這個基礎上學習Shiro實現功能記住我rememberMe,以及登錄時驗證碼Kaptcha。

Remember Me記住我:用戶的登錄狀態會不會因為瀏覽器的關閉而失效,直到Cookie失效。關閉瀏覽器后,再次訪問登錄后的頁面可以不用登錄。因為用Cookie實現,故只在同一瀏覽器中有效。

Kaptcha驗證碼:是谷歌開源的驗證碼插件,實現登錄的驗證碼驗證攔截。

一、記住我rememberMe

用戶的登錄狀態會不會因為瀏覽器的關閉而失效,直到Cookie失效。關閉瀏覽器后,再次訪問登錄后的頁面可以不用登錄。因為用Cookie實現,故只在同一瀏覽器中有效。

修改ShiroConfig

/**
 * 路徑過濾規則
 * @return
 */
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
	ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
	shiroFilterFactoryBean.setSecurityManager(securityManager);
	// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
	shiroFilterFactoryBean.setLoginUrl("/login");
	shiroFilterFactoryBean.setSuccessUrl("/index");
	// 攔截器
	LinkedHashMap<String, String> map = new LinkedHashMap<>();
	// 配置不會被攔截的鏈接 順序判斷
	// 對靜態資源設置匿名訪問
	map.put("/static/**", "anon");
	map.put("/css/**", "anon");
	map.put("/js/**", "anon");

	// 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊
	// 進行身份認證后才能訪問
	// authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問
	// user指的是用戶認證通過或者配置了Remember Me記住用戶登錄狀態后可訪問
	map.put("/**", "user");
	shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
	return shiroFilterFactoryBean;
}

因為對登錄頁面做了一些樣式,新增了靜態資源文件static,這時候遇到了坑,頁面引用的jscss都無效了,然后發現時因為被攔截了,我們需要在Shiro的攔截器中允許對靜態資源的匿名anon訪問。

注意到將ShiroFilterFactoryBeanmap.put("/**", "authc");更改為map.put("/**", "user");user是指用戶認證通過或配置了RememberMe記住用戶登錄狀態后可訪問。

解決過程查閱了一些資料,不光光只對cssjs的放開,還需要對static也放開

對靜態資源的攔截相關問題可以參照這里了解學習一下:Spring Boot Shiro無法訪問JS/CSS/IMG+自定義Filter無法訪問完美方案

回來繼續,調用SimpleCookie,配置Cookie的基本屬性:名稱和過期時間。

/**
 * cookie對象
 * @return
 */
public SimpleCookie rememberMeCookie() {
	// 設置cookie名稱,對應login.html頁面的<input type="checkbox" name="rememberMe"/>
	SimpleCookie cookie = new SimpleCookie("rememberMe");
	// 設置cookie的過期時間,單位為秒,這里為一天
	cookie.setMaxAge(86400);
	return cookie;
}

SimleCookie參數中的名稱為頁面的name標簽屬性名稱。

實現了Cookie對象屬性配置,還需要通過CookieRememberMeManager進行管理起來。

/**
 * cookie管理對象
 * rememberMeManager()方法是生成rememberMe管理器,而且要將這個rememberMe管理器設置到securityManager中
 * @return
 */
public CookieRememberMeManager rememberMeManager() {
	CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
	cookieRememberMeManager.setCookie(rememberMeCookie());
	// rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)
	cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
	return cookieRememberMeManager;
}

接下來將cookie管理對象設置到SecurityManager中:

@Bean
public SecurityManager securityManager() {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	// 設置realm
	securityManager.setRealm(authRealm());
	// 用戶授權/認證信息Cache, 采用EhC//注入記住我管理器
	securityManager.setRememberMeManager(rememberMeManager());
	return securityManager;
}

加密處理

《Spring Boot2(十二):手摸手教你搭建Shiro安全框架》這個項目中用的明文,這里我們升個級,使用MD5加密

新建MD5加密工具類。

public class MD5Utils {

    private static final String SALT = "niaobulashi";

    private static final String ALGORITH_NAME = "md5";

    private static final int HASH_ITERATIONS = 2;

    public static String encrypt(String pwd) {
        String newPassword = new SimpleHash(ALGORITH_NAME, pwd, ByteSource.Util.bytes(SALT), HASH_ITERATIONS).toHex();
        return newPassword;
    }

    public static String encrypt(String username, String pwd) {
        String newPassword = new SimpleHash(ALGORITH_NAME, pwd, ByteSource.Util.bytes(username + SALT),
                HASH_ITERATIONS).toHex();
        return newPassword;
    }
    
    public static void main(String[] args) {
        System.out.println("MD5加密后的密文為:" + MD5Utils.encrypt("root", "root"));
    }
}

其中SALT是加密的鹽,可自行定義。

main方法中,根據登錄名和密碼明文,輸出最終加密的密文,將輸出內容粘貼到我們的數據庫中,待后續登錄時使用。

新增登錄頁面和主頁面

登錄頁login.html

添加Remember Me checkbox

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <link rel="stylesheet" th:href="@{/static/css/login.css}" type="text/css">
    <script th:src="@{/static/js/jquery-1.11.1.min.js}"></script>
</head>
<body>
<div class="login-page">
    <div class="form">
        <input type="text" placeholder="用戶名" name="account" required="required"/>
        <input type="password" placeholder="密碼" name="password" required="required"/>
        <p><input type="checkbox" name="rememberMe"/>記住我</p>
        <button onclick="login()">登錄</button>
    </div>
</div>
</body>
<script th:inline="javascript">var ctx = [[@{/}]];</script>
<script th:inline="javascript">
    function login() {
        var account = $("input[name='account']").val();
        var password = $("input[name='password']").val();
        var rememberMe = $("input[name='rememberMe']").is(':checked');
        $.ajax({
            type: "post",
            url: ctx + "login",
            data: {
                "account": account,
                "password": password,
                "rememberMe": rememberMe
            },
            success: function(r) {
                if (r.code == 100) {
                    location.href = ctx + 'index';
                } else {
                    alert(r.message);
                }
            }
        });
    }
</script>
</html>

靜態資源js和css可以在源碼中查看

首頁index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首頁</title>
</head>
<body>
<p>你好![[${user.getUsername()}]]</p>
<a th:href="@{/logout}">注銷</a>
</body>
</html>

Controller層

在原來的基礎上,新增參數rememberMe,同時對用戶名和明文密碼進行MD5加密處理獲得密文。

登錄接口

/**
 * 登錄操作
 * @param account
 * @param password
 * @param rememberMe
 * @return
 */
@PostMapping("/login")
@ResponseBody
public ResponseCode login(String account, String password, Boolean rememberMe) {
	logger.info("登錄請求-start");
	password = MD5Utils.encrypt(account, password);
	Subject userSubject = SecurityUtils.getSubject();
	UsernamePasswordToken token = new UsernamePasswordToken(account, password, rememberMe);
	try {
		// 登錄驗證
		userSubject.login(token);
		return ResponseCode.success();
	} catch (UnknownAccountException e) {
		return ResponseCode.error(StatusEnums.ACCOUNT_UNKNOWN);
	} catch (DisabledAccountException e) {
		return ResponseCode.error(StatusEnums.ACCOUNT_IS_DISABLED);
	} catch (IncorrectCredentialsException e) {
		return ResponseCode.error(StatusEnums.INCORRECT_CREDENTIALS);
	} catch (AuthenticationException e) {
		return ResponseCode.error(StatusEnums.AUTH_ERROR);
	} catch (Throwable e) {
		e.printStackTrace();
		return ResponseCode.error(StatusEnums.SYSTEM_ERROR);
	}
}

注銷接口

/**
 * 登出
 * @return
 */
@GetMapping("/logout")
public String logout() {
	getSubject().logout();
	return "login";
}

啟動項目,進行測試可以看到效果如下:

二、驗證碼Kaptcha

kaptcha 是一個非常實用的驗證碼生成工具。有了它,你可以生成各種樣式的驗證碼,因為它是可配置的。kaptcha工作的原理是調用 com.google.code.kaptcha.servlet.KaptchaServlet,生成一個圖片。同時將生成的驗證碼字符串放到 HttpSession中。

Kaptcha官網:https://code.google.com/archive/p/kaptcha/

使用kaptcha可以方便的配置:

  • 驗證碼的字體
  • 驗證碼字體的大小
  • 驗證碼字體的字體顏色
  • 驗證碼內容的范圍(數字,字母,中文漢字!)
  • 驗證碼圖片的大小,邊框,邊框粗細,邊框顏色
  • 驗證碼的干擾線(可以自己繼承com.google.code.kaptcha.NoiseProducer寫一個自定義的干擾線)
  • 驗證碼的樣式(魚眼樣式、3D、普通模糊……當然也可以繼承com.google.code.kaptcha.GimpyEngine自定義樣式)

kaptcha配置詳解

kaptcha對象屬性 作用 默認值
kaptcha.border 是否有邊框 默認為true
kaptcha.border.color 邊框顏色 默認為Color.BLACK
kaptcha.border.thickness 邊框粗細度 默認為1
kaptcha.producer.impl 驗證碼生成器 默認為DefaultKaptcha
kaptcha.textproducer.impl 驗證碼文本生成器 默認為DefaultTextCreator
kaptcha.textproducer.char.string 驗證碼文本字符內容范圍 默認為abcde2345678gfynmnpwx
kaptcha.textproducer.char.length 驗證碼文本字符長度 默認為5
kaptcha.textproducer.font.names 驗證碼文本字體樣式 宋體,楷體,微軟雅黑,默認為new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
kaptcha.textproducer.font.size 驗證碼文本字符大小 默認為40
kaptcha.textproducer.font.color 驗證碼文本字符顏色 默認為Color.BLACK
kaptcha.textproducer.char.space 驗證碼文本字符間距 默認為2
kaptcha.noise.impl 驗證碼噪點生成對象 默認為DefaultNoise
kaptcha.noise.color 驗證碼噪點顏色 默認為Color.BLACK
kaptcha.obscurificator.impl 驗證碼樣式引擎 默認為WaterRipple
kaptcha.word.impl 驗證碼文本字符渲染 默認為DefaultWordRenderer
kaptcha.background.impl 驗證碼背景生成器 默認為DefaultBackground
kaptcha.background.clear.from 驗證碼背景顏色漸進 默認為Color.LIGHT_GRAY
kaptcha.background.clear.to 驗證碼背景顏色漸進 默認為Color.WHITE
kaptcha.image.width 驗證碼圖片寬度 默認為200
kaptcha.image.height 驗證碼圖片高度 默認為50

添加maven依賴

<!--驗證碼-->
<dependency>
	<groupId>com.github.penggle</groupId>
	<artifactId>kaptcha</artifactId>
	<version>2.3.2</version>
</dependency>

新增驗證碼圖片樣式配置器

具體配置可以參考上面的kaptche配置詳情,針對不同的常見配置。

@Configuration
public class KaptchaConfig {

    @Bean(name="captchaProducer")
    public DefaultKaptcha getKaptchaBean(){
        DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
        Properties properties=new Properties();
        //驗證碼字符范圍
        properties.setProperty("kaptcha.textproducer.char.string", "23456789");
        //圖片邊框顏色
        properties.setProperty("kaptcha.border.color", "245,248,249");
        //字體顏色
        properties.setProperty("kaptcha.textproducer.font.color", "black");
        //文字間隔
        properties.setProperty("kaptcha.textproducer.char.space", "1");
        //圖片寬度
        properties.setProperty("kaptcha.image.width", "100");
        //圖片高度
        properties.setProperty("kaptcha.image.height", "35");
        //字體大小
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        //session的key
        //properties.setProperty("kaptcha.session.key", "code");
        //長度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字體
        properties.setProperty("kaptcha.textproducer.font.names", "宋體,楷體,微軟雅黑");
        Config config=new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

新增圖片驗證碼Controller層

是一個創建文件圖片流的過程,使用ServletOutPutStream輸出最后的圖片。

開頭聲明的@Resource(name = "captchaProducer"),是驗證碼圖片樣式配置器啟動時配置的Bean:captchaProducer

@Controller
@RequestMapping("/captcha")
public class KaptchaController {

    private static final Logger logger = LoggerFactory.getLogger(KaptchaController.class);

    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @GetMapping("/captchaImage")
    public ModelAndView getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ServletOutputStream out = response.getOutputStream();
        try {
            HttpSession session = request.getSession();
            response.setDateHeader("Expires", 0);
            // Set standard HTTP/1.1 no-cache headers.
            response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
            // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
            response.addHeader("Cache-Control", "post-check=0, pre-check=0");
            // Set standard HTTP/1.0 no-cache header.
            response.setHeader("Pragma", "no-cache");
            // return a jpeg
            response.setContentType("image/jpeg");
            // create the text for the image
            String capText = captchaProducer.createText();
            //將驗證碼存到session
            session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
            logger.info(capText);
            // 創建一張文本圖片
            BufferedImage bi = captchaProducer.createImage(capText);
            // 響應
            out = response.getOutputStream();
            // 寫入數據
            ImageIO.write(bi, "jpg", out);

            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

注意最后都需要將流關閉out.close()

放開圖片驗證碼的攔截

重啟會發現,圖片驗證碼的接口請求無法訪問,還是跳轉到了localhost:8081/login登錄頁面

因為Shiro配置的攔截器沒有放開,需要再ShiroConfig中允許匿名訪問改請求資源

map.put("/captcha/captchaImage**", "anon");

登錄頁面添加圖片驗證碼

<div class="login-page">
    <div class="form">
        <input type="text" placeholder="用戶名" name="account" required="required"/>
        <input type="password" placeholder="密碼" name="password" required="required"/>
        <p>
            <label>驗證碼<br/>
                <input type="text" name="validateCode" id="validateCode" class="validateCode" required="required"/>
                <a href="javascript:void(0);">
                    <img src="/captcha/captchaImage" onclick="this.src='/captcha/captchaImage?'+Math.random()"/>
                </a>
            </label>
        </p>
        <br>
        <p><input type="checkbox" name="rememberMe"/>記住我</p>
        <button onclick="login()">登錄</button>
    </div>
</div>

上面div為body的全部部分

我在請求/captcha/captchaImage后面添加隨機值Math.random()。是因為客戶瀏覽器會緩存URL相同的資源,故使用隨機數來重新請求。這和前端上線時,請求后綴都會變更一個版本號一樣,不需要讓客戶手動刷新瀏覽器就可以獲取最新資源一樣。

修改登錄請求接口

主要是驗證后台生成的驗證碼,與前台輸入的驗證碼進行比較,驗證是否相同

這里只粘貼出驗證碼驗證的邏輯,源碼在文章最后。

可以看出validateCode是前端請求過來的參數,先校驗是否為空。

然后從session中獲取后台生成的驗證碼。

最后通過比較前端輸入的驗證碼和后台生成的是否一致。

//1、檢驗驗證碼
if(validateCode == null || validateCode == ""){
	return ResponseCode.error(StatusEnums.PARAM_NULL);
}
Session session = SecurityUtils.getSubject().getSession();
//轉化成小寫字母
validateCode = validateCode.toLowerCase();
String v = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
//還可以讀取一次后把驗證碼清空,這樣每次登錄都必須獲取驗證碼
//session.removeAttribute("_come");
if(!validateCode.equals(v)){
	return ResponseCode.error(StatusEnums.VALIDATECODE_ERROR);
}

下圖是登錄校驗驗證碼的debug過程。

三、源碼

源碼地址:spring-boot-23-shiro-remember
歡迎star、fork,給作者一些鼓勵


菜鳥也要成為架構師,一起努力

歡迎關注我微信公眾號【鳥不拉屎】

謝謝,一起學習,共同進步,成為優秀的人。

微信公眾號:鳥不拉屎


免責聲明!

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



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