Shiro是apache旗下一個開源安全框架(http://shiro.apache.org/),它將軟件系統的安全認證相關的功能抽取出來,實現用戶身份認證,權限受權、加密、會話管理等功能,組成了一個通用的安全認證框架。使用shiro就能夠很是快速的完成認證、受權等功能的開發,下降系統成本。html
用戶在進行資源訪問時,要求系統要對用戶進行權限控制,其具體流程如圖所示:web
在概念層面,Shiro 架構包含三個主要的理念,如圖所示:redis
其中:算法
Shiro框架進行權限管理時,要涉及到的一些核心對象,主要包括:認證管理對象,受權管理對象,會話管理對象,緩存管理對象,加密管理對象以及Realm管理對象(領域對象:負責處理認證和受權領域的數據訪問題)等,其具體架構如圖-3所示:spring
其中:chrome
使用spring整合shiro時,須要在pom.xml中添加以下依賴:數據庫
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency>
基於SpringBoot 實現的項目中,咱們的shiro應用基本配置以下:。apache
第一步:建立SpringShiroConfig類。關鍵代碼以下:segmentfault
package com.cy.pj.common.config; /**@Configuration 註解描述的類爲一個配置對象, * 此對象也會交給spring管理 */ @Configuration public class SpringShiroConfig { }
第二步:在Shiro配置類中添加SecurityManager配置(這裏必定要使用org.apache.shiro.mgt.SecurityManager這個接口對象),關鍵代碼以下:瀏覽器
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); return sManager; }
第三步: 在Shiro配置類中添加ShiroFilterFactoryBean對象的配置。經過此對象設置資源匿名訪問、認證訪問。關鍵代碼以下:
其配置過程當中,對象關係以下圖所示:
當服務端攔截到用戶請求之後,斷定此請求是否已經被認證,假如沒有認證應該先跳轉到登陸頁面。
第一步:在PageController中添加一個呈現登陸頁面的方法,關鍵代碼以下:
@RequestMapping("doLoginUI") public String doLoginUI(){ return "login"; }
第二步:修改SpringShiroConfig類中shiroFilterFactorybean的配置,添加登錄url的設置。關鍵代碼見sfBean.setLoginUrl("/doLoginUI")部分。
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); sfBean.setLoginUrl("/doLoginUI"); //定義map指定請求過濾規則(哪些資源容許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //靜態資源容許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/modules/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); //除了匿名訪問的資源,其它都要認證("authc")後訪問 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
在/templates/pages/添加一個login.html頁面,而後將項目部署到web服務器,並啓動測試運行.
具體代碼見項目中login.html。
身份認證即斷定用戶是不是系統的合法用戶,用戶訪問系統資源時的認證(對用戶身份信息的認證)流程圖所示:
其中認證流程分析以下:
思考:不使用shiro框架如何完成認證操做?filter,intercetor。
認證業務API處理流程分析,如圖所示:
在用戶數據層對象SysUserDao中,按特定條件查詢用戶信息,並對其進行封裝。
在SysUserDao接口中,添加根據用戶名獲取用戶對象的方法,關鍵代碼以下:
SysUser findUserByUserName(String username)。
根據SysUserDao中定義的方法,在SysUserMapper文件中添加元素定義。
基於用戶名獲取用戶對象的方法,關鍵代碼以下:
<select id="findUserByUserName" resultType="com.cy.pj.sys.entity.SysUser"> select * from sys_users where username=#{username} </select>
本模塊的業務在Realm類型的對象中進行實現,咱們編寫realm時,要繼承
AuthorizingRealm並重寫相關方法,完成認證及受權業務數據的獲取及封裝。
第一步:定義ShiroUserRealm類,關鍵代碼以下:
package com.cy.pj.sys.service.realm; @Service public class ShiroUserRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; /** * 設置憑證匹配器(與用戶添加操做使用相同的加密算法) */ @Override public void setCredentialsMatcher( CredentialsMatcher credentialsMatcher) { //構建憑證匹配對象 HashedCredentialsMatcher cMatcher= new HashedCredentialsMatcher(); //設置加密算法 cMatcher.setHashAlgorithmName("MD5"); //設置加密次數 cMatcher.setHashIterations(1); super.setCredentialsMatcher(cMatcher); } /** * 經過此方法完成認證數據的獲取及封裝,系統 * 底層會將認證數據傳遞認證管理器,由認證 * 管理器完成認證操做。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //1.獲取用戶名(用戶頁面輸入) UsernamePasswordToken upToken= (UsernamePasswordToken)token; String username=upToken.getUsername(); //2.基於用戶名查詢用戶信息 SysUser user= sysUserDao.findUserByUserName(username); //3.斷定用戶是否存在 if(user==null) throw new UnknownAccountException(); //4.斷定用戶是否已被禁用。 if(user.getValid()==0) throw new LockedAccountException(); //5.封裝用戶信息 ByteSource credentialsSalt= ByteSource.Util.bytes(user.getSalt()); //記住:構建什麼對象要看方法的返回值 SimpleAuthenticationInfo info= new SimpleAuthenticationInfo( user,//principal (身份) user.getPassword(),//hashedCredentials credentialsSalt, //credentialsSalt getName());//realName //6.返回封裝結果 return info;//返回值會傳遞給認證管理器(後續 //認證管理器會經過此信息完成認證操做) } .... }
第二步:對此realm,須要在SpringShiroConfig配置類中,注入給SecurityManager對象,修改securityManager方法,見黃色背景部分,例如:
@Bean public SecurityManager securityManager(Realm realm) { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); sManager.setRealm(realm); return sManager; }
在此對象中定義相關方法,處理客戶端的登錄請求,例如獲取用戶名,密碼等而後提交該shiro框架進行認證。
第一步:在SysUserController中添加處理登錄的方法。關鍵代碼以下:
@RequestMapping("doLogin") public JsonResult doLogin(String username,String password){ //1.獲取Subject對象 Subject subject=SecurityUtils.getSubject(); //2.經過Subject提交用戶信息,交給shiro框架進行認證操做 //2.1對用戶進行封裝 UsernamePasswordToken token= new UsernamePasswordToken( username,//身份信息 password);//憑證信息 //2.2對用戶信息進行身份認證 subject.login(token); //分析: //1)token會傳給shiro的SecurityManager //2)SecurityManager將token傳遞給認證管理器 //3)認證管理器會將token傳遞給realm return new JsonResult("login ok"); }
第二步:修改shiroFilterFactory的配置,對/user/doLogin這個路徑進行匿名訪問的配置,查看以下黃色標記部分的代碼:
@Bean public ShiroFilterFactoryBean shiroFilterFactory ( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //假如沒有認證請求先訪問此認證的url sfBean.setLoginUrl("/doLoginUI"); //定義map指定請求過濾規則(哪些資源容許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //靜態資源容許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); map.put("/user/doLogin","anon"); //authc表示,除了匿名訪問的資源,其它都要認證("authc")後才能訪問訪問 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
第三步:當咱們在執行登陸操做時,爲了提升用戶體驗,可對系統中的異常信息進行處理,例如,在統一異常處理類中添加以下方法:
@ExceptionHandler(ShiroException.class) @ResponseBody public JsonResult doHandleShiroException( ShiroException e) { JsonResult r=new JsonResult(); r.setState(0); if(e instanceof UnknownAccountException) { r.setMessage("帳戶不存在"); }else if(e instanceof LockedAccountException) { r.setMessage("帳戶已被禁用"); }else if(e instanceof IncorrectCredentialsException) { r.setMessage("密碼不正確"); }else if(e instanceof AuthorizationException) { r.setMessage("沒有此操做權限"); }else { r.setMessage("系統維護中"); } e.printStackTrace(); return r; }
在/templates/pages/目錄下添加登錄頁面(login.html)。
點擊登陸操做時,將輸入的用戶名,密碼異步提交到服務端。
$(function () { $(".login-box-body").on("click",".btn",doLogin); }); function doLogin(){ var params={ username:$("#usernameId").val(), password:$("#passwordId").val() } var url="user/doLogin"; $.post(url,params,function(result){ if(result.state==1){ //跳轉到indexUI對應的頁面 location.href="doIndexUI?t="+Math.random(); }else{ $(".login-box-msg").html(result.message); } }); }
在SpringShiroConfig配置類中,修改過濾規則,添加黃色標記部分代碼的配置,請看以下代碼:
@Bean public ShiroFilterFactoryBean shiroFilterFactory( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //假如沒有認證請求先訪問此認證的url sfBean.setLoginUrl("/doLoginUI"); //定義map指定請求過濾規則(哪些資源容許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map=new LinkedHashMap<>(); //靜態資源容許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); map.put("/user/doLogin","anon"); map.put("/doLogout","logout"); //除了匿名訪問的資源,其它都要認證("authc")後訪問 map.put("/**","authc"); sfBean.setFilterChainDefinitionMap(map); return sfBean; }
受權即對用戶資源訪問的受權(是否容許用戶訪問此資源),用戶訪問系統資源時的受權流程如圖所示:
其中受權流程分析以下:
思考:思考不使用shiro如何完成受權操做?intercetor,aop。
在SpringShiroConfig配置類中,添加受權時的相關配置:
第一步:配置bean對象的生命週期管理(SpringBoot能夠不配置)。
@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }
第二步: 經過以下配置要爲目標業務對象建立代理對象(SpringBoot中可省略)。
@DependsOn("lifecycleBeanPostProcessor") @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { return new DefaultAdvisorAutoProxyCreator(); }
第三步:配置advisor對象,shiro框架底層會經過此對象的matchs方法返回值(相似切入點)決定是否建立代理對象,進行權限控制。
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor ( SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor= new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } 說明:使用框架最重要的尊重規則,框架規則指定了什麼方式就使用什麼方式。
說明:使用框架最重要的尊重規則,框架規則指定了什麼方式就使用什麼方式。
受權時,服務端核心業務以及API分析,如圖所示:
基於登錄用戶ID,認證信息獲取登錄用戶的權限信息,並進行封裝。
第一步:在SysUserRoleDao中定義基於用戶id查找角色id的方法(假如方法已經存在則無需再寫),關鍵代碼以下:
List<Integer> findRoleIdsByUserId(Integer id);
第二步:在SysRoleMenuDao中定義基於角色id查找菜單id的方法,關鍵代碼以下:
List<Integer> findMenuIdsByRoleIds( @Param("roleIds")List<Integer> roleIds);
第三步:在SysMenuDao中基於菜單id查找權限標識的方法,關鍵代碼以下:
List<String> findPermissions( @Param("menuIds") List<Integer> menuIds);
基於Dao中方法,定義映射元素。
第一步:在SysUserRoleMapper中定義findRoleIdsByUserId元素。關鍵代碼以下:
<select id="findRoleIdsByUserId" resultType="int"> select role_id from sys_user_roles where user_id=#{userId} </select>
第二步:在SysRoleMenuMapper中定義findMenuIdsByRoleIds元素。關鍵代碼以下:
<select id="findMenuIdsByRoleIds" resultType="int"> select menu_id from sys_role_menus where role_id in <foreach collection="roleIds" open="(" close=")" separator="," item="item"> #{item} </foreach> </select>
第三步:在SysMenuMapper中定義findPermissions元素,關鍵代碼以下:
<select id="findPermissions" resultType="string"> select permission <!-- sys:user:update --> from sys_menus where id in <foreach collection="menuIds" open="(" close=")" separator="," item="item"> #{item} </foreach> </select>
在ShiroUserReam類中,重寫對象realm的doGetAuthorizationInfo方法,並完成用戶權限信息的獲取以及封裝,最後將信息傳遞給受權管理器完成受權操做。
修改ShiroUserRealm類中的doGetAuthorizationInfo方法,關鍵代碼以下:
@Service public class ShiroUserRealm extends AuthorizingRealm { @Autowired private SysUserDao sysUserDao; @Autowired private SysUserRoleDao sysUserRoleDao; @Autowired private SysRoleMenuDao sysRoleMenuDao; @Autowired private SysMenuDao sysMenuDao; /**經過此方法完成受權信息的獲取及封裝*/ @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //1.獲取登陸用戶信息,例如用戶id SysUser user=(SysUser)principals.getPrimaryPrincipal(); Integer userId=user.getId(); //2.基於用戶id獲取用戶擁有的角色(sys_user_roles) List<Integer> roleIds= sysUserRoleDao.findRoleIdsByUserId(userId); if(roleIds==null||roleIds.size()==0) throw new AuthorizationException(); //3.基於角色id獲取菜單id(sys_role_menus) List<Integer> menuIds= sysRoleMenuDao.findMenuIdsByRoleIds(roleIds); if(menuIds==null||menuIds.size()==0) throw new AuthorizationException(); //4.基於菜單id獲取權限標識(sys_menus) List<String> permissions= sysMenuDao.findPermissions(menuIds); //5.對權限標識信息進行封裝並返回 Set<String> set=new HashSet<>(); for(String per:permissions){ if(!StringUtils.isEmpty(per)){ set.add(per); } } SimpleAuthorizationInfo info= new SimpleAuthorizationInfo(); info.setStringPermissions(set); return info;//返回給受權管理器 } 。。。。 }
在須要進行受權訪問的業務層(Service)方法上,添加執行此方法須要的權限標識,參考代碼
@RequiresPermissions(「sys:user:update」)
說明:此要註解必定要添加到業務層方法上。
當咱們進行受權操做時,每次都會從數據庫查詢用戶權限信息,爲了提升受權性能,能夠將用戶權限信息查詢出來之後進行緩存,下次受權時從緩存取數據便可。
Shiro中內置緩存應用實現,其步驟以下:
第一步:在SpringShiroConfig中配置緩存Bean對象(Shiro框架提供)。
@Bean public CacheManager shiroCacheManager(){ return new MemoryConstrainedCacheManager(); }
說明:這個CacheManager對象的名字不能寫cacheManager,由於spring容器中已經存在一個名字爲cacheManager的對象了.
第二步:修改securityManager的配置,將緩存對象注入給SecurityManager對象。
@Bean public SecurityManager securityManager( Realm realm, CacheManager cacheManager) { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); sManager.setRealm(realm); sManager.setCacheManager(cacheManager); return sManager; }
說明:對於shiro框架而言,還能夠藉助第三方的緩存產品(例如redis)對用戶的權限信息進行cache操做.
記住我功能是要在用戶登陸成功之後,假如關閉瀏覽器,下次再訪問系統資源(例如首頁doIndexUI)時,無需再執行登陸操做。
在頁面上選中記住我,而後執行提交操做,將用戶名,密碼,記住我對應的值提交到控制層,如圖所示:
其客戶端login.html中關鍵JS實現:
function doLogin(){ var params={ username:$("#usernameId").val(), password:$("#passwordId").val(), isRememberMe:$("#rememberId").prop("checked"), } var url="user/doLogin"; console.log("params",params); $.post(url,params,function(result){ if(result.state==1){ //跳轉到indexUI對應的頁面 location.href="doIndexUI?t="+Math.random(); }else{ $(".login-box-msg").html(result.message); } return false;//防止刷新時重複提交 }); }
服務端業務實現的具體步驟以下:
第一步:在SysUserController中的doLogin方法中基因而否選中記住我,設置token的setRememberMe方法。
@RequestMapping("doLogin") @ResponseBody public JsonResult doLogin( boolean isRememberMe, String username, String password) { //1.封裝用戶信息 UsernamePasswordToken token= new UsernamePasswordToken(username, password); if(isRememberMe) { token.setRememberMe(true); } //2.提交用戶信息 Subject subject=SecurityUtils.getSubject(); subject.login(token);//token會提交給securityManager return new JsonResult("login ok"); }
第二步:在SpringShiroConfig配置類中添加記住我配置,關鍵代碼以下:
@Bean public RememberMeManager rememberMeManager() { CookieRememberMeManager cManager= new CookieRememberMeManager(); SimpleCookie cookie=new SimpleCookie("rememberMe"); cookie.setMaxAge(7*24*60*60); cManager.setCookie(cookie); return cManager; }
第三步:在SpringShiroConfig中修改securityManager的配置,爲securityManager注入rememberManager對象。參考黃色部分代碼。
@Bean public SecurityManager securityManager( Realm realm,CacheManager cacheManager RememberMeManager rememberManager) { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); sManager.setRealm(realm); sManager.setCacheManager(cacheManager); sManager.setRememberMeManager(rememberManager); return sManager; }
第四步:修改shiro的過濾認證級別,將/=author修改成/=user,查看黃色背景部分。
@Bean public ShiroFilterFactoryBean shiroFilterFactory( SecurityManager securityManager) { ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean(); sfBean.setSecurityManager(securityManager); //假如沒有認證請求先訪問此認證的url sfBean.setLoginUrl("/doLoginUI"); //定義map指定請求過濾規則(哪些資源容許匿名訪問,哪些必須認證訪問) LinkedHashMap<String,String> map= new LinkedHashMap<>(); //靜態資源容許匿名訪問:"anon" map.put("/bower_components/**","anon"); map.put("/build/**","anon"); map.put("/dist/**","anon"); map.put("/plugins/**","anon"); map.put("/user/doLogin","anon"); map.put("/doLogout", "logout");//自動查LoginUrl //除了匿名訪問的資源,其它都要認證("authc")後訪問 map.put("/**","user");//authc sfBean.setFilterChainDefinitionMap(map); return sfBean; }
說明:查看瀏覽器cookie設置,可在瀏覽器中輸入以下語句。
chrome://settings/content/cookies
使用shiro框架實現認證操做,用戶登陸成功會將用戶信息寫入到會話對象中,其默認時長爲30分鐘,假如須要對此進行配置,可參考以下配置:
第一步:在SpringShiroConfig類中,添加會話管理器配置。關鍵代碼以下:
@Bean public SessionManager sessionManager() { DefaultWebSessionManager sManager= new DefaultWebSessionManager(); sManager.setGlobalSessionTimeout(60*60*1000); return sManager; }
第二步:在SpringShiroConfig配置類中,對安全管理器 securityManager 增長 sessionManager值的注入,關鍵代碼以下:
@Bean public SecurityManager securityManager( Realm realm,CacheManager cacheManager, RememberMeManager rememberManager, SessionManager sessionManager) { DefaultWebSecurityManager sManager= new DefaultWebSecurityManager(); sManager.setRealm(realm); sManager.setCacheManager(cacheManager); sManager.setRememberMeManager(rememberMeManager); sManager.setSessionManager(sessionManager); return sManager; }
課堂練習:
1.獲取用戶登錄信息,並將登錄用戶名呈如今系統主頁(starter.html)上.
第一步:定義一個工具類(ShiroUtils),獲取用戶登錄信息.
package com.cy.pj.common.util; import org.apache.shiro.SecurityUtils; import com.cy.pj.sys.entity.SysUser; public class ShiroUtils { public static String getUsername() { return getUser().getUsername(); } public static SysUser getUser() { return (SysUser) SecurityUtils.getSubject().getPrincipal(); } }
第二步:修改PageController中的doIndexUI方法,代碼以下:
@RequestMapping("doIndexUI") public String doIndexUI(Model model) { SysUser user=ShiroUtils.getUser(); model.addAttribute("user",user); return "starter"; }
第三步:藉助thymeleaf中的表達式直接在頁面上(starter.html)獲取登錄用戶信息
<span class="hidden-xs" id="loginUserId">[[${user.username}]]</span>
2.修改登錄用戶的密碼?(參考用戶模塊文檔)
分析:
1)肯定都要修改誰?(密碼,鹽值,修改時間)
2)服務端的設計實現?(dao,service,controller)
3)客戶端的設計實現?(異步提交用戶密碼信息)