接着上次學習的《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可以在源碼中檢視
首頁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,給作者一些鼓勵
菜鳥也要成為架構師,一起努力
歡迎關注我微信公衆号【鳥不拉屎】
謝謝,一起學習,共同進步,成為優秀的人。