今天研究一下 常用的安全框架shiro
先好好研究一下shiro 的實現原理,方便後面的學習
shiro (java安全框架)
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。使用Shiro的易於理解的API,您可以
快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。
軟體名稱 Apache Shiro 開發商 Apache 性質 Java安全框架
複製程式碼
主要功能 三個核心元件:Subject, SecurityManager 和 Realms.
Subject:即“當前操作使用者”。但是,在Shiro中,Subject這一概念並不僅僅指人,也可以是第三方程式、後臺帳戶
(Daemon Account)或其他類似事物。它僅僅意味著“當前跟軟體互動的東西”。但考慮到大多數目的和用途,你可以把它認為
是Shiro的“使用者”概念。Subject代表了當前使用者的安全操作,SecurityManager則管理所有使用者的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來
提供安全管理的各種服務。
Realm: Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控
制)驗證時,Shiro會從應用配置的Realm中查詢使用者及其許可權資訊。從這個意義上講,Realm實質上是一個安全相關的DAO:它
封裝了資料來源的連線細節,並在需要時將相關資料提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(
或)授權。配置多個Realm是可以的,但是至少需要一個。 Shiro內建了可以連線大量安全資料來源(又名目錄)的Realm,
如LDAP、關聯式資料庫(JDBC)、類似INI的文字配置資源以及屬性檔案等。如果預設的Realm不能滿足需求,你還可以插入代表
自定義資料來源的自己的Realm實現。
複製程式碼
shiro 原理剖析: shiro的核心是java servlet規範中的filter,通過配置攔截器,使用攔截器鏈來攔截請求,如果允許訪問,則通過。通常情況下,系統的登入、退出會配置攔截器。登入的時候,呼叫subject.login(token),token是使用者驗證資訊,這個時候會在Realm中doGetAuthenticationInfo方法中進行認證。這個時候會把使用者提交的驗證資訊與資料庫中儲存的認證資訊進行比較,一致則允許訪問,並在瀏覽器種下此次回話的cookie,在伺服器端儲存session資訊。退出的時候,呼叫subject.logout(),會清除回話資訊。
shiro中核心概念介紹: Filter:
1.AnonymousFilter:通過此filter修飾的url,任何人都可以進行訪問,即使沒有進行許可權認證
2.FormAuthenticationFilter:通過此filter修飾的url,會對請求的url進行驗證,如果沒有通過,則會重定向返回到loginurl
3.BasicHttpAuthenticationFilter:通過此filter修飾的url,要求使用者已經通過認證,如果沒有通過,則會要求通過Authorization 資訊進行認證
4.LogoutFilter:通過此filter修飾的url,一旦收到url請求,則會立即呼叫subject進行退出,並重定向到redirectUrl
5.NoSessionCreationFilter:通過此filter修飾的url,不會建立任何會話
6.PermissionAuthorizationFilter:許可權攔截器,驗證使用者是否具有相關許可權
7.PortFilter:埠攔截器,不是通過制定埠訪問url,將自動將埠重定向到指定埠
8.HttpMethodPermissionFilter:rest風格攔截器,配置rest的訪問方式
9.RolesAuthorizationFilter:角色攔截器,未登陸,將跳轉到loginurl,未授權,將跳轉到unauthorizedUrl
10.SslFilter:HTTPS攔截器,需要以HTTPS的方式進行訪問
11.UserFilter:使用者攔截器,需要使用者已經認證,或已經remember me
攔截配置說明:
anon:例子/admins/**=anon 沒有引數,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要認證(登入)才能使用,沒有引數
roles:例子/admins/user/**=roles[admin],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,當有多個引數時,例如admins/user/**=roles["admin,guest"],每個引數通過才算通過,相當於hasAllRoles()方法。
perms:例子/admins/user/**=perms[user:add:*],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個引數時必須每個引數都通過才通過,想當於isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根據請求的方法,相當於/admins/user/**=perms[user:method] ,其中method為post,get,delete等。
port:例子/admins/user/**=port[8081],當請求的url的埠不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裡port的埠,queryString是你訪問的url裡的?後面的引數。
authcBasic:例如/admins/user/**=authcBasic沒有參數列示httpBasic認證
ssl:例子/admins/user/**=ssl沒有引數,表示安全的url請求,協議為https
user:例如/admins/user/**=user沒有參數列示必須存在使用者,當登入操作時不做檢查
注:anon,authcBasic,auchc,user是認證過濾器,
perms,roles,ssl,rest,port是授權過濾器
複製程式碼
shiro 的許可權控制只是做到資源的許可權控制,要想實現業務資料的許可權控制,肯定是需要耦合到我們具體的業務程式碼裡面的,後面有時間在分享一下現在自己公司的一種解決思路。
上面簡單介紹了一下shiro,接下來就進入正題。
加入需要的依賴
<!-- spring-data-jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--模板引擎,訪問靜態資源-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 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>
<!-- shiro+redis快取外掛 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<!--shiro與thymelef的整合外掛-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
複製程式碼
yml檔案在加入一點配置,這裡要注意的是jpa 和之前的datasource 處於同一級
#通過jpa 生成資料庫表
spring:
jpa:
hibernate:
ddl-auto: update
show-sql: true
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
content-type: text/html
mode: HTML5
複製程式碼
資料庫設計 一般的許可權管理都會設計到這五張表(使用者表、角色表、使用者角色中間表、許可權表、角色許可權中間表)
1、使用者表:
@Entity //實體類的註解
@Table(name="sys_user")
public class SysUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String userName;
private String passWord;
private int userEnable;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public int getUserEnable() {
return userEnable;
}
public void setUserEnable(int userEnable) {
this.userEnable = userEnable;
}
}
複製程式碼
2、角色表
@Entity //實體類的註解
@Table(name="sys_role")
public class SysRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String roleName;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
複製程式碼
3、使用者角色中間表
@Entity //實體類的註解
@Table(name="sys_user_role")
public class SysUserRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int userId;
private int roleId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public int getRoleId() {
return roleId;
}
public void setRoleId(int roleId) {
this.roleId = roleId;
}
}
複製程式碼
4、許可權表
@Entity //實體類的註解
@Table(name="sys_permission")
public class SysPermission implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String userName;
private String resUrl;
private String userType;
private String parentId;
private String userSort;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getResUrl() {
return resUrl;
}
public void setResUrl(String resUrl) {
this.resUrl = resUrl;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public String getUserSort() {
return userSort;
}
public void setUserSort(String userSort) {
this.userSort = userSort;
}
}
複製程式碼
5、角色許可權中間表
@Entity //實體類的註解
@Table(name="sys_role_permission")
public class SysRolePermission implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private int roleId;
private int permissionId;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getRoleId() {
return roleId;
}
public void setRoleId(int roleId) {
this.roleId = roleId;
}
public int getPermissionId() {
return permissionId;
}
public void setPermissionId(int permissionId) {
this.permissionId = permissionId;
}
}
複製程式碼
基本上五個表的結構就是這樣,有問題到時候再改,實體類建好了,先建資料庫也行都一樣,現在先建了實體類就通過spring-data-jpa 生成一下資料庫的表結構 這時候在重啟專案就可以幫我們在資料庫建好表了。
shiro 配置類
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 沒有登陸的使用者只能訪問登陸頁面
shiroFilterFactoryBean.setLoginUrl("/auth/login");
// 登入成功後要跳轉的連結
shiroFilterFactoryBean.setSuccessUrl("/auth/index");
// 未授權介面; ----這個配置了沒鳥用,具體原因想深入瞭解的可以自行百度
shiroFilterFactoryBean.setUnauthorizedUrl("/auth/err");
//自定義攔截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
shiroFilterFactoryBean.setFilters(filtersMap);
// 許可權控制map.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/auth/login", "anon");
filterChainDefinitionMap.put("/auth/logout", "logout");
filterChainDefinitionMap.put("/auth/kickout", "anon");
//filterChainDefinitionMap.put("/book/**", "authc,perms[book:list],roles[admin]");
//filterChainDefinitionMap.put("/**", "authc,kickout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設定realm.
securityManager.setRealm(myShiroRealm());
// 自定義快取實現 使用redis
securityManager.setCacheManager(cacheManager());
// 自定義session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
*
* @return
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/**
* cacheManager 快取 redis實現
* 使用的是shiro-redis開源外掛
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis開源外掛
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPort(6379);
redisManager.setExpire(1800);// 配置快取過期時間
redisManager.setTimeout(0);
// redisManager.setPassword(password);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis開源外掛
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao層的實現 通過redis
* 使用的是shiro-redis開源外掛
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/***
* 授權所用配置
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/***
* 使授權註解起作用不如不想配置可以在pom檔案中加入
* <dependency>
*<groupId>org.springframework.boot</groupId>
*<artifactId>spring-boot-starter-aop</artifactId>
*</dependency>
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro生命週期處理器
*
*/
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
複製程式碼
自定義Realm:
public class MyShiroRealm extends AuthorizingRealm {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
//如果專案中用到了事物,@Autowired註解會使事物失效,可以自己用get方法獲取值
@Autowired
private SysRoleService roleService;
@Autowired
private UserService userService;
/**
* 認證資訊.(身份驗證) : Authentication 是用來驗證使用者身份
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
logger.info("---------------- 執行 Shiro 憑證認證 ----------------------");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String name = token.getUsername();
String password = String.valueOf(token.getPassword());
SysUser user = new SysUser();
user.setUserName(name);
user.setPassWord(password);
// 從資料庫獲取對應使用者名稱密碼的使用者
SysUser userList = userService.getUser(user);
if (userList != null) {
// 使用者為禁用狀態
if (userList.getUserEnable() != 1) {
throw new DisabledAccountException();
}
logger.info("---------------- Shiro 憑證認證成功 ----------------------");
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userList, //使用者
userList.getPassWord(), //密碼
getName() //realm name
);
return authenticationInfo;
}
throw new UnknownAccountException();
}
/**
* 授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("---------------- 執行 Shiro 許可權獲取 ---------------------");
Object principal = principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
if (principal instanceof SysUser) {
SysUser userLogin = (SysUser) principal;
Set<String> roles = roleService.findRoleNameByUserId(userLogin.getId());
authorizationInfo.addRoles(roles);
Set<String> permissions = userService.findPermissionsByUserId(userLogin.getId());
authorizationInfo.addStringPermissions(permissions);
}
logger.info("---- 獲取到以下許可權 ----");
logger.info(authorizationInfo.getStringPermissions().toString());
logger.info("---------------- Shiro 許可權獲取成功 ----------------------");
return authorizationInfo;
}
/**
* 清除所有使用者授權資訊快取.
*/
public void clearCachedAuthorizationInfo(String principal) {
SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
clearCachedAuthorizationInfo(principals);
}
/**
* 清除所有使用者授權資訊快取.
*/
public void clearAllCachedAuthorizationInfo() {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if (cache != null) {
for (Object key : cache.keys()) {
cache.remove(key);
}
}
}
/**
* @Description: TODO 清楚快取的授權資訊
* @return void 返回型別
*/
public void clearAuthz(){
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
複製程式碼
在資料庫裡面插入幾條資料開始測試。
就拿之前的程式碼做測試了,先在配置裡配置好需要做許可權過濾的路徑,和許可權規則
這樣設定應該是必須登入才能訪問,瀏覽器直接訪問一下
結果發現跳轉到之前配置的登入頁了,就是說這個許可權起作用了,把它改成anon,重啟試一下 ,其實在啟動的控制檯我們能看到shiroFilter這個過濾器的資訊,過濾的是 /*在訪問一下之前的連結,發現可以正常訪問到,查到了之前的測試資料
改個角色的許可權試試在,之前我們已經給使用者設定了 ‘admin’ ‘test’兩個角色 , 沒有設定‘demo’這個角色,請求應該也會被攔截
果然又跳到了登入頁面,把‘demo’ 去掉,發現可以正常請求查到了資料
其實除了在shiro的配置檔案配置過濾規則,也可以通過註解的方式在controller上加入許可權,效果是一樣的
圖中框起來的地方可以設定 AND 或者是 OR , 就是設定多個角色的時候, 是全部滿足還是滿足一個即可
基於角色的許可權設定粒度還比較粗,可以在細一點,針對每個功能進行設定,這時候就用到了那張許可權表
還用剛才的做測試,設定兩個許可權,我們在資料庫設定的許可權是 ‘book:*’ , 測試發現沒問題可以請求到
其實除了這種配置方式,還可以通過註解的方式,配置方式類似角色的設定
這樣許可權配置就差不多了,還有種情況就是要對頁面中按鈕之類的進行許可權控制,做法其實也比較簡單
在thymelef 下使用html 進行測試,需要的jar 上面已經匯入了,在shiro的config中配置ShiroDialect ,這個上面的配置檔案也已經配置好了,剩下的就是在頁面頭部中引入xmlns
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
複製程式碼
頁面裡放兩個按鈕,配置不同的許可權,資料庫中的許可權改為"book:list",看一下效果
<tr>
<td colspan="2">
<button shiro:hasPermission="book:list" type="reset">
重置
</button>
<button shiro:hasPermission="book:add" type="button" onclick="submit1()">
提交
</button>
</td>
</tr>
複製程式碼
發現只有擁有許可權的按鈕才能顯示出來,而且檢視頁面原始碼發現沒有許可權的按鈕根本就沒有生成在頁面中
總結一下,瞭解了上面的這一系列概念和配置,shiro的基本使用應該是沒啥問題的了,接下來在研究一下shiro怎麼做單點登入的。