Shiro 一款簡單易用,功能強大的安全框架,幫助我們安全高效的構(gòu)建企業(yè)級應(yīng)用。之前幾個項目都用到過 Shiro,最近抽空梳理了一下,分享一些經(jīng)驗。
本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
本文demo選型:thymeleaf, springboot 2, shiro, ehcache
PS:如果我不拖延的話,估計還是會有后續(xù)的 :)
目錄
Shiro 能做什么
Shiro 常用組件介紹
Shiro 是如何工作的
Shiro 如何集成
關(guān)于 thymeleaf-extras-shiro
Shiro 能做什么
- 認證:登錄用戶的認證
- 權(quán)限:基于角色和權(quán)限的訪問權(quán)限(url權(quán)限),以及顆?;瘷?quán)限控制(按鈕權(quán)限)
- 加密技術(shù):Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)輔助類
- session管理:可在web容器以及 EJB容器中使用 session,可擴展 (例如我們可以通過重寫 sessionDao 將 session 存儲到數(shù)據(jù)庫中)
- RememberMe:基于cookie的記住我服務(wù)
Shiro 常用組件介紹

Subject:Subject其實代表的就是當前正在執(zhí)行操作的用戶,只不過因為“User”一般指代人,但是一個“Subject”可以是人,也可以是任何的第三方系統(tǒng),服務(wù)賬號等任何其他正在和當前系統(tǒng)交互的第三方軟件系統(tǒng)。
所有的Subject實例都被綁定到一個SecurityManager,如果你和一個Subject交互,所有的交互動作都會被轉(zhuǎn)換成Subject與SecurityManager的交互SecurityManager:Shiro的核心,他主要用于協(xié)調(diào)Shiro內(nèi)部各種安全組件,不過我們一般不用太關(guān)心SecurityManager,對于應(yīng)用程序開發(fā)者來說,主要還是使用Subject的API來處理各種安全驗證邏輯
Realm:這是用于連接Shiro和客戶系統(tǒng)的用戶數(shù)據(jù)的橋梁。一旦Shiro真正需要訪問各種安全相關(guān)的數(shù)據(jù)(比如使用用戶賬戶來做用戶身份驗證以及權(quán)限驗證)時,他總是通過調(diào)用系統(tǒng)配置的各種Realm來讀取數(shù)據(jù)
關(guān)于Shiro 的其余核心組件參考 Shiro 官網(wǎng) 或者 Shiro的架構(gòu) 本文不做過多的闡述
Shiro 是如何工作的
簡單來講的話,在Spring項目中
- Shiro 會將他的所有組件注冊到 SecurityManager中
- 再通過將 SecurityManager 注冊到 ShiroFilterFactoryBean(這個類實現(xiàn)了Spring 的BeanPostProcessor會預(yù)先加載) 中,
- 最后以 filter 的形式注冊到Spring容器(實現(xiàn)了Spring的FactoryBean,構(gòu)造一個 filter 注冊到 Spring 容器中),實現(xiàn)用戶權(quán)限的管理。
Shiro 如何集成
- shiro 所需依賴,完整見demo源碼
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 基于thymeleaf的shiro擴展 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- ShiroConfig
@Configuration
public class ShiroConfig {
private static final Logger log = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, String> chainDefinition = new LinkedHashMap<>();
// 靜態(tài)資源與登錄請求不攔截
chainDefinition.put("/js/**", "anon");
chainDefinition.put("/css/**", "anon");
chainDefinition.put("/img/**", "anon");
chainDefinition.put("/layui/**", "anon");
chainDefinition.put("/login", "anon");
chainDefinition.put("/login.html", "anon");
// 用戶為授權(quán)通過認證 && 包含'admin'角色
chainDefinition.put("/admin/**", "authc, roles[super_admin]");
// 用戶為授權(quán)通過認證或者RememberMe && 包含'document:read'權(quán)限
chainDefinition.put("/docs/**", "user, perms[document:read]");
// 用戶訪問所有請求 授權(quán)通過 || RememberMe
chainDefinition.put("/**", "user");
shiroFilter.setFilterChainDefinitionMap(chainDefinition);
// 當 用戶身份失效時重定向到 loginUrl
shiroFilter.setLoginUrl("/login.html");
// 用戶登錄后默認重定向請求
shiroFilter.setSuccessUrl("/index.html");
return shiroFilter;
}
@Bean
public Realm realm() {
ShiroRealm realm = new ShiroRealm();
realm.setCredentialsMatcher(credentialsMatcher());
realm.setCacheManager(ehCacheManager());
return realm;
}
@Bean
public CacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
@Bean
public CredentialsMatcher credentialsMatcher() {
AuthCredentialsMatcher credentialsMatcher = new AuthCredentialsMatcher(ehCacheManager());
credentialsMatcher.setHashAlgorithmName(AuthCredentialsMatcher.HASH_ALGORITHM_NAME);
credentialsMatcher.setHashIterations(AuthCredentialsMatcher.HASH_ITERATIONS);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean
public DefaultWebSecurityManager securityManager() {
log.debug("--------------shiro已經(jīng)加載----------------");
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setCacheManager(ehCacheManager());
manager.setRealm(realm());
manager.setRememberMeManager(rememberMeManager());
return manager;
}
@Bean
public RememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
@Bean
public SimpleCookie rememberMeCookie(){
//這個參數(shù)是cookie的名稱,對應(yīng)前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 記住我cookie生效時間30天 ,單位秒;-->
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* Shiro生命周期處理器:
* 用于在實現(xiàn)了Initializable接口的Shiro bean初始化時調(diào)用Initializable接口回調(diào)(例如:UserRealm)
* 在實現(xiàn)了Destroyable接口的Shiro bean銷毀時調(diào)用 Destroyable接口回調(diào)(例如:DefaultSecurityManager)
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 啟用shrio授權(quán)注解攔截方式,AOP式方法級權(quán)限檢查
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* thymeleaf的shiro擴展
*
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
以上基本是Spring項目集成 Shiro 的通用配置,下面針對上述的幾個Bean 聊一聊
1. ShiroFilterFactoryBean:用于定義 請求的攔截規(guī)則, Shiro為我們默認提供了一些選項,常用如下
-
anon: 請求不攔截 -
authc: 要求用戶必須認證通過 -
user: 要求用戶為記住我狀態(tài) -
roles[xxx]: 要求用戶必須滿足 xxx 角色 -
perms[xxx]: 要求用戶必須滿足 xxx 權(quán)限
其實上述每一個都對應(yīng)了一個 Shiro 過濾器
| Filter Name | Class |
|---|---|
| anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
| authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
| authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
| logout | org.apache.shiro.web.filter.authc.LogoutFilter |
| noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter |
| 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 |
- 我們也可以自定義 過濾器來實現(xiàn)攔截
2. Realm:上面提到過Realm是用于連接Shiro和客戶系統(tǒng)的用戶數(shù)據(jù)的橋梁, 我們通過實現(xiàn)AuthorizingRealm 來提供用戶認證和授權(quán)兩個API
public class ShiroRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
@Autowired
@Lazy // 這里lazy 是有必要的, shiro組件會預(yù)先加載,導(dǎo)致依賴的bean 沒有生成代理對象(AOP失效)
private UserService userService;
/**
* 認證
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
if (log.isDebugEnabled()) {
log.debug(String.format("user:%s executing doGetAuthenticationInfo", username));
}
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
if (Constant.IS_LOCK.equals(user.getIsLock())) {
throw new LockedAccountException();
}
// ShiroUser 作為實際的 principal
ShiroUser shiroUser = new ShiroUser();
BeanUtils.copyProperties(user, shiroUser);
// SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
// principal 會被封裝到 subject 中
// shiro 默認會把我們的 credentials (也就是password) 和 token 中的作對比,所以我們可以不用做密碼校驗
ByteSource salt = ByteSource.Util.bytes(user.getUsername());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(shiroUser, user.getPassword(), salt, getName());
if (log.isDebugEnabled()) {
log.debug(String.format("user:%s executed doGetAuthenticationInfo", username));
}
return info;
}
/**
* 授權(quán)
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();
if (log.isDebugEnabled()) {
log.debug(String.format("user:%s executing doGetAuthorizationInfo", shiroUser.getUsername()));
}
AuthorizationDTO authorizationDTO = userService.getRolesAndPermissions(shiroUser.getId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(authorizationDTO.getRoleCodeSet());
info.addStringPermissions(authorizationDTO.getPermissionCodeSet());
if (log.isDebugEnabled()) {
log.debug(String.format("user:%s executed doGetAuthorizationInfo", shiroUser.getUsername()));
}
return info;
}
}
doGetAuthenticationInfo : 認證方法,在執(zhí)行 subject.login(token);后,Shiro認證器會讀取 Realm 中的該方法獲取
AuthenticationInfo對象(認證信息),包含principal(我們存儲在shiro subject中的對象),credentials (密碼)。doGetAuthorizationInfo: 授權(quán)方法,在需要校驗用戶訪問權(quán)限的時候,Shiro授權(quán)器會讀取 Realm 中的該方法獲取
AuthorizationInfo對象(授權(quán)信息)讀取DB后,可以通過addRoles(roleCollection)和addStringPermissions(permCollection)設(shè)置當前用戶的角色和權(quán)限。Shiro 在拿到這個權(quán)限信息后,會去找緩存管理器,以當前 subject 的 principal 作為key 緩存起來。
3. CredentialsMatcher: 密碼匹配器,用于匹配 doGetAuthenticationInfo 方法返回的 credentials 和 subject.login(token);時的 token 中的 password是否一致。常用的實現(xiàn)有 SimpleCredentialsMatcher(默認是該實現(xiàn))、HashedCredentialsMatcher (該實現(xiàn)可以進行加密匹配)
4. DefaultWebSecurityManager:如上述,用于協(xié)調(diào)Shiro內(nèi)部各種安全組件,我們需要將我們擴展的bean 注冊到 SecurityManager 中
5. RememberMeManager:開啟該組件后使用記住我服務(wù), token 中 rememberMe 為 true 時,登錄成功之后會創(chuàng)建RememberMe cookie。
其余參考上文代碼注釋
關(guān)于 thymeleaf-extras-shiro
Shiro 默認支持在 jsp 中使用 shiro標簽。但是想在 thymeleaf 中使用 Shiro 標簽?zāi)兀?/p>
使用 thymeleaf-extras-shiro 完美解決 thymeleaf 顆粒化權(quán)限控制
你好, <span th:text="${principal}"></span><br>
<p shiro:hasRole="super_admin">當前角色超級管理員</p>
<button shiro:hasPermission="'sys:user:add'">添加</button>
<button shiro:hasPermission="'sys:user:update'">編輯</button>
<button shiro:hasPermission="'sys:user:lock'">凍結(jié)</button>
<div shiro:hasAllPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
<span>滿足所有權(quán)限時顯示</span>
</div>
<div shiro:hasAnyPermissions="'sys:user:add, sys:user:update, sys:user:lock'">
<span>滿足一個權(quán)限即可顯示</span>
</div>
更多用法參考
Github 文檔:https://github.com/theborakompanioni/thymeleaf-extras-shiro
本文demo:https://gitee.com/yintianwen7/taven-springboot-learning/tree/master/springboot-shiro
如果你發(fā)現(xiàn)我的文章或者demo中存在問題,請聯(lián)系我