此文章作爲自己學習總結用。請各位看官多多指正留言或發郵件給我。
郵箱地址:[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;
}