天天看點

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,這時候遇到了坑,頁面引用的

js

css

都無效了,然後發現時因為被攔截了,我們需要在Shiro的攔截器中允許對靜态資源的匿名

anon

通路。

注意到将

ShiroFilterFactoryBean

map.put("/**", "authc");

更改為

map.put("/**", "user");

user是指使用者認證通過或配置了RememberMe記住使用者登入狀态後可通路。

解決過程查閱了一些資料,不光光隻對

css

js

的放開,還需要對

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可以在源碼中檢視

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

首頁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";
}           

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

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

二、驗證碼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相同的資源,故使用随機數來重新請求。這和前端上線時,請求字尾都會變更一個版本号一樣,不需要讓客戶手動重新整理浏覽器就可以擷取最新資源一樣。

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

修改登入請求接口

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

這裡隻粘貼出驗證碼驗證的邏輯,源碼在文章最後。

可以看出

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 Boot2(十五):Shiro記住我rememberMe、驗證碼Kaptcha

三、源碼

源碼位址:spring-boot-23-shiro-remember

歡迎star、fork,給作者一些鼓勵

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

歡迎關注我微信公衆号【鳥不拉屎】

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