權限管理,可以實現用戶訪問系統的控制,權限管理分爲認證和授權
Shiro(Apache Shiro)是Java的一個安全框架,Shiro屬於輕量級框架,可以實現認證、授權、加密、會話管理、與Web集成、緩存等
Shiro核心類示意圖
主要類:
Authentication: 身份認證
Authorization: 授權,即權限驗證,驗證某個已認證用戶是否擁有某個權限
Seesion Manager: 會話管理,每次用戶登錄都是一次會話,在沒有退出之前,他的所有信息都在會話中
Cryptography: 加密,保護數據的安全性
Web Support: Web支持,可以容易的集成到Web環境中
Catching: 緩存,用戶登錄後,其用戶信息、所擁有的角色、權限不用每次去查,可以提高效率
Concurrency: shiro支持多線程應用的併發驗證,即在一個線程中開啓另一個線程,能把權限自動傳播過去
Testing: 提供測試支持
Run As: 允許一個用戶以另一個用戶的身份進行訪問
Remember Me: 記住我,即一次登錄後,下次就不用登錄了
Shiro架構圖:
主要組件:
Subject: 主體,主體可以是用戶也可以是程序,主體要訪問系統,系統需要對主體進行認證、授權。
SecurityManager: 安全管理器,主體進行認證和授權都是通過securityManager進行
Authenticator: 認證器,主體進行認證最終通過Authenticator進行的。
Authorizer: 授權器,主體進行授權最終通過authenticator進行的。
SessionManager: 會話管理,web應用中一般是用web容器對session進行管理,Shiro也提供一套session管理的方式。
SessionDao: 通過SessionDao管理Session數據
CacheManager: 緩存管理器,主要對Session和授權數據進行緩存
Realm: 領域,相當於數據源,通過Realm存取認證、授權相關數據
Cryptography: 密碼管理,提供一套加密/解密的組件,便於開發
登錄認證
1. 整合Shiro,添加Shiro所需要的pom依賴
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- shiro核心jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro整合到web工程所依賴的jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro緩存依賴的jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro整合與Spring的整合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2. 在spring當中配置shiro過濾器和安全管理器
<!-- 配置shiro過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--配置登錄認證的路徑
如果請求是login,則做去執行認證操作(調用對應的Realm執行認證操作);
如果請求不是login請求,且沒有認證,則會直接執行login請求(下面貼有login請求對應的方法)
-->
<property name="loginUrl" value="/login"/>
<property name="securityManager" ref="securityManager"></property>
<!-- 配置shiro過濾器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登錄驗證-->
/login.jsp = anon <!--不需要登錄驗證-->
/**=authc <!--除指定請求外,其它所有的請求都需要身份驗證-->
</value>
</property>
</bean>
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"></bean>
login請求對應的控制器方法
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
return "redirect:login.jsp";
}
}
3. 在web.xml當中配置過濾器攔截所有請求,進行處理
<!-- 攔截到所有請求,使用spring一個bean(ShiroFilter)來進行處理 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 是否filter中的init和 destroy-->
<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>
4. 配置及編寫用於認證操作的Realm
把自己寫的自定義Realm交給Spring管理
<!--自定義realm-->
<!-- 配置realm數據源 -->
<!-- class爲自己編寫的realm -->
<bean id="employeeRealm" class="com.eh.web.realm.EmployeeRealm"></bean>
並將Realm配置到安全管理器中
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
<!-- 注入緩存-->
<property name="cacheManager" ref="ehCache"/>
</bean>
自定義realm編寫認證操作
public class EmployeeRealm extends AuthorizingRealm {
@Autowired
private EmployeeService employeeService;
//認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("來到了認證");
//獲取表單傳來的參數(表單傳的參數已經被封裝到token中)
String username = (String) token.getPrincipal();
System.out.println(username);
//到數據庫中查詢有沒有當前用戶
Employee employee = employeeService.getEmployeeWithName(username);
System.out.println(employee);
if (employee == null) {
//沒有該用戶
return null;
}
//認證
//參數: 主體(當前用戶) 正確的密碼,鹽(此處省略沒寫,下面會單獨介紹),當前realm名稱
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(employee, employee.getPassword(), this.getName());
//將認證信息返回(成功還是失敗) 定義一個過濾器,用來監聽該認證是否成功
return info;
}
}
5. 編寫用於監聽認證是否成功的過濾器
將該過濾器交給Spring管理
<!-- 配置自定義的監聽的過濾器-->
<bean id="myFormFilter" class="com.eh.web.filter.MyFormFilter"></bean>
<!-- 配置shiro過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--配置登錄認證的路徑
如果請求是login,則做去執行認證操作(調用對應的Realm執行認證操作);
如果請求不是login請求,且沒有認證,則會直接執行login請求(下面貼有login請求對應的方法)
-->
<property name="loginUrl" value="/login"/>
<!--將監聽的過濾器配置到shiro中-->
<property name="filters">
<map>
<entry key="authc" value-ref="myFormFilter"/>
</map>
</property>
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
</bean>
<!-- 配置shiro過濾器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登錄驗證-->
/login.jsp = anon <!--不需要登錄驗證-->
/logout = logout <!--取消認證-->
/**=authc <!--除指定請求外,其它所有的請求都需要身份驗證-->
</value>
</property>
</bean>
//表單認證過濾器
public class MyFormFilter extends FormAuthenticationFilter {
//攔截成功
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
System.out.println("認證成功");
AjaxRes ajaxRes = new AjaxRes();
ajaxRes.setSuccess(true);
ajaxRes.setMsg("登錄成功");
//把對象轉換成json類型的字符串(前端請求使用的ajax請求)
String JsonString = new ObjectMapper().writeValueAsString(ajaxRes);
//設置字符集編碼
response.setCharacterEncoding("utf-8");
//相應給瀏覽器
response.getWriter().print(JsonString);
//返回false 表示不需要執行下一個攔截器
return false;
}
//攔截失敗
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
System.out.println("認證失敗");
AjaxRes ajaxRes = new AjaxRes();
//根據不同的異常相應不同的信息
if (e != null) {
//獲取異常名稱
String name = e.getClass().getName();
if (name.equals(UnknownAccountException.class.getName())) {
//賬號錯誤
ajaxRes.setSuccess(false);
ajaxRes.setMsg("賬號不存在");
} else if (name.equals(IncorrectCredentialsException.class.getName())) {
//密碼錯誤
ajaxRes.setSuccess(false);
ajaxRes.setMsg("密碼錯誤");
} else {
//未知異常
ajaxRes.setSuccess(false);
ajaxRes.setMsg("未知錯誤");
}
}
//設置字符集編碼
response.setCharacterEncoding("utf-8");
try {
//把對象轉換成json類型的字符串
String JsonString = new ObjectMapper().writeValueAsString(ajaxRes);
response.setCharacterEncoding("utf-8");
//相應給瀏覽器
response.getWriter().print(JsonString);
} catch (IOException ex) {
ex.printStackTrace();
}
//返回false 表示不需要執行下一個攔截器
return false;
}
}
瀏覽器請求及相應轉發代碼
$.post("login", $("form").serialize(), function (data) {
//把json格式的字符串轉換成json數據
data = $.parseJSON(data)
if (data.success) {
//成功
window.location.href = "${pageContext.request.contextPath}/index.jsp";
} else {
alert(data.msg);
}
})
用戶退出功能的實現:
前端界面:
<a style="font-size: 18px; color: white;text-decoration: none;" href="${pageContext.request.contextPath}/logout">註銷</a>
在shiro過濾器中添加logout的處理
<!-- 配置shiro過濾器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登錄驗證-->
/login.jsp = anon <!--不需要登錄驗證-->
/logout = logout <!--取消認證-->
/**=authc <!--除指定請求外,其它所有的請求都需要身份驗證-->
</value>
</property>
這樣整個認證登錄過程就實現實現了
用戶授權
授權方法調用的時機:
- 有shiro標籤的使用
<shiro:hasPermission name="employee:delete">
<a href="#" class="easyui-linkbutton" data-options="iconCls:'icon-remove',plain:true" id="delete">離職</a>
</shiro:hasPermission>
- 有權限(shiro)的註解
@RequiresPermissions("employee:index")
@RequestMapping("/employee")
public String employee() {
return "employee";
}
注:在使用shiro註解時,需要在配置文件彙總添加shiro的註解掃描:
<!--
配置爲true即使用cglib繼承的方式,
false爲jdk的接口動態代理 控制器沒有實現接口
-->
<aop:config proxy-target-class="true" ></aop:config>
<!-- 使用第三方去掃描shiro的註解 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor ">
<property name="securityManager" ref="securityManager"></property>
</bean>
授權方法
//授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("授權");
Employee employee = (Employee) principal.getPrimaryPrincipal();
//查詢角色和對應的權限
List<String> roles = new ArrayList<>();
List<String > permissions = new ArrayList<>();
//判斷當前用戶是否是管理員, 如果是管理員則擁有所有權限
if (employee.getAdmin()) {
//擁有所有權限
permissions.add("*:*");
} else {
//查詢角色
roles = employeeService.getRolesById(employee.getId());
System.out.println("------------------" + roles);
//查詢權限
permissions = employeeService.getPermissionsById(employee.getId());
System.out.println("------------------" + permissions);
}
//賦給授權信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
沒有權限結果處理,當沒有權限訪問某一頁面時,會報500錯誤,這時候可以定義一個處理沒有權限的方法
//未授權異常
@ExceptionHandler(AuthorizationException.class)
public void ExceptionHandler(HandlerMethod method, HttpServletResponse response) throws IOException {//拿取發生異常的方法
//判斷當前請求是不是Json(AJAX)請求,如果是(則在該方法中無法實現跳轉),返回json數據給瀏覽器,讓其自己做跳轉
//獲取發生異常的方法
//獲取方法上的註解,通過方法上是否有@ResponseBody註解來判斷
ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class);
if (responseBody != null) {
//則該請求爲(Ajax)json請求
AjaxRes ajaxRes = new AjaxRes();
ajaxRes.setMsg("沒有權限操作");
ajaxRes.setSuccess(false);
String s = new ObjectMapper().writeValueAsString(ajaxRes);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
} else {
response.sendRedirect("nopermission.jsp");
}
}
這樣就實現了用戶授權
補充:
Shiro還有一些比較常用但前面還沒有介紹的業務,這裏補充說明一下
Shiro緩存的使用
- 所需要的pom依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
- 添加shiro緩存配置
添加shiro-ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
- 將該緩存配置添加到安全管理器(SecurityManager)中
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
<!--注入緩存-->
<property name="cacheManager" ref="ehCache"/>
</bean>
Shiro密碼加密
- 添加憑證匹配器
<!-- 憑證匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 散列算法 -->
<property name="hashAlgorithmName" value="md5"/>
<!-- 散列次數 -->
<property name="hashIterations" value="2"></property>
</bean>
並將該憑證匹配器配置到自定義Realm(數據源)中
<!--自定義realm-->
<!-- 配置realm數據源 -->
<bean id="employeeRealm" class="com.eh.web.realm.EmployeeRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
- 在保存用戶時, 給用戶密碼進行加密處理
@Override
public void saveEmployee(Employee employee) {
//把密碼進行加密,把該用戶名當做鹽 散列次數2
Md5Hash md5Hash = new Md5Hash(employee.getPassword(), employee.getUsername(), 2);
employee.setPassword(md5Hash.toString());
//保存員工
employeeMapper.insert(employee);
//保存關係表
for (Role role : employee.getRoles()) {
employeeMapper.insertEmployeeAndRoleRel(employee.getId(), role.getRid());
}
}
- 認證時,添加密碼處理
//參數 正確的密碼,鹽,當前realm名稱
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(employee, employee.getPassword(), ByteSource.Util.bytes(employee.getUsername()), this.getName());