1.shiro介紹
1.1介紹
shiro是apache的一個開源框架,是一個權限管理的框架,實現 用戶認證、用戶授權。
shiro不依賴於spring,shiro不僅可以實現 web應用的權限管理,還可以實現c/s系統,分布式系統權限管理,shiro屬於輕量框架,越來越多企業項目開始使用shiro。
使用shiro實現系統的權限管理,有效提高開發效率,從而降低開發成本。
1.2shrio功能特點:
1) Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份。
2) Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限。
3) Session Manager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通 JavaSE 環境的,也可以是如 Web 環境的。
4) Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。
5) Web Support:Web支持,可以非常容易的集成到 web 環境。
6) Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率。
7) Concurrency:shiro 支持多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去。
8) Testing:提供測試支持。
9) Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問。
10) Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。
1.3shiro運行原理
1) Subject:主體,可以看到主體可以是任何與應用交互的“用戶”。
2) SecurityManager:相當於 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具體的交互都通過 SecurityManager 進行控制。它管理着所有 Subject、且負責進行認證和授權、及會話、緩存的管理。
3) Authenticator:認證器,負責主體認證的,這是一個擴展點,如果用戶覺得 Shiro 默認的不好,我們可以自定義實現。其需要認證策略(Authentication Strategy),即什么情況下算用戶認證通過了。
4) Authrizer:授權器,或者訪問控制器。它用來決定主體是否有權限進行相應的操作,即控制着用戶能訪問應用中的哪些功能。
5) Realm:可以有1個或多個 Realm,可以認為是安全實體數據源,即用於獲取安全實體的。它可以是 JDBC 實現,也可以是 LDAP 實現,或者內存實現等。
6) SessionManager:如果寫過 Servlet 就應該知道 Session 的概念,Session 需要有人去管理它的生命周期,這個組件就是 SessionManager。而 Shiro 並不僅僅可以用在 Web 環境,也可以用在如普通的 JavaSE 環境。
7) SessionDAO:DAO 大家都用過,數據訪問對象,用於會話的 CRUD。我們可以自定義 SessionDAO 的實現,控制 session 存儲的位置。如通過 JDBC 寫到數據庫或通過 jedis 寫入 redis 中。另外 SessionDAO 中可以使用 Cache 進行緩存,以提高性能。
8) CacheManager:緩存管理器。它來管理如用戶、角色、權限等的緩存的。因為這些數據基本上很少去改變,放到緩存中后可以提高訪問的性能。
9) Cryptography:密碼模塊,Shiro 提高了一些常見的加密組件用於如密碼加密/解密的。
1.4過濾器
當 Shiro 被運用到 web 項目時,Shiro 會自動創建一些默認的過濾器對客戶端請求進行過濾。以下是 Shiro 提供的過濾器:
過濾器簡稱 |
對應的 Java 類 |
anon |
org.apache.shiro.web.filter.authc.AnonymousFilter |
authc |
org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic |
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms |
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port |
org.apache.shiro.web.filter.authz.PortFilter |
rest |
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles |
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl |
org.apache.shiro.web.filter.authz.SslFilter |
user |
org.apache.shiro.web.filter.authc.UserFilter |
logout |
org.apache.shiro.web.filter.authc.LogoutFilter |
noSessionCreation |
org.apache.shiro.web.filter.session.NoSessionCreationFilter |
解釋:
/admins/**=anon # 表示該 uri 可以匿名訪問
/admins/**=auth # 表示該 uri 需要認證才能訪問
/admins/**=authcBasic # 表示該 uri 需要 httpBasic 認證
/admins/**=perms[user:add:*] # 表示該 uri 需要認證用戶擁有 user:add:* 權限才能訪問
/admins/**=port[8081] # 表示該 uri 需要使用 8081 端口
/admins/**=rest[user] # 相當於 /admins/**=perms[user:method],其中,method 表示 get、post、delete 等
/admins/**=roles[admin] # 表示該 uri 需要認證用戶擁有 admin 角色才能訪問
/admins/**=ssl # 表示該 uri 需要使用 https 協議
/admins/**=user # 表示該 uri 需要認證或通過記住我認證才能訪問
/logout=logout # 表示注銷,可以當作固定配置
注意:
anon,authcBasic,auchc,user 是認證過濾器。
perms,roles,ssl,rest,port 是授權過濾器。
2.spring+springmvc+mybatis+shrio權限認證
2.1 pom.xml
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-quartz</artifactId> <version>1.2.3</version> </dependency> |
2.2 web.xml配置shiro
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:applicationContext.xml classpath*:applicationContext-shiro.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 用戶單會話過濾器 --> <!-- <filter> <filter-name>userSingleSessionFilter</filter-name> <filter-class>com.vulnverify.web.filter.UserSingleSessionFilter</filter-class> </filter> <filter-mapping> <filter-name>userSingleSessionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>-->
<!-- 字符編碼過濾器 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <!-- shiro權限 --> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- log4j日志 --> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>classpath:log4j.properties</param-value> </context-param> <context-param> <param-name>log4jRefreshInterval</param-name> <param-value>60000</param-value> </context-param> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <!-- springmvc DispatcherServlet --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>/index.jsp</welcome-file> </welcome-file-list> <error-page> <error-code>404</error-code> <location>/page/404</location> </error-page> <error-page> <error-code>500</error-code> <location>/page/500</location> </error-page> <error-page> <exception-type>org.apache.shiro.authz.AuthorizationException</exception-type> <location>/page/401</location> </error-page> <error-page> <error-code>400</error-code> <location>/page/400</location> </error-page> </web-app> |
2.3 shrio配置文件 applicationContext-shiro.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans classpath:schema/spring-beans.xsd http://www.springframework.org/schema/util classpath:schema/spring-util.xsd">
<description>apache shiro配置</description>
<!-- web.xml中shiro的filter對應的bean --> <!-- Shiro 的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證 --> <property name="loginUrl" value="/static/login/loginPanel/login.html"/> <!-- 認證成功跳轉界面 --> <property name="successUrl" value="/static/index.html"/> <!-- 通過unauthorizedUrl指定沒有權限操作時跳轉頁面 --> <property name="unauthorizedUrl" value="/page/401"/> <property name="filterChainDefinitions"> <value> <!-- 靜態資源允許訪問 --> /static/** = anon <!-- 登錄頁允許訪問 --> /user/login = anon /publicKey = anon /page/exception = anon /verificationCode = anon <!-- 其他資源需要認證 --> /** = authc </value> </property> </bean>
<!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 注入realm --> <property name="realms"> <list> <ref bean="securityRealm"/> </list> </property> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="shiroEhcacheManager" /> <!-- 注入sessiong管理器 --> <property name="sessionManager" ref="sessionManager" /> <!-- 記住我 --> <property name="rememberMeManager" ref="rememberMeManager" /> </bean>
<!-- 緩存管理器 使用Ehcache實現 --> <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/> </bean>
<!-- 會話DAO --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.MemorySessionDAO"/> <!-- <bean id="sessionDAO" class="com.vulnverify.web.session.SessionRedisDao"/> -->
<!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- <bean id="sessionManager" class="com.vulnverify.web.security.WebSessionManager"> --> <!-- session的失效時長,單位毫秒 --> <property name="globalSessionTimeout" value="600000" /> <!-- 刪除失效的session --> <property name="deleteInvalidSessions" value="true" /> </bean>
<!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- cookie的name,對應的默認是 JSESSIONID --> <constructor-arg name="name" value="SHAREJSESSIONID" /> <!-- jsessionId的path為 / 用於多個系統共享jsessionId --> <property name="path" value="/" /> <property name="httpOnly" value="true"/> </bean> <!-- rememberMeManager管理器,寫cookie,取出cookie生成用戶信息 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cookie" ref="rememberMeCookie" /> </bean> <!-- 記住我cookie --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- rememberMe是cookie的名字 --> <constructor-arg value="rememberMe" /> <!-- 記住我cookie生效時間30天 --> <property name="maxAge" value="2592000" /> </bean>
<!-- 自定義 realm 安全數據 --> <bean id="securityRealm" class="com.vulnverify.web.security.SecurityRealm"></bean>
<!-- Shiro生命周期處理器 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans> |
2.4 自定義realm 安全數據庫
package com.vulnverify.web.security;
import java.util.List;
import javax.annotation.Resource;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.stereotype.Component;
import com.vulnverify.web.model.TSysRights; import com.vulnverify.web.model.TSysRole; import com.vulnverify.web.model.TUser; import com.vulnverify.web.service.SysRightsService; import com.vulnverify.web.service.SysRoleService; import com.vulnverify.web.service.UserService;
/** * 用戶身份驗證,授權 Realm 組件 * * @author linan **/ @Component(value = "securityRealm") public class SecurityRealm extends AuthorizingRealm {
@Resource private UserService sysUserService;
@Resource private SysRoleService sysRoleService;
@Resource private SysRightsService sysRightsService;
/** * 權限檢查 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); final TUser user = (TUser)SecurityUtils.getSubject().getSession(false).getAttribute("userInfo"); String roleType = user.getUserType();//用戶角色 //角色信息 final TSysRole roleInfos = sysRoleService.selectRoleByRoleType(Integer.parseInt(roleType)); if(null!=roleInfos){ // 添加角色 authorizationInfo.addRole(roleInfos.getRoleName()); //根據角色id查詢角色權限 final List<TSysRights> sysRightsList = sysRightsService.selectSysRightsByRoleId(roleInfos.getRoleId()); for (TSysRights sysRights : sysRightsList) { // 添加權限 authorizationInfo.addStringPermission(sysRights.getRightCode()); } } return authorizationInfo; }
/** * 身份驗證信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //身份 String loginName = String.valueOf(token.getPrincipal()); //密碼 String password = new String((char[]) token.getCredentials()); TUser su = new TUser(); su.setUserAccount(loginName); su.setPassword(password); // 通過數據庫進行驗證 final TUser authentication = sysUserService.authentication(su); if (authentication == null) { throw new AuthenticationException("用戶名或密碼錯誤."); } //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginName, password, getName()); return authenticationInfo; }
} |
2.5 登陸controller
package com.vulnverify.web.controller;
import java.util.Date;
import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.validation.Valid;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody;
import com.vulnverify.core.entity.PageData; import com.vulnverify.core.entity.PageQuery; import com.vulnverify.core.entity.SimpleException; import com.vulnverify.core.orm.mybatis.Page; import com.vulnverify.core.redis.RedisDb; import com.vulnverify.core.utils.ApplicationUtils; import com.vulnverify.core.utils.DateUtil; import com.vulnverify.web.constant.Constant; import com.vulnverify.web.model.TUser; import com.vulnverify.web.model.requestbody.IdReqBody; import com.vulnverify.web.model.requestbody.LoginReqBody; import com.vulnverify.web.model.requestbody.UserCreateReqBody; import com.vulnverify.web.model.requestbody.UserListReqBody; import com.vulnverify.web.model.requestbody.UserModifyReqBody; import com.vulnverify.web.model.responsebody.LoginResBody; import com.vulnverify.web.model.view.UserView; import com.vulnverify.web.service.UserService;
/** * 用戶控制類 * @author linan * @date 2018年4月23日 * */ @Controller @RequestMapping(value="/user") public class UserController extends BaseController{ private static Logger logger=LoggerFactory.getLogger(UserController.class); @Resource private UserService userService; /**驗證碼驗證是否開啟的標識*/ @Value("${verfication.code.check}") private String verficationCodeCheck = "true";
/** * 登陸 * @param loginBody * @param request * @param response * @return * @throws Exception */ @RequestMapping(value = "/login", method = RequestMethod.POST) public Object login(@Valid @RequestBody LoginReqBody loginBody,HttpServletRequest request,HttpServletResponse response) throws Exception{ Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(false); try{
//驗證碼 /*String verificationCode = (String)session.getAttribute("verificationCode"); if("true".equals(verficationCodeCheck)){ if(verificationCode == null || !verificationCode.equalsIgnoreCase(loginBody.getVerificationCode())){ throw new SimpleException(Constant.EXCEPTION_S0010005, ApplicationUtils.getMessage(Constant.EXCEPTION_S0010005)); } }*/
if (subject.isAuthenticated()) { throw new SimpleException(Constant.EXCEPTION_S0010006, ApplicationUtils.getMessage(Constant.EXCEPTION_S0010006)); }
final TUser authUserInfo = userService.getUserByUserAccout(loginBody.getUserAccount()); if(authUserInfo != null){ if(authUserInfo.getStatus() == Constant.USER_STATE_UNABLE){ throw new Exception("用戶"+authUserInfo.getUserAccount()+"已被停用"); } } String sha256Hex = ApplicationUtils.sha256Hex(loginBody.getPassword()); String password = authUserInfo.getPassword(); if(sha256Hex.equals(password)){ System.out.println("------true---------"); } //調用shrio的自定義realm的doGetAuthenticationInfo驗證身份 subject.login( new UsernamePasswordToken( loginBody.getUserAccount(), ApplicationUtils.sha256Hex(loginBody.getPassword())));
session.setAttribute("userInfo", authUserInfo); session.setAttribute("userKey",loginBody.getUserKey()); logger.info("login userKey is "+session.getId()+":"+loginBody.getUserKey());
/* String key = "loginUser."+authUserInfo.getUserAccount(); RedisDb.setString(key, session.getId().toString()); RedisDb.expireString(key, 1800);*/
LoginResBody lrb = new LoginResBody(); lrb.setId(authUserInfo.getUserId()+""); lrb.setUserAccount(authUserInfo.getUserAccount()); lrb.setUserName(authUserInfo.getUserName());
return generateResultData(lrb); }catch(Exception e){ TUser sysUser = userService.getUserByUserAccout(loginBody.getUserAccount()); if(sysUser != null){ session.setAttribute("failUserInfo", sysUser); // ApplicationUtils.optData2Request(sysUser.getUserName()); } throw e; }finally{ session.removeAttribute("verificationCode"); } } } |
2.6權限驗證:
2.6.1 shiro什么時候會進入doGetAuthorizationInfo(PrincipalCollection principals)
會進入授權方法一共有三種情況!
1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去調用這個是否有什么角色或者是否有什么權限的時候;
2、在方法上加注解的時候
@RequiresRoles("admin") :角色驗證
@RequiresPermissions(value = PermissionSign.GET_ORGIP_LIST)權限驗證
PermissionSign為常量類。與realm中授權的添加權限對應。
3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在頁面上加shiro標簽的時候,即進這個頁面的時候掃描到有這個標簽的時候。
2.7運行流程:
1.登陸調用userController登陸中的 subject.login(new UsernamePasswordToken( loginBody.getUserAccount(), ApplicationUtils.sha256Hex(loginBody.getPassword())));方法
2.調用自定義realm中的doGetAuthenticationInfo(AuthenticationToken token)進行身份登陸驗證。
3.方法中
@RequiresPermissions()權限會去自定義realm的授權接口doGetAuthorizationInfo(PrincipalCollection principals) 去授權,然后判斷是否有操作此方法的權限。
3.參考文檔:
https://www.cnblogs.com/moonlightL/p/8126910.html
https://blog.csdn.net/mine_song/article/details/61616259
http://jinnianshilongnian.iteye.com/blog/2022468