Shiro系列文章:
【Shiro】Apache Shiro架構之身份認證(Authentication)
【Shiro】Apache Shiro架構之權限認證(Authorization)
【Shiro】Apache Shiro架構之集成web
【Shiro】Apache Shiro架構之實際運用(整合到Spring中)
之前寫的博客裏都是使用.ini文件來獲取信息的,包括用戶信息,角色信息,權限信息等。進入系統時,都是從.ini文件這讀取進入的。實際中除非這個系統特別特別簡單,否則一般都不是這樣乾的,這些信息都是需要在數據庫中進行維護的,所以就需要用到自定義realm了。
寫在前面:這篇博文是基於上一篇Shiro集成web基礎之上修改的。
1. 數據庫建表
首先在數據庫中新建三個表:t_user,t_role和t_permission,分別存儲用戶信息,角色信息和權限信息,建表語句如下:
CREATE TABLE `t_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`rolename` varchar(20) DEFAULT NULL COMMENT '角色名稱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶主鍵',
`username` varchar(20) NOT NULL COMMENT '用戶名',
`password` varchar(20) NOT NULL COMMENT '密碼',
`role_id` int(11) DEFAULT NULL COMMENT '外鍵關聯role表',
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
CONSTRAINT `t_user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
CREATE TABLE `t_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`permissionname` varchar(50) NOT NULL COMMENT '權限名',
`role_id` int(11) DEFAULT NULL COMMENT '外鍵關聯role',
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
每個表中我添加了一些測試數據,如下:
2. 自定義realm
自定義realm中需要操作數據庫,所有首先得先寫一個dao,使用的是原始的jdbc,主要是下面的自定義realm。
public class UserDao {
//根據用戶名查找用戶
public User getByUsername(Connection conn, String username) throws Exception {
User resultUser = null;
String sql = "select * from t_user where username=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
if(rs.next()) {
resultUser = new User();
resultUser.setId(rs.getInt("id"));
resultUser.setUsername(rs.getString("username"));
resultUser.setPassword(rs.getString("password"));
}
return resultUser;
}
//根據用戶名查找改用戶所擁有的角色
public Set<String> getRoles(Connection conn, String username) throws Exception {
Set<String> roles = new HashSet<String>();
String sql = "select * from t_user u, t_role r where u.role_id=r.id and u.username=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
roles.add(rs.getString("rolename"));
}
return roles;
}
//根據用戶名查找該用戶角色所擁有的權限
public Set<String> getPerms(Connection conn, String username) throws Exception {
Set<String> perms = new HashSet<String>();
String sql = "select * from t_user u, t_role r, t_permission p where u.role_id=r.id and p.role_id=r.id and u.username=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ResultSet rs = ps.executeQuery();
while(rs.next()) {
perms.add(rs.getString("permissionname"));
}
return perms;
}
}
有了dao了,接下來就可以寫自定義的realm了,自定義realm需要繼承AuthorizingRealm類,因爲該類封裝了很多方法,它也是一步步繼承自Realm類的,繼承了AuthorizingRealm類後,需要重寫兩個方法:
doGetAuthenticationInfo()方法:用來驗證當前登錄的用戶,獲取認證信息
doGetAuthorizationInfo()方法:用來爲當前登陸成功的用戶授予權限和角色(已經登陸成功了)
下面來看一下具體的實現:
public class MyRealm extends AuthorizingRealm {
private UserDao userDao = new UserDao();
// 爲當前登陸成功的用戶授予權限和角色,已經登陸成功了
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal(); //獲取用戶名
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Connection conn = null;
try {
conn = DbUtil.getConnection();
authorizationInfo.setRoles(userDao.getRoles(conn, username)); //設置角色
authorizationInfo.setStringPermissions(userDao.getPerms(conn, username)); //設置權限
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
DbUtil.closeConnection(conn);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return authorizationInfo;
}
// 驗證當前登錄的用戶,獲取認證信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); // 獲取用戶名
Connection conn = null;
try {
conn = DbUtil.getConnection();
User user = userDao.getByUsername(conn, username); // 僅僅是根據用戶名查出的用戶信息,不涉及到密碼
if (user != null) {
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(
user.getUsername(), user.getPassword(), "myrealm");
return authcInfo;
} else {
return null;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
DbUtil.closeConnection(conn);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
}
從上面兩個方法中可以看出:驗證身份的時候是根據用戶輸入的用戶名先從數據庫中查出該用戶名對應的用戶,這時候並沒有涉及到密碼,也就是說到這一步的時候,即使用戶輸入的密碼不對,也是可以查出來該用戶的,然後將該用戶的正確信息封裝到authcInfo 中返回給Shiro,接下來就是Shiro的事了,它會根據這裏面的真實信息與用戶前臺輸入的用戶名和密碼進行校驗, 這個時候也要校驗密碼了,如果校驗通過就讓用戶登錄,否則跳轉到指定頁面。同理,權限驗證的時候也是先根據用戶名獲取與該用戶名有關的角色和權限,然後封裝到authorizationInfo中返回給Shiro。
3. 修改ini文件
在該配置文件中,[users]和[roles]的信息就可以刪掉了,因爲這些信息都是從數據庫中維護的,另外還要在文件中指定我們自定義的realm的完全限定名,並且指定securityManager的realm使用我們自定義的realm,如下:
[main]
authc.loginUrl=/login
roles.unauthorizedUrl=/unauthorized.jsp
perms.unauthorizedUrl=/unauthorized.jsp
#定義自己的realm
myRealm=demo.shiro.realm.MyRealm
securityManager.realms=$myRealm
#定義請求的地址需要做什麼驗證
[urls]
/login=anon
/admin=authc
/student=roles[teacher]
/teacher=perms["user:create"]
這樣我們自定義的realm就搞定了,根據配置文件,當我們請求…/admin的時候會進行身份認證,所以會進入LoginServlet中,當調用currentUser.login(token);
的時候,就會進入我們自定義的realm中的doGetAuthenticationInfo方法進行身份初始化,然後交給Shiro去驗證。當我們請求…./student的時候,也會先進行身份驗證,就是上面的過程,然後驗證通過,當我們再次請求…/student的時候,就會進入我們自定義的realm中的doGetAuthorizationInfo方法進行權限的初始化,然後交給Shiro去驗證。
下一篇博文我將總結一下如何將Shiro運用到實際項目中,即如何將Shiro整合到Spring中。