接着上次學習的《SpringBoot2(十二):手摸手教你搭建Shiro安全框架》,實現了Shiro的認證和受權。今天繼續在這個基礎上學習Shiro實現功能記住我rememberMe,以及登陸時驗證碼Kaptcha。javascript
Remember Me記住我:用戶的登陸狀態會不會由於瀏覽器的關閉而失效,直到Cookie失效。關閉瀏覽器後,再次訪問登陸後的頁面能夠不用登陸。由於用Cookie實現,故只在同一瀏覽器中有效。css
Kaptcha驗證碼:是谷歌開源的驗證碼插件,實現登陸的驗證碼驗證攔截。html
用戶的登陸狀態會不會由於瀏覽器的關閉而失效,直到Cookie失效。關閉瀏覽器後,再次訪問登陸後的頁面能夠不用登陸。由於用Cookie實現,故只在同一瀏覽器中有效。前端
/** * 路徑過濾規則 * @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
訪問。java
注意到將ShiroFilterFactoryBean
的map.put("/**", "authc");
更改成map.put("/**", "user");
user是指用戶認證經過或配置了RememberMe記住用戶登陸狀態後可訪問。jquery
解決過程查閱了一些資料,不光光只對css
和js
的放開,還須要對static
也放開git
對靜態資源的攔截相關問題能夠參照這裏瞭解學習一下:Spring Boot Shiro沒法訪問JS/CSS/IMG+自定義Filter沒法訪問完美方案github
回來繼續,調用SimpleCookie,配置Cookie的基本屬性:名稱和過時時間。ajax
/**
* 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>
複製代碼
在原來的基礎上,新增參數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工做的原理是調用 com.google.code.kaptcha.servlet.KaptchaServlet,生成一個圖片。同時將生成的驗證碼字符串放到 HttpSession中。
Kaptcha官網:code.google.com/archive/p/k…
使用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 |
<!--驗證碼-->
<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;
}
}
複製代碼
是一個建立文件圖片流的過程,使用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中獲取後臺生成的驗證碼。
最後經過比較前端輸入的驗證碼和後臺生成的是否一致。
//一、檢驗驗證碼
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,給做者一些鼓勵
菜鳥也要成爲架構師,一塊兒努力
歡迎關注我微信公衆號【鳥不拉屎】
謝謝,一塊兒學習,共同進步,成爲優秀的人