這兩天接手了下師兄的項目,要給系統加個日志管理模塊,其中需要記錄登錄功能的日志,那么首先要知道系統的登錄是在哪里實現驗證的。
該系統把所有登錄驗證還有權限控制的工作都交給了shiro。
這篇文章就先簡單記錄下這兩天看的關於shiro登錄驗證的小總結。
(本文是看了一天代碼和博客總結出的大概理解,有點模糊還可能不一定對……有大佬知道哪里錯的話希望能評論指出下哈哈哈)
主要就一直在解決幾個問題:
1. 怎么驗證身份?
首先理解幾個概念,token是用戶提交的東西,一般有兩部分Principal(賬號或者用戶名)和Credentials(密碼或者驗證的東西)。
然后Subject很重要,可以看作是驗證用戶或者用戶的一個抽象。
然后有個AuthenticationInfo,這個可以理解成是正確的用戶驗證信息的一個抽象吧。驗證主要就是重寫一個方法:
/*主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //獲取用戶的輸入的賬號. String name = (String)token.getPrincipal(); // System.out.println("token---------++++++++---------->"+(String)token.getPrincipal()+(String)token.getCredentials()); //通過name從數據庫中查找 User對象,如果找到,沒找到. //實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鍾內不會重復執行該方法 Map<String, Object> map= new HashMap<String, Object>(); map.put("uName", name); User user = securityShiroService.selectByUserInfo(map); if(user == null){ return null; } //System.out.println("token---------++++++++---------->"+user.getuName()+user.getId()); SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用戶名 user.getuPass(), //密碼 ByteSource.Util.bytes(user.getuName()+"jintu"),//salt=username+salt getName() //realm name ); // 當驗證都通過后,把用戶信息放在session里 Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("userInfo", user); return authenticationInfo; }
然后看這里的代碼我很不解——這里就根據用戶提交的token的name也就用戶名,然后去數據庫里面找到用戶的具體信息,然后就把信息保存在session中???
exm??不用對比密碼對不對的嗎??
然后看了這篇博客:https://www.jianshu.com/p/1a371f3cec27
他說 查看shiro源碼才知道:
大概就是首先在進入自定義Realm之前,會經過AuthenticatingRealm這個類的getAuthenticationInfo方法,在這個方法里面會用到你的authenticationInfo然后和token進行比較,然后判斷是不是驗證成功,也就是說,你在這個重寫的realm方法中給出正確的用戶信息后,登錄驗證這個東西交給shiro去做了。
嗯應該這樣理解吧。
2. 師兄代碼里面並沒有currentUser.login(token)???
我看網上的demo基本都是在控制器里面,攔截登錄請求,然后調用這個login(token)語句,怎么項目里面找不到呢????
這個我看了這篇文章大概理解了下:https://www.jianshu.com/p/0662cf366161
首先,shiro會有個過濾器filter,你可以配置哪些資源需要攔截然后通過驗證才能訪問,哪些資源不用驗證可以直接訪問。
一般會有幾個屬性設置:
filter里面是可以放行的資源;然后logout顯然就和退出登錄有關,大概就是退出登錄的請求吧;authc=/**意味着所有資源都要經過驗證除了前面放行的;loginUrl就是登錄請求的路徑咯,然后兩個很顯然了,shiro判斷你登錄成功后跳轉的界面還有最后一個應該是沒有權限后顯示的界面。
根據上面那個博客,大概可以猜到,系統沒有開放login界面,因此我們訪問login界面也好其他系統界面也好,首先就會要驗證,要驗證shiro就會啟動他那套驗證,內部調用我們重寫的doGetAuthenticationInfo然后完成驗證工作。
所以大概梳理了下系統可能的登錄驗證邏輯
首先是網關處controller的代碼:
@Controller public class HomeController { private final static Logger logger = Logger.getLogger(HomeController.class); @RequestMapping({ "/", "/index" }) public String index(ModelMap model,HttpServletResponse resp) { Session session = SecurityUtils.getSubject().getSession(); User user = (User) session.getAttribute("userInfo"); Cookie cookie = new Cookie("sssid", user.getId()); cookie.setMaxAge(7200); resp.addCookie(cookie); Cookie cookies = new Cookie("zytypes", String.valueOf(user.getuType())); cookies.setMaxAge(7200); resp.addCookie(cookies); return "redirect:/gate-ui/index"; } @RequestMapping("/login") //public ModelAndView login(HttpServletRequest request,Model model) throws Exception { public String login( HttpServletRequest request,Model model) throws Exception { //進入登錄頁面時,清空數據 Subject subject = SecurityUtils.getSubject(); subject.logout(); // 登錄失敗從request中獲取shiro處理的異常信息,shiroLoginFailure:就是shiro異常類的全類名. String exception = (String) request.getAttribute("shiroLoginFailure"); logger.info("登錄異常 -- > " + exception); String msg = ""; if (exception != null) { if (UnknownAccountException.class.getName().equals(exception)) { logger.info("UnknownAccountException -- > 賬號不存在!"); msg = "賬號不存在!"; } else if (IncorrectCredentialsException.class.getName().equals(exception)) { logger.info("IncorrectCredentialsException -- > 密碼不正確!"); msg = "密碼不正確!"; } else if ("kaptchaValidateFailed".equals(exception)) { logger.info("kaptchaValidateFailed -- > 驗證碼錯誤!"); msg = "驗證碼錯誤!"; } else { msg = "else >> " + exception; logger.info("else -- >" + exception); } } model.addAttribute("msg",msg); // 此方法不處理登錄成功,由shiro進行處理 // return new ModelAndView("/login"); return "login"; } @RequestMapping("/403") public String unauthorizedRole() { return "403"; } }
這個代碼我看的真的很懵一開始……為什么login那個controller完全沒有登錄業務?????而是在處理登錄失敗的信息????
現在大概梳理下:首先,我們輸入url的時候不管是不是login的請求,就不管是不是/login,都是會被shiro的filter攔截,然后就要驗證嘛。
因為我們設定了登錄地址:
所以我猜應該shiro一旦需要驗證你身份了,就會跳到這個界面。所以應該出現這個界面的時候,還沒經過login的那個controller。
然后你就提交表單請求,有token然后shiro就會根據你的doGetAuthenticationInfo和token對比完成驗證,我們不是設置了shiro的successUrl嘛:
那登錄失敗呢???登錄失敗應該就是還有驗證信息的意思吧,所以應該跳轉到/login,所有我們看到homeController里面/login的controller,它第一步是logOut(),這個應該是清除session中存好的用戶信息,然后將之前登陸失敗的原因從request中拿出來,這個失敗的原因應該是shiro在登錄失敗后會放在request中。
然后將登錄失敗的信息通過SPringMVC給到前端顯示。
為什么要第一步logOut???因為師兄doGetAuthenticationInfo方法的實現里面,根據token的用戶名拿到用戶信息后,不管三七二十一都會吧信息存到shiro的session中,登錄成功的話就session里面有用戶信息,登錄失敗又跳到/login就肯定要先logout()清楚session唄。
附:關於Shiro的session
項目用的是SpringCloud微服務的框架,登錄的驗證代碼是在gate工程中,但在admin工程中我看到一樣可以使用shiro的session???、
不應該是不一樣的tomcat嗎,怎么會一樣的session???
后面百度了下,這個session和http中的session是不一樣的:
shiro的session好像是基於Java對象的,是和之前理解的不一樣。
如上圖所示,shiro自己定義了一個新的Session接口,用於統一操作接口,並通過SessionManager實現Session管理。
其中的3個實現類HttpServletSession,SimpleSession和StoppingAwareProxiedSession是我們經常需要打交道的。
然后shiro有個sessionDao的東西,就是可以吧session持久化,然后項目里面看到相關的代碼實現好像用了redis,也就是說系統應該是吧session存到redis里了。
還有個不懂的地方
我們來看看這個登錄界面login:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>廣東省中葯資源動態監測系統</title> <link rel="stylesheet" type="text/css" href="css/styles.css"/> </head> <body> <div class="htmleaf-container"> <div class="wrapper"> <div class="container"> <h1>廣東省中葯資源動態監測系統</h1> <form class="form" action="" method="post"> <input type="text" name="username" id="username" autofocus placeholder="Username"> <input type="password" name="password" id="password" placeholder="Password"> <button type="submit" id="login-button">登陸</button> <input type="checkbox" type="hidden" name="rememberMe" title="記住我" checked=""> <p id="msg" th:text="${msg}"></p> </form> </div> <ul class="bg-bubbles"> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> </div> </div> <script src="js/jquery-2.1.1.min.js" type="text/javascript"></script> </body> </html>
可以看到,這個頁面除了jQuery的js包,沒有引入其他的js文件了。
那登錄請求是怎么發給服務器的????
而且,暫時在代碼中沒有找到任何解析登錄表單然后轉換成shiro相關token的代碼……
所以兩個疑問:
1. 登錄請求怎么發到后端的?
2. 用戶請求怎么轉換成token的???
我猜測就是指定了shiro登錄的界面后,就會將登錄界面表單的東西自動封裝成token,但網上沒能找到相關資料………………
代碼也看不到相關配置……和代碼,希望哪個大佬知道為什么可以解答我哈哈哈謝謝。
最后還找到了個思路和這個項目差不多的登錄驗證博客:https://blog.csdn.net/caiqiandu/article/details/88973995
評論了作者我的疑問但到目前還沒回復哈哈
寫的挺詳細的但仍然沒有回答我不明白的地方&………………
參考博客:
https://www.jianshu.com/p/1a371f3cec27——《應該在自定義Realm的doGetAuthenticationInfo方法中做什么》——告訴我大概怎這個方法有什么用,一般怎么重寫
https://www.jianshu.com/p/0662cf366161——《shiro 登錄驗證的一個問題》——讓我大概知道為什么可以不用調用user.login(token)
兩個關於shirosession的:
https://blog.csdn.net/changudeng1992/article/details/81914628——《Shiro筆記四(會話管理):SessionDao》
https://blog.csdn.net/bitree1/article/details/50498970——《org.apache.shiro.SecurityUtils.getSubject().getSession()》
一個常規的登錄授權demo——https://blog.csdn.net/qq_33556185/article/details/51579680——《詳解登錄認證及授權--Shiro系列(一)》
一個和項目基本思路一樣的登錄驗證例子——https://blog.csdn.net/caiqiandu/article/details/88973995——《shiro登陸注冊攔截器》