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