Shiro簡介及SpringBoot整合Shiro

此文章作爲自己學習總結用。請各位看官多多指正留言或發郵件給我。

郵箱地址:[email protected]

Shiro簡介

(僅僅是簡介,只實現了用戶登錄認證、授權認證和用戶權限緩存功能,可以滿足小型項目的登錄功能。如果想深入瞭解shiro,可以搜索《跟我學shiro》。)

Shiro架構:

1、Subject(org.apache.shiro.subject.Subject): 簡稱用戶,但這個用戶不一定是一個具體的人,和當前應用交互的任何東西都可以稱爲Subject;與Subject的所有交互都會委託給SecurityManager,SecurityManager是實際的執行者。

2、SecurityManager(org.apache.shiro.mgt.SecurityManager): SecurityManager是shiro的核心,協調shiro各個組件。

3、Authenticator(org.apache.shiro.authc.Authenticator): 認證器,登錄控制。

4、Authorizer(org.apahce.shiro.authz.Authorizer): 授權器,決定subject能擁有什麼樣的角色或者權限。

5、SessionManager(org.apache.shiro.session.SessionManager): 創建和管理用戶Session。

6、CacheManager(org.apache.shiro.cache.CacheManager): 緩存管理器,主要存儲Session和權限數據。

7、Cryptography(org.apache.shiro.crypto): 安全加密工具,Shiro的api大幅度簡化java api中繁瑣的密碼加密。

8、Realm(org.apache.shiro.realm.Realm): 相當於數據源,程序與安全數據的橋樑;負責用戶認證和用戶授權(可以配置多個realm)。


權限管理原理:

用戶、角色、權限和資源的關係模型:

目的:基於資源的訪問控制RBAC(ResourceBased Access Control)

通常企業開發中將資源和權限合併爲一張權限表。

示例:


Type字段用於區分menu(菜單)和permission(權限)。

Percode權限代碼。字符串通配符權限規則:“資源標識符:操作:對象實例ID”。

Parent_id_tree菜單層級關係。


簡述Shiro身份驗證、授權和攔截機制

身份驗證流程:

1、首先會調用Subject.login(token)進行登錄,會自動委託給SecurityManager(登錄前必須通過SecurityUtils.setSecurityManager(SecurityManager)將SecurityManager設置到當前環境當中)。

2、SecurityManager負責驗證邏輯,真正的驗證工作會委託給Authenticator

3、Authenticator會把傳入的token傳入Realm,從Realm獲取身份驗證信息,驗證失敗返回AuthenticationException異常。若配置了多個Realm,將按照相應的順序及策略進行驗證。


授權流程:

Shiro的支持三種授權方式:

1、編程式,Subject.hasRole(roleCode),Subject.hasPermission(perCode)等。

2、註解式,@RequiresRoles(roleCode),@RequiresPermissions(perCode)(需要開啓SpringMVC的AOP代理)。

3、JSP標籤,

<shiro:hasRolename="roleCode">

<!— 有權限顯示的標籤 —>

</shiro:hasRole>

 

授權流程:

1、SecurityManager把真正的授權工作委託給Authorizer

2、接着Authorizer會通過PermissionResolver將權限字符串轉換成相應的Permission實例。

3、授權之前會調用realm的doGetAuthorizationInfo方法獲取當前Subject相應的角色/權限。

4、Authorizer會判斷Realm的角色/權限是否和傳入的匹配;如果有多個Realm,會委託給ModularRealmAuthorizer進行循環判斷。


攔截機制:

Shiro攔截器類關係圖:


1、org.apache.shiro.web.servlet.NameableFilter:給Filter起名字。

2、org.apache.shiro.web.servlet.OncePerRequestFilter:用於防止多次執行Filter;doFilter方法具體實現一次請求只會走一次攔截器鏈;另外會提供enable屬性,表示否是開啓攔截器實例,默認true。

3、org.apache.shiro.web.servlet.ShiroFilter:整個Shiro的入口,用於攔截需要驗證的請求,並進行處理。

4、org.apache.shiro.web.servlet.AdviceFilter:提供了AOP風格的支持,通過preHandle和postHandle方法分別在執行攔截器鏈執行前後進行處理;afterCompletion屬於postHandle的增強方法,無論是否有異常都會執行,一般進行資源清理的工作。

5、org.apache.shiro.web.filter.PathMatchingFilter:提供請求路徑的匹配功能(pathsMatch方法),以及攔截器參數解析的功能(onPreHandle方法)。

6、org.apache.shiro.web.filter.AccessControlFilter:提供了訪問控制的基礎功能;比如是否允許訪問(isAccessAllowed方法),訪問拒絕時如何處理(onAccessDenied方法);AccessControlFilter還提供了基於表單的身份驗證功能(isLoginRequest、saveRequestAndRedirectToLogin、saveRequest和redirectToLogin方法)。


內置攔截器:

authc:org.apache.shiro.web.filter.authc.FormAuthenticationFilter。基於表單的攔截器;需要登錄才能訪問。主要屬性:用戶名usernameParam,密碼passwordParam,記住登錄remremberMeParam,登錄失敗後錯誤信息failureKeyAttribute。

authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter。Basic HTTP身份驗證攔截器。

logout:org.apache.shiro.web.filter.authc.LogoutFilter。退出攔截器。主要屬性:redirectUrl:退出成功後重定向的地址。

user:org.apache.shiro.web.filter.authc.UserFilter。用戶攔截器,記住我登錄後可以訪問的地址。

anon:org.apache.shiro.web.filter.authc.AnonymousFilter。匿名攔截器,即不需要登錄即可訪問的資源。一般用於過濾靜態資源。

roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter。角色授權攔截器,驗證用戶是或否擁有角色。

perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter。權限授權攔截器,驗證用戶是否擁有權限。

port:org.apache.shiro.web.filter.authz.PortFilter。端口攔截器。主要屬性:port:可通過的端口。

rest:org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter。rest風格攔截器,自動根據請求方法構建權限字符串(例:“/users=rest[user]”,會自動拼出“user:read,user:create,user:update,user:delete”權限字符串進行權限匹配,調用isPermittedAll)。

ssl:org.apache.shiro.web.filter.authz.SslFilter。ssl攔截器,只有請求協議是https才能通過。

onSessionCreation:org.apache.shiro.web.filter.session.NoSessionCreationFilter。不創建會話攔截器。


權限註解:

@RequiredAuthentication:表示當前Subject已經通過了login進行了身份驗證;即Subject.isAuthenticated()返回true。

@RequiresUser:表示當前Subject已經身份驗證或者通過記住我登錄的。

@RequiresGuest:表示當前Subject沒有身份驗證或通過記住我登陸過,可視爲遊客身份。

@RequiresRoles(value={“roleCode1”,“roleCode2”} , logical= Logical.AND):表示當前Subject需要角色roleCode1和roleCode2。

@RequiresPermissions(value={“user:perCode1”,“user:perCode2”} , logical= Logical.OR):表示當前Subject需要權限user:perCode1或user:perCode2。


SpringBoot整合Shiro Demo

依賴:

<dependency>  
    <groupId>org.apache.shiro</groupId>  
    <artifactId>shiro-core</artifactId>  
    <version>1.2.2</version>  
</dependency> 
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.2.2</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-ehcache</artifactId>
   <version>1.2.2</version>
</dependency>

建立相關表:

CREATE TABLE `sys_user` (
  `user_id` varchar(32) NOT NULL,
  `user_name` varchar(255) NOT NULL,
  `user_code` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `salt` varchar(255) NOT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';

CREATE TABLE `sys_role` (
  `role_id` varchar(32) NOT NULL,
  `role_name` varchar(255) NOT NULL,
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

CREATE TABLE `sys_user_role` (
  `user_role_id` varchar(32) NOT NULL,
  `user_id` varchar(32) NOT NULL,
  `role_id` varchar(32) NOT NULL,
  PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶角色關係表';

CREATE TABLE `sys_permission` (
  `permission_id` varchar(32) NOT NULL,
  `permission_name` varchar(255) NOT NULL,
  `type` varchar(255) NOT NULL COMMENT '授權類型:menu菜單;permission權限',
  `url` varchar(255) NOT NULL,
  `percode` varchar(255) DEFAULT NULL COMMENT '權限代碼(格式:“對象:操作”)',
  `parent_id` varchar(32) NOT NULL,
  `parent_id_tree` varchar(255) NOT NULL COMMENT '菜單結構樹',
  `sort` varchar(255) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='權限表';

CREATE TABLE `sys_role_permission` (
  `role_permission_id` varchar(32) NOT NULL,
  `role_id` varchar(32) DEFAULT NULL,
  `permission_id` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`role_permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色權限表';

整合流程概述:

1、 配置、註冊安全管理器SecurityManager:

    ①   自定義realm,注入到SecurityManager。

    ②   配置憑證匹配器CredentialsMatcher,注入到SecurityManager。

    ③   自定義緩存管理器CacheManager,注入到SecurityManager。

2、 配置、註冊表單過濾器FormAuthenticationFilter。

3、 配置、註冊ShiroFilter。

    ①   注入安全管理器SecurityManager。

    ②   注入表單過濾器FormAuthenticationFilter。

    ③   配置、注入攔截器過濾鏈。

4、開啓shiro註解支持。

5、開啓SpringMVC AOP代理。


自定義Realm

用於登錄驗證、用戶授權和清空緩存。

AuthenticationInfodoGetAuthenticationInfo(AuthenticationToken token):Shiro攔截到url時會進行過濾,PathMatchingFilter攔截器會判斷是否已經登錄(檢查是否有session或token),沒有登錄時會調用Subject.login(new UsernamePasswordToken(userCode,password))方法,轉交給realm的doGetAuthenticationInfo方法匹配。

AuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principals):需要驗證權限時(Subject.isPermitted(percode)、Subject. isPermittedAl(percode1,percode2…)、Subject.checkPermission(percode)或進入帶有@RequiresPermissions(percode)註解的方法),會調用realm的doGetAuthorizationInfo方法進行授權。

void clearCache():用於清空權限緩存。

public class MyRealm extends AuthorizingRealm {

	@Autowired
	private SysUserService sysUserService;
	
	/**
	 * 用戶驗證
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 獲得用戶賬號
		String userCode = (String)token.getPrincipal();
		SysUser sysUser = sysUserService.findByUserCode(userCode);
		// 校驗用戶
		String password = sysUser.getPassword();
		// 鹽
		String salt = sysUser.getSalt();
		// 此處會根據shiroFilter注入的憑證匹配器的配置,對錶單提交的密碼加密後進行比對
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser, password, ByteSource.Util.bytes(salt), this.getName());
		return simpleAuthenticationInfo;
	}
	
	/**
	 * 用戶授權
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		SysUser sysUser = (SysUser)principals.getPrimaryPrincipal();
		List<SysPermission> permissionList = null;
		try {
			permissionList = sysUserService.selectPermissionByUserId(sysUser.getUserId());
		} catch (Exception e) {
		}
		List<String> permissions = new ArrayList<String>();
		if (permissionList != null) {
			for (SysPermission sysPermission : permissionList) {
				permissions.add(sysPermission.getPercode());
			}
		}
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		simpleAuthorizationInfo.addStringPermissions(permissions);
		return simpleAuthorizationInfo;
	}
	
	public void clearCache() {
		PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
		super.clearCache(principals);
	}
}

註冊MyRealm:

/**
 * 自定義realm 認證授權管理器
 * @return
 */
@Bean(name = "myRealm")
public MyRealm myRealm() {
	return new MyRealm();
}


設置憑證匹配器HashedCredentialsMatcher

hashAlgorithmName:使用的加密方式(md5或Base64等)。

hashIterations:加密次數。

/**
 * HashedCredentialsMatcher 憑證匹配器
 * @return
 */
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
	HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
	// 設置加密方式及加密次數
	hashedCredentialsMatcher.setHashAlgorithmName(hashAlgorithmName);
	hashedCredentialsMatcher.setHashIterations(hashIterations);
	return hashedCredentialsMatcher;
}


設置緩存管理器CacheManager

此處以redis作爲容器。

概述:

1、注入redis數據源,使用Spirng的RedisTemplate或注入JedisPool。

2、實現org.apache.shiro.cache.Cache接口。

3、實現org.apache.shiro.cache.CacheManager接口。

4、將自定義CacheManager的子類注入到Spring。

5、將自定義CacheManager注入到SecurityManager。


注入redis數據源:

redis依賴:
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>
註冊JedisPool:
@Configuration
@PropertySource(value = "classpath:redis.properties")
public class RedisConfig {
	@Value("${reids.url}")
	private String redisUrl;
	
	@Value("${redis.password}")
	private String redisPassword;

	@Value("${redis.port}")
	private Integer redisPort;
	
	@Value("${redis.timeout}")
	private Integer redisTimeout;
	
	@Value("${redis.pool.maxTotal}")
	private Integer maxTotal;
	
	@Value("${redis.pool.minIdle}")
	private Integer minIdle;
	
	@Value("${redis.pool.maxIdle}")
	private Integer maxIdle;
	
	@Value("${redis.pool.maxWaitMillis}")
	private Integer maxWaitMillis;

	@Bean(name = "jedisPoolConfig")
	public JedisPoolConfig jedisPoolConfig() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		// 最大連接數
		jedisPoolConfig.setMaxTotal(maxTotal);
		// 最大空閒連接數
		jedisPoolConfig.setMaxIdle(maxIdle);
		// 最小空閒連接數, (不設置爲0)
		jedisPoolConfig.setMinIdle(minIdle);
		// 獲取連接時的最大等待毫秒數
		jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
		// 在獲取連接的時候檢查有效性
		jedisPoolConfig.setTestOnBorrow(true);
		// 在獲取連接的時候檢查有效性
		jedisPoolConfig.setTestOnReturn(true);
		return jedisPoolConfig;
	}
	
	@Bean(name = "jedisPool")
	public JedisPool jedisPool() {
		JedisPool jedisPool = new JedisPool(jedisPoolConfig(), redisUrl, redisPort, redisTimeout, redisPassword);
		return jedisPool;
	}
}


實現org.apache.shiro.cache.Cache接口:

回調接口:
public interface RedisCallback {
	Object doWithRedis(Jedis jedis);
}
序列化工具類:
public enum JDKSerializer implements Serializer {
	INSTANCE;
	private static Logger log = Logger.getLogger(JDKSerializer.class);

	private JDKSerializer() {}
	
	public byte[] serialize(Object object) {
		ObjectOutputStream oos = null;
		ByteArrayOutputStream baos = null;
		try {
			// Object序列化
			baos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(baos);
			oos.writeObject(object);
			return baos.toByteArray();
		} catch (Exception e) {
			log.error(new String("JDKSerializer : serialize "), e);
			throw new CacheException(e);
		} finally {
			JDKSerializer.close(oos);
			JDKSerializer.close(baos);
		}
	}
	
	/**
	 * Object反序列化
	 * @param bytes
	 * @return
	 */
	public Object unserialize(byte[] bytes) {
		if (bytes == null) {
			return null;
		}
		ByteArrayInputStream bais = null;
		ObjectInputStream ois = null;
		try {
			bais = new ByteArrayInputStream(bytes);
			ois = new ObjectInputStream(bais);
			return ois.readObject();
		} catch (Exception e) {
			log.error(new String("JDKSerializer : unserialize "), e);
			throw new CacheException(e);
		} finally {
			JDKSerializer.close(bais);
			JDKSerializer.close(ois);
		}
	}

	/**
	 * 關閉IO流對象
	 * @param closeable
	 */
	public static void close(Closeable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (Exception e) {
				log.error("JDKSerializer : close ",e);
				throw new CacheException(e);
			}
		}
	}
}
實現Cache接口:
public final class ShiroRedisCache<K, V> implements Cache<K, V> {

	private JedisPool jedisPool;
	
	private static final Integer DATABASE = 3;
	
	private Serializer serializer = JDKSerializer.INSTANCE;
	
	private static final Integer TIMEOUT = 1800;
	
	public Object execute(RedisCallback callback) {
		if (jedisPool == null) {
			synchronized (ShiroRedisCache.class) {
				if (jedisPool == null) {
					this.setJedisPool(SpringUtil.getBean("jedisPool", JedisPool.class));
				}
			}
		}
		Jedis jedis = this.getJedisPool().getResource();
		jedis.select(DATABASE);
		try {
			return callback.doWithRedis(jedis);
		} finally {
			jedis.close();
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Object get(final Object key) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Object value = serializer.unserialize(jedis.get(key.toString().getBytes()));
				return value;
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object put(final Object key, final Object value) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				jedis.set(key.toString().getBytes(), serializer.serialize(value));
				if (TIMEOUT != null && jedis.ttl(key.toString().getBytes()) == -1) {
					jedis.expire(key.toString().getBytes(), TIMEOUT);
				}
				return serializer.unserialize(jedis.get(key.toString().getBytes()));
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object remove(final Object key) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Object value = serializer.unserialize(jedis.get(key.toString().getBytes()));
				jedis.del(key.toString().getBytes());
				return value;
			}
		});
	}

	@Override
	public void clear() throws CacheException {
		this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				jedis.flushDB();
				return null;
			}
		});
	}

	@Override
	public int size() {
		return (Integer)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Long size = jedis.dbSize();
				return size.intValue();
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Set keys() {
		return (Set<Object>)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Set<byte[]> keys = jedis.keys("*".getBytes());
				Set<Object> set = new HashSet<Object>();
				for (byte[] bs : keys) {
					set.add(serializer.unserialize(bs));
				}
				return set;
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Collection values() {
		final Set<Object> keys = this.keys();
		return (List<Object>)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				List<Object> values = new ArrayList<Object>();
				for (Object key : keys) {
					values.add(serializer.unserialize(jedis.get(key.toString().getBytes())));
				}
				return values;
			}
		});
	}
}


實現org.apache.shiro.cache.CacheManager接口

public class RedisCacheManager implements CacheManager {
	@Override
	public <K, V> Cache<K, V> getCache(String name) throws CacheException {
		return new ShiroRedisCache<K, V>();
	}
}

將自定義CacheManager的子類注入到Spring

/**
 * cacheManager 緩存管理器
 * @return
 */
@Bean(name = "cacheManager")
public CacheManager cacheManager() {
	return new RedisCacheManager();
}

配置、註冊安全管理器SecurityManager

/**
 * scurityManager 安全管理器
 * @param realm
 * @param credentialsMatcher
 * @param cacheManager
 * @return
 */
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("myRealm")AuthorizingRealm realm, @Qualifier("credentialsMatcher")HashedCredentialsMatcher credentialsMatcher,@Qualifier("cacheManager")CacheManager cacheManager) {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	// 注入憑證匹配器
	realm.setCredentialsMatcher(credentialsMatcher);
	// 注入realm
	securityManager.setRealm(realm);
	// 注入cache管理器
	securityManager.setCacheManager(cacheManager);
	return securityManager;
}


配置、註冊ShiroFilter

/**
 * ShiroFilterFactoryBean shiro配置工廠
 * @param securityManager
 * @param formAuthenticationFilter
 * @return
 */
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager, 
		@Qualifier("formAuthenticationFilter")FormAuthenticationFilter formAuthenticationFilter) {
	ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
	// 注入securityManager
	shiroFilterFactory.setSecurityManager(securityManager);
	
	// 認證提交地址
	shiroFilterFactory.setLoginUrl(loginUrl);
	// 無權操作跳轉頁面
	shiroFilterFactory.setUnauthorizedUrl(unauthorizedUrl);
	// 驗證成功後跳轉頁面
	shiroFilterFactory.setSuccessUrl(successUrl);
	
	// 設置filter
	Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>();
	filterMap.put("authc", formAuthenticationFilter);
	shiroFilterFactory.setFilters(filterMap);
	
	// 設置過濾鏈,根據取出順序執行
	Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
	// 靜態資源可以匿名訪問
	filterChainDefinitionMap.put("/static/**", "anon");
	filterChainDefinitionMap.put("/images/**", "anon");
	filterChainDefinitionMap.put("/js/**", "anon");
	filterChainDefinitionMap.put("/styles/**", "anon");
	// 退出
	filterChainDefinitionMap.put("/logout", "logout");
	// 所有url必須認證通過才能訪問
	filterChainDefinitionMap.put("/**", "authc");
	shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinitionMap);
	
	return shiroFilterFactory;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章