首發日期:2019-06-03
前言
在以往的權限管理中,我們的權限管理通常是有以下幾個步驟: 1.創建用戶,分配權限。 2.用戶登錄,權限攔截器攔截請求,識別當前用戶登錄信息 3.從權限表中判斷是否擁有權限
從以上步驟中可以提取到以下三個問題。 三個問題: > 1.如何讓Shiro攔截請求。 > 在web開發中,Shiro會提供一個攔截器來對請求進行攔截。 > > 2.Shiro如何判斷發起請求用戶的身份? > 在web開發中,會借助session來判斷,如果禁用了session,那么可能需要重寫一些方法。 > > 3.如何判斷權限? > Shiro使用realm來判斷權限。
下面的也將以這三個問題為中心來描述Shiro。
Shiro的介紹
- Shiro是一個開源的java安全(權限)框架,它能夠實現身份驗證、授權、加密和會話管理等功能。
- Shiro是apache旗下的產品,它的官網是: shiro官網: Apache Shiro
- Shiro不僅可以用於javaEE環境,也可以用於javaSE
Shiro功能
- Authentication:身份認證,驗證用戶是否擁有某個身份。
- Authorization: 權限校驗,驗證某個已認證的用戶是否擁有某個權限。確定“誰”可以訪問“什么”。
- Session Management:會話管理,管理用戶登錄后的會話,
- Cryptography:加密,使用密碼學加密數據,如加密密碼。
- Web Support:Web支持,能夠比較輕易地整合到Web環境中。
- Caching:緩存,對用戶的數據進行緩存,
- Concurrency:並發,Apache Shiro支持具有並發功能的多線程應用程序,也就是說支持在多線程應用中並發驗證。
- Testing:測試,提供了測試的支持。
- Run as :允許用戶以其他用戶的身份來登錄。
- Remember me :記住我
補充
- 同類的比較知名的安全框架還有spring security,Shiro的優點是比較簡潔,功能雖然比不上Spring Security多樣,但對於安全需求不多的時候可以使用Shiro。
HelloWorld
依賴包:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 這里有用到日志打印,所以引入 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency>
對於不同的場景有不同依賴包【可以參考這個http://shiro.apache.org/download.html#latestSource】
基礎包是shiro-core
,這里僅作基礎示例所以僅僅導入這個包。
示例代碼
【一些代碼可以參考https://github.com/apache/shiro/tree/master/samples/quickstart】
1.src/main/resources/shiro.ini的代碼:
# -----------------------------------------------------------------------------
# users用來定義用戶
[users]
# 用戶名 = 密碼,角色1,角色2...
admin = secret, admin
guest = guest, guest
aa = 123456, guest
# -----------------------------------------------------------------------------
# roles用來定義角色
[roles]
# 角色 = 權限 (* 代表所有權限)
admin = *
# 角色 = 權限 (* 代表所有權限)
guest = see
aa = see
2.src/main/com/demo/ShiroDemo的代碼:
package com.demo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ShiroDemo {
private static final Logger log = LoggerFactory.getLogger(ShiroDemo.class);
public static void main(String[] args) {
//1.創建SecurityManagerFactory
IniSecurityManagerFactory factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
//2.獲取SecurityManager,綁定到SecurityUtils中
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3.獲取一個用戶識別信息
Subject currentUser = SecurityUtils.getSubject();
//4.判斷是否已經身份驗證
if (!currentUser.isAuthenticated()) {
// 4.1把用戶名和密碼封裝為 UsernamePasswordToken 對象
UsernamePasswordToken token = new UsernamePasswordToken("guest", "guest");
// 4.2設置rememberme
token.setRememberMe(true);
try {
// 4.3登錄.
currentUser.login(token);
}
catch (UnknownAccountException uae) { //用戶不存在異常
log.info("****---->用戶名不存在: " + token.getPrincipal());
return;
}
catch (IncorrectCredentialsException ice) {// 密碼不匹配異常
log.info("****---->" + token.getPrincipal() + " 的密碼錯誤!");
return;
}
catch (LockedAccountException lae) {// 用戶被鎖定
log.info("****---->用戶 " + token.getPrincipal() + " 已被鎖定");
}
catch (AuthenticationException ae) { // 其他異常,認證異常的父類
log.info("****---->用戶" + token.getPrincipal() + " 驗證發生異常");
}
}
// 5.權限測試:
//5.1判斷用戶是否有某個角色
if (currentUser.hasRole("guest")) {
log.info("****---->用戶擁有角色guest!");
} else {
log.info("****---->用戶沒有擁有角色guest");
return;
}
//5.2判斷用戶是否執行某個操作的權限
if (currentUser.isPermitted("see")) {
log.info("****----> 用戶擁有執行此功能的權限");
} else {
log.info("****---->用戶沒有擁有執行此功能的權限");
}
//6.退出
System.out.println("****---->" + currentUser.isAuthenticated());
currentUser.logout();
System.out.println("****---->" + currentUser.isAuthenticated());
}
}
代碼解析
解析一下上面的代碼做了什么:
對於shiro.ini:
1.在[users]
標簽下以用戶名 = 密碼,角色1,角色2...
的格式創建了用戶
2.在[roles]
標簽下以角色 = 權限 (* 代表所有權限)
的格式為用戶分配了角色
對於ShiroDemo.java:
1.使用shiro.ini來獲取了IniSecurityManagerFactory
2.通過IniSecurityManagerFactory獲取SecurityManager,並綁定到SecurityUtils中
3.使用SecurityUtils獲取一個用戶識別信息Subject
4.對Subject對象判斷是否已經身份驗證(Authenticated)
5.將用戶名和密碼封裝成UsernamePasswordToken對象,調用Subject對象的login方法來進行登錄
6.登錄成功后,調用Subject對象的hasRole方法來判斷用戶是否擁有某個角色
7.調用Subject對象的isPermitted方法來判斷用戶是否擁有某個行為
8.調用Subject對象的logout方法來退出。
補充
上面展示了一個”登錄-權限驗證-退出“的流程。但上面的一些代碼還是硬編碼的,比如說上面的用戶名和密碼還是從ini表中獲取的,比如說上面的權限信息還是從ini中獲取的,這些問題都是需要解決的,下面會進行解決。
一些概念
先來了解一些概念。
-
Application Code:代表着應用,應用使用Subject來標識自己的身份,以及使用Subject來進行認證和授權。
-
Subject:代表着“當前用戶”。
-
Shiro SecurityManager:應用使用Subject來進行認證和授權,實際上執行認證和授權的是SecurityManager
-
Realm:SecurityManager的認證和授權需要使用Realm,Realm負責獲取用戶的權限和角色等信息,再返回給SecurityManager來進行判斷。
-
【下圖是對這些概念的補充】
-
Authenticator:認證器,對用戶身份進行認證的。
-
Authorizer:授權器,對用戶進行授權的。用來判斷用戶是否擁有某個權限。
-
Realms:補充上面Realm的內容:Realm是可以有多個的。用戶的認證和授權都需要Realm,也就是說Authenticator和Authorizer的實際操作還是交給了Realm
-
CacheManager:緩存控制器,來管理如用戶、角色、權限等的緩存的
-
Cryptography :加密模塊,提供對”密碼“的加密等功能。
Realm
- Realm是真正負責處理認證和授權的組件。
- SecurityManager要完成認證,需要Realm返回一個AuthenticationInfo,AuthenticationInfo會攜帶存儲起來的正確的用戶認證信息,用來與用戶提交的信息進行比對,如果信息不匹配,那么會認證失敗。
- SecurityManager要完成授權,需要Realm返回一個AuthorizationInfo
認證
下面的例子是以繼承了AuthenticatingRealm的自定義Realm來實現自定義認證。
認證依賴於方法doGetAuthenticationInfo,需要返回一個AuthenticationInfo,通常返回一個他的子類SimpleAuthenticationInfo,構造方法的第一個參數是用戶名,第二個是驗證密碼,第三個是當前realm的className。
package com.demo.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
public class MyRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
System.out.println("MyRealm認證中---->用戶:"+token.getPrincipal());
// 可以從token中獲取用戶名來從數據庫中查詢數據
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假設這是從數據庫中查詢到的用戶密碼
// 創建一個SimpleAuthenticationInfo,第一個參數是用戶名,第二個是驗證密碼,第三個是當前realm的className
// 驗證密碼會與用戶提交的密碼進行比對
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}
當創建了一個Realm之后,需要告訴SecurityManager,所以在shiro.ini中配置:
# -------------------------------------------------------------------
[main]
myRealm = com.demo.realms.MyRealm
# --------由於自定義認證,所以去除users,roles------------------------
這樣子就可以進行自定義認證了,在上面的用戶密碼中都設置了"123456",所以如果輸入的密碼不正確都會認證失敗。但上面沒有設置授權,所以代碼中要去掉授權的判斷。
授權
下面的例子是以繼承了AuthorizingRealm的自定義Realm來實現自定義認證和自定義授權。
授權依賴於方法doGetAuthorizationInfo,需要返回一個AuthorizationInfo,通常返回一個他的子類SimpleAuthorizationInfo。構造SimpleAuthorizationInfo可以空構造,也可以傳入一個Set<String> roles
來構造。
package com.demo.realms;
import org.apache.shiro.authc.*;
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 java.util.HashSet;
import java.util.Set;
public class RealmForDouble extends AuthorizingRealm {
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1. 獲取授權的用戶
Object principal = principals.getPrimaryPrincipal();
System.out.println("RealmForDouble授權中---->用戶:"+principal);
//2.下面使用Set<String> roles來構造SimpleAuthorizationInfo
SimpleAuthorizationInfo info = null;
// SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<>();
if ("admin".equals(principal)){
roles.add("admin"); // 假設這個角色是從數據庫中查出的
// 如果SimpleAuthorizationInfo實例化了,
// 可以這樣來加角色,行為需要這樣添加
// 角色可以傳構造函數來實例化SimpleAuthorizationInfo
// info.addRole("admin");
// info.addStringPermission("*");
}
if ("guest".equals(principal)){
roles.add("guest");
}
info = new SimpleAuthorizationInfo(roles);
return info;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble認證中---->用戶:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假設這是從數據庫中查詢到的用戶密碼
// 創建一個SimpleAuthenticationInfo,第一個參數是用戶名,第二個是驗證密碼,第三個是當前realm的className
// 驗證密碼會與用戶提交的密碼進行比對
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}
ini文件中也需要對應修改:
# -------------------------------------------------------------------
[main]
myRealm = com.demo.realms.RealmForDouble
# --------由於自定義認證,所以去除users,roles------------------------
這樣子就可以進行自定義認證和授權了,上面的認證信息和授權信息都是可以修改成從數據庫中獲取的。
幾個父類:
AuthorizingRealm:可以同時認證和授權。
AuthenticatingRealm:用於認證。
Realm:既可以用於認證也可以用於授權。
幾個方法
有好幾個父類,怎么判斷他們能否進行認證或授權呢?
- 認證依賴於方法doGetAuthenticationInfo
- 授權依賴於方法doGetAuthorizationInfo
- supports(AuthenticationToken token)方法可以處理非UsernamePasswordToken的情況。
異常
當認證錯誤時會拋出異常,下面列舉一下常見的異常。
- UnknownAccountException:用戶不存在的異常
- ExcessiveAttemptsException:登錄失敗次數過多的異常
- IncorrectCredentialsException:密碼不匹配的異常
- LockedAccountException:用戶已被鎖定的異常
- ExpiredCredentialsException:用戶登錄憑證已過期的異常。
- AuthenticationException:身份認證異常的基類。
集成於Spring MVC
依賴包
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
步驟
1.先配置Shiro攔截器:
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<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>
2.匹配SpringMVC的DispatcherServlet:
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
3.配置Spring的ContextLoaderListener:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
4.配置springmvc.xml【springmvc的配置文件】:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.progor"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler/>
</beans>
5.在applicationContext.xml中配置Shiro:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--1. 配置 SecurityManager!-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--緩存管理器-->
<property name="cacheManager" ref="cacheManager"/>
<!--realms-->
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
</bean>
<!--2. 配置 CacheManager緩存管理器.-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!--緩存配置文件(這里暫不涉及,可以隨便拷貝一個)-->
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<!--3. 配置 Realm-->
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
</bean>
<!--4. 配置 LifecycleBeanPostProcessor,用來管理shiro一些bean的生命周期-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!--5. 啟用shiro 的注解。但必須在配置了 LifecycleBeanPostProcessor 之后才可以使用-->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--6. 配置 ShiroFilter.-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/** = authc
</value>
</property>
</bean>
</beans>
6.創建控制器來接受登錄請求,執行Shiro認證:
package com.progor.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/shiro")
public class UserController {
@RequestMapping("/login")
public String login(String username,String password){
System.out.println(username+":"+password);
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
try {
currentUser.login(token);
}
catch (AuthenticationException ae) {
System.out.println("登錄失敗: " + ae.getMessage());
}
}
return "redirect:/admin.jsp";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
System.out.println("退出成功");
return "redirect:/login.jsp";
}
}
7.創建realm:
package com.progor.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.HashSet;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Object principal = principals.getPrimaryPrincipal();
System.out.println("RealmForDouble授權中---->用戶:"+principal);
SimpleAuthorizationInfo info = null;
Set<String> roles = new HashSet<>();
if ("admin".equals(principal)){
roles.add("admin");
}
if ("guest".equals(principal)){
roles.add("guest");
}
info = new SimpleAuthorizationInfo(roles);
return info;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble認證中---->用戶:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="123456";// 假設這是從數據庫中查詢到的用戶密碼
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,this.getName());
return info;
}
}
8.創建幾個jsp,用於權限測試:
一個用於登錄的login.jsp,一個用於驗證登錄成功的admin.jsp。【預期結果是如果未登錄,那么訪問admin.jsp會重定向到login.jsp】
login.jsp:
<form action="shiro/login" method="POST">
username: <input type="text" name="username"/>
<br><br>
password: <input type="password" name="password"/>
<br><br>
<input type="submit" value="Submit"/>
</form>
admin.jsp:
<body>
<p>這是admin.jsp</p>
<a href="shiro/logout">退出</a>
</body>
9.配置ehcache.xml,可以參考https://github.com/apache/shiro/blob/master/samples/spring-mvc/src/main/resources/ehcache.xml
代碼解析
1.配置ShiroFilter是為了讓ShiroFilter能夠攔截請求來進行權限判斷。
2.applicationContext中配置的Shiro請參考注釋。
3.ehcache.xml是緩存管理器的配置文件。
補充
上述的代碼簡略地演示了在Spring環境中Shiro的運行流程。下面將會對一些細節進行描述。
ShiroFilter攔截
上面的ShiroFilter中有如下圖的代碼
這主要是用來定義ShiroFilter攔截哪些請求,以及怎么攔截請求的。
攔截器鏈
在上圖中,左邊是url,右邊是攔截器。
常見的攔截器有:
- anon:任何人都可以訪問
- authc:只有認證后才可以訪問
- logout:只有登錄后才可以訪問
- roles[角色名]:只有擁有特定角色才能訪問,例如
/admin.jsp = roles[user]
- perms["行為"]:只有擁有某種行為的才能訪問,例如
/admin/deluser = prems["user:delete"]
- 想了解更多攔截器,可以參考
shiro.apache.org/web.html#default-filters
url匹配
- 在上圖中,有用到
/**
,這是代表所有請求,是為了攔截其余未定義攔截規則的請求。 - 其實這透露了url匹配是從上到下的,比如login.jsp由於前面定義了
/login.jsp = anon
,所以就不會交給/**
來攔截了。 - 另外,是可以有多個攔截器的,所以
/admin/** = authc, roles[administrator]
也是可以的。
url屬性
上面的ShiroFilter還配置了下圖的屬性,這是用來定義發生一些情況時跳轉到哪個頁面的。
- 比如配置了loginUrl,那么發起未認證的請求都會跳轉到loginUrl
- successUrl是用來定義登錄成功后調整到哪個頁面(如果controller跳轉了視圖那么這個失效)
- unauthorizedUrl是用來定義訪問不是自己權限的時跳轉到哪個頁面(普通的authc不會觸發,roles會觸發。)。
攔截器鏈的自定義
在上面都是使用硬編碼的方式來定義攔截器鏈。下面將解決這個硬編碼問題
一種方法是使用FilterChainResolver來處理,這里使用map的方式來處理。
定義一個類,核心方法是返回一個LinkedHashMap【有序是為了確保從上到下匹配】:
package com.progor.utils;
import java.util.LinkedHashMap;
public class FilterChainMap {
// 使用靜態工廠
public static LinkedHashMap<String, String> getFilterChainMap(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 下面的數據可以從數據庫中查詢出來。
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/admin.jsp", "authc");
map.put("/**", "authc");
return map;
}
}
修改applicationContext.xml:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filteChainMap"></property>
<!--去掉filterChainDefinitions-->
</bean>
<!--核心是獲取這個map,由於使用了靜態工廠,所以這樣定義這個bean-->
<bean id="filteChainMap" class="com.progor.utils.FilterChainMap" factory-method="getFilterChainMap" ></bean>
補充:
上面講述了ShiroFilter的配置,解決了請求的攔截問題。
密碼加密
在上面的密碼比對中,都是使用明文來比對。
而通常來說,被存儲起來的用戶密碼通常都是加密后的。也就是說,在使用SimpleAuthenticationInfo返回的認證信息時候,里面的密碼信息是被加密過的,如果我們直接拿用戶提交的明文密碼匹配的話就會匹配失敗,所以我們應該還需要告訴Shiro使用什么加密方式來進行密碼比較。
在Shiro中,使用credentialsMatcher來解決這個問題。
算法加密
在配置realm的時候,可以定義一個credentialsMatcher屬性,例如:
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--定義加密的算法-->
<property name="hashAlgorithmName" value="MD5"></property>
</bean>
</property>
</bean>
多重加密
密碼加密一次后可以得到一串hash值,但還可以進行多次加密來提高安全性。
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--定義加密的算法-->
<property name="hashAlgorithmName" value="MD5"></property>
<!--定義加密的次數-->
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
鹽值加密
除了多重加密,還可以加入一個”鹽值“來進行加密。一個人的名字可能是會重復的,但如果帶上他的身份證的話,那么這個人就是特定唯一的。密碼也是如此,直接將密碼進行加密可能還是比較容易分析出來的(網上有一些md5的密碼庫,常見的加密結果很容易查找出來),但如果加入一個具有比較罕見的參數來進行加密的話,那么得到的結果就會難以解析了。
鹽值由於不是每一個加密都是一樣的,所以不能在realm中設置,需要在返回AuthenticationInfo時帶上,這樣securityManager就會對提交的明文密碼依據加密算法、加密次數和鹽值來進行加密后再與AuthenticationInfo中的密碼進行比對。
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("RealmForDouble認證中---->用戶:"+token.getPrincipal());
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String password="e10adc3949ba59abbe56e057f20f883e";// md5(123456)
String salt = "lilei";//假設這個鹽值是從數據庫中查出的
ByteSource credentialsSalt = ByteSource.Util.bytes(salt);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(upToken.getUsername(),password,credentialsSalt,this.getName());
return info;
}
多realm認證
- realm是可以有多個的。
- 多個realm代表可以使用不同來源的認證信息來進行用戶認證。【適用於一些需要從多個來源查詢認證信息的情況】
多個realm的配置
上面已經講述過realm的定義方法了,所以這里主要講怎么讓Shiro知道這多個realm。
只需要把新的realm配置成bean,並告訴securityManager即可。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!--省略其他配置 -->
<bean id="jdbcRealm" class="com.progor.realms.MyRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
<bean id="secondRealm" class="com.progor.realms.SecondRealm"></bean>
對於上面的多個realm的認證,你可以嘗試兩個地方使用不同的密碼來進行測試,借助sysout的話你會發現確實經過了兩個realm.
認證策略
- 對於多個realm的默認的認證策略是只要其中一個通過了即可認證通過,也就是說一個密碼不匹配,一個密碼匹配,最終的結果將會是認證通過。
- 這個認證策略是可以修改的,需要對authenticator認證器進行配置。
- FirstSuccessfulStrategy:只要有一個 Realm 驗證成功即可,只返回第一個 Realm 身份驗證成功的認證信息,其他的忽略;
- AtLeastOneSuccessfulStrategy:只要有一個 Realm 驗證成功即可,和 FirstSuccessfulStrategy不同,返回所有 Realm 身份驗證成功的認證信息;
- AllSuccessfulStrategy:所有 Realm 驗證成功才算成功,且返回所有 Realm 身份驗證成功的認證信息,如果有一個失敗就失敗了。
可以在applicationContext.xml中配置authenticator來設置認證策略。
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
</bean>
使用上述的代碼后,需要所有的realm都驗證成功才能認證成功。
授權
上面講了權限攔截,下面講一下怎么給請求/頁面/業務來設置權限。
攔截鏈式
第一種是上面展示的使用攔截器鏈的方式,這種方式可以攔截一些請求/頁面的非法權限操作。
編程式
編程式就是在代碼中使用hasRole或isPermitted等方法來進行權限判斷。
@RequestMapping("/deluser")
public String deluser(){
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")){
//一系列操作....
System.out.println("執行了刪除用戶的操作");
return "redirect:/admin.jsp";
}else{
System.out.println("你沒有權限執行");
return "redirect:/unauthorized.jsp";
}
}
除了hashRole,常見的方法還有:
hasRoles(List<String> roleIdentifiers)
:擁有List中的所有角色才返回truehasAllRoles(Collection<String> roleIdentifiers)
:擁有集合中的所有角色才返回trueisPermitted(String... permissions)
:是否擁有某個行為(支持傳入多個參數)
注解式
注解式就是使用注解來進行權限管理。【這些注解不能用在controller中】
public class UserService {
@RequiresRoles("admin") // 需要角色admin
public void deluser(){
System.out.println("執行了刪除用戶的操作");
}
}
除了·@RequiresRoles("admin"),常見的注解還有:
- @RequiresPermissions():是否擁有某個權限
注意,注解的使用需要以下兩個bean:
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
補充
除此之外,還可以在jsp中進行授權,這將在后面再講。
shiro標簽
也可以在jsp中進行授權。
首先導入標簽庫:
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:guest></shiro:guest>
:當用戶沒進行認證時,顯示標簽中的內容。<shiro:user></shiro:user>
:當用戶進行認證了,顯示標簽中的內容。<shiro:authenticated></shiro:authenticated>
:當用戶已經認證時,顯示標簽中的內容。<shiro:notAuthenticated></shiro:notAuthenticated>
:當用戶未認證的時候顯示標簽中的內容(包括“remember me”的時候)<shiro:principal />
:用來獲取用戶憑證(用戶名等)(從AuthenticationInfo中獲取),標簽所在的位置將被替換成憑證信息<shiro:principal property="username" />
:如果存入的用戶憑證是一個對象,那么可以使用property指定獲取這個對象中的屬性。<shiro:hasRole name="角色"></shiro:hasRole>
:擁有指定角色,將顯示標簽中的內容。<shiro:hasAnyRoles name="角色1,角色2..."></shiro:hasAnyRoles>
:只要擁有多個角色中的一個就顯示標簽中的內容。<shiro:lacksRole name="角色"></shiro:lacksRole>
:沒有某個角色將不顯示標簽中的內容<shiro:hasPermission name="行為"></shiro:hasPermission>
:如果擁有某個行為的權限,那么顯示標簽中的內容<shiro:lacksPermission name="行為"></shiro:lacksPermission>
:如果沒有擁有某個行為,那么顯示標簽中內容
示例
<!-- 一個未登錄的場景 -->
<shiro:guest>
Hi there! Please <a href="login.jsp">Login</a> or <a href="signup.jsp">Signup</a> today!
</shiro:guest>
<!-- 已登錄過,准備切換其他用戶的場景 -->
<shiro:user>
Welcome back John! Not John? Click <a href="login.jsp">here<a> to login.
</shiro:user>
<!-- 顯示登錄用戶的用戶名的場景 -->
Hello, <shiro:principal/>, how are you today?
<!-- 用戶已經認證通過的場景 -->
<shiro:authenticated>
<a href="/logout">退出</a>.
</shiro:authenticated>
<!-- 擁有某個角色的場景 -->
<shiro:hasRole name="administrator">
<a href="createUser.jsp">創建用戶</a>
</shiro:hasRole>
<!-- 擁有某個行為的場景 -->
<shiro:hasPermission name="user:create">
<a href="createUser.jsp">創建用戶</a>
</shiro:hasPermission>
remember me
remember me 主要用於再次訪問時仍然保留認證狀態的場景。例如,離開某個網站后,兩三天再打開仍然保留你的登錄信息。
setRememberMe
remember me的功能的一個前提是在認證時使用了setRememberMe :
為true才會“記住我”。
rememberme權限級別
記住我的權限並不是authc
,而是user【用戶已經身份驗證/記住我】
所以做實驗的要記得修改攔截器鏈。
參數設置
maxAge:過期時間
httpOnly:禁止使用js腳本讀取到cookie信息
【其他的不太常用,有興趣的自查。還有domain之類的】
一種配置方法:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/><!-- cookie的名稱 -->
<property name="httpOnly" value="true"/>
<property name="maxAge" value="60"/><!-- 過期時間:60s -->
</bean>
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie"/>
</bean>
第二種配置方法:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
</list>
</property>
<property name="rememberMeManager.cookie.maxAge" value="15"/>
</bean>
補充
- remember借助了cookie來實現記住登錄狀態,但這是不太安全的,因為(黑客)把cookie竊取了也能進行登錄。
結語
這里僅僅只是“開了個門”,Shiro的世界還有很多廣闊的地方。比如會話管理、單點登錄【這些什么時候有空再寫吧】
如果你想了解更多,可以參考Shiro官方參考手冊http://shiro.apache.org/reference.html
;
除此之外,張開濤的Shiro的PDF也是可以值得一看的。