關於Shiro框架的學習(一)
前言
由於最近在做一個專案,剛完成到登入註冊,不想和以前的專案搬同樣的磚了,想完成點不那麼low的功能,像單點登入、許可權控制等,於是就想起了Shiro框架。
初識Shiro
任何一種技術總有個開始,又總是這麼巧,每個開始總是個HelloWorld。 官方給出的依賴:
示例程式碼:
public class FirstShiro {
private static final transient Logger log = LoggerFactory.getLogger(FirstShiro.class);
public static void main(String[] args) {
// TODO Auto-generated method stub
log.info("My First Apache Shiro Application" );
System.exit(0);
}
}
複製程式碼
執行結果:
[main] INFO com.shiro.first.FirstShiro - My First Apache Shiro Application
複製程式碼
Shiro概念
在沒有Shiro的時候,我們在做專案中的登入、許可權之類的功能有五花八門的實現方式,不同系統的做法不統一。但是有Shiro之後,大家就可以一致化地做許可權系統,優點就是各自的程式碼不再晦澀難懂,有一套統一的標準。另外Shiro框架也比較成熟,能很好地滿足需求。這就是我對Shiro的總結。
在Java SE中驗證Shiro
Shiro不僅不依賴任何容器,可以在EE環境下執行,也可以在SE環境下執行,在快速入門中,我在SE環境下體驗了Shiro的登入驗證、角色驗證、許可權驗證功能。
-
配置檔案方式
- 在src目錄下建立shiro.ini檔案,內容如下:
[users] #使用者 密碼 角色 #部落格管理員 Object=123456,BlogManager #讀者 Reader=654321,SimpleReader #定義各種角色 [roles] #部落格管理員許可權 BlogManager=addBlog,deleteBlog,modifyBlog,readBlog #普通讀者許可權 SimpleReader=readBlog,commentBlog 複製程式碼
- 建立使用者實體類
/** * @author Object * 使用者實體類 */ public class User { private String name; private String password; public String getName
- 建立ShiroTest,驗證登入、許可權、角色: 獲取當前使用者:
登入:/** * 獲取當前使用者(Subject) * * @param user * @return */ public static Subject getSubject() { // 載入配置檔案,獲取SecurityManager工廠 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); // 從工廠中獲取SecurityManager物件 SecurityManager securityManager = factory.getInstance(); // 通過SecurityUtil將SecurityManager物件放入全域性物件 SecurityUtils.setSecurityManager(securityManager); // 全域性物件通過SecurityManager生成Subject Subject subject = SecurityUtils.getSubject(); return subject; } 複製程式碼
判斷使用者是否為某個角色:/** * 使用者登入方法 * * @param user * @return */ public static boolean login(User user) { Subject subject = getSubject(); // 如果使用者已經登入 則退出 if (subject.isAuthenticated()) { subject.logout(); } // 封裝使用者資料 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword()); // 驗證使用者資料 try { subject.login(token); } catch (AuthenticationException e) { // 登入失敗 // e.printStackTrace();為了看結果,暫時不讓它列印 return false; } return subject.isAuthenticated(); } 複製程式碼
判斷使用者是否擁有某項許可權/** * 判斷使用者是否擁有某個角色 * * @param user * @param role * @return */ public static boolean hasRole(User user,String role) { Subject subject = getSubject(); return subject.hasRole(role); } 複製程式碼
有了這四個方法,我們就可以開始寫測試類了。我會建立兩個在配置檔案中的使用者 —— Object and Reader 和一個不在配置檔案中的使用者 —— Tom/** * 判斷使用者是否擁有某種許可權 * * @param user * @param permit * @return */ public static boolean isPermit(User user,String permit) { Subject subject = getSubject(); return subject.isPermitted(permit); } 複製程式碼
執行結果如下(紅字是由於缺少部分jar,暫不解決):public static void main(String[] args) { // 使用者Object User object = new User(); object.setName("Object"); object.setPassword("123456"); // 使用者Reader User reader = new User(); reader.setName("Reader"); // 錯誤的密碼 reader.setPassword("654321"); // 不存在的使用者 User tom = new User(); tom.setName("Tom"); tom.setPassword("123456"); List<User> users = new LinkedList<User>(); users.add(object); users.add(reader); users.add(tom); // 角色:BlogManager String blogManager = "BlogManager"; // 角色:SimpleReader String simpleReader = "SimpleReader"; List<String> roles = new LinkedList<String>(); roles.add(blogManager); roles.add(simpleReader); // 許可權 String addBlog = "addBlog"; String deleteBlog = "deleteBlog"; String modifyBlog = "modifyBlog"; String readBlog = "readBlog"; String commentBlog = "commentBlog"; List<String> permits = new LinkedList<String>(); permits.add(addBlog); permits.add(deleteBlog); permits.add(modifyBlog); permits.add(readBlog); permits.add(commentBlog); /**************************** 開始驗證 ****************************/ System.out.println("=========================驗證使用者是否登入成功========================="); // 驗證使用者是否登入成功 for (User u : users) { if (login(u)) { System.out.println("使用者:" + u.getName() + " 登入成功 " + "密碼為:" + u.getPassword()); } else { System.out.println("使用者:" + u.getName() + " 登入失敗 " + "密碼為:" + u.getPassword()); } } System.out.println("=========================驗證使用者角色資訊========================="); // 驗證使用者角色 for (User u : users) { for (String role : roles) { if (login(u)) { if (hasRole(u,role)) { System.out.println("使用者:" + u.getName() + " 的角色是" + role); } } } } System.out.println("=========================驗證使用者許可權資訊========================="); for(User u:users) { System.out.println("========================="+u.getName()+"許可權========================="); for(String permit:permits) { if(login(u)) { if(isPermit(u,permit)) { System.out.println("使用者:"+u.getName() +" 有 "+permit+" 的許可權 "); } } } } } 複製程式碼
到這裡為止,已經完成了Shiro的入門。但是在實際專案中,我們不可能用配置檔案配置使用者許可權,所以還是得結合資料庫進行開發。
Shiro結合資料庫
-
RABC概念
要結合資料庫進行開發,得先理解一個概念 —— RABC。
RBAC 是當下許可權系統的設計基礎,同時有兩種解釋: 一: Role-Based Access Control,基於角色的訪問控制。 即:你要能夠增刪改查部落格,那麼當前使用者就必須擁有博主這個角色。 二:Resource-Based Access Control,基於資源的訪問控制。 即,你要能夠讀部落格、評論部落格,那麼當前使用者就必須擁有讀者這樣的許可權。
所以,基於這個概念,我們的資料庫將有:使用者表、角色表、許可權表、使用者——角色關係表、許可權——角色關係表,其中使用者與角色關係為多對多,即一個使用者可以對應多個角色,一個角色也可以由多個使用者扮演,許可權與角色關係也為多對多,即一個角色可以有多個許可權,一個許可權也可以賦予多個角色。
-
需要的Jar包
-
資料庫構建
我使用的是MySQL,建立語句如下:
CREATE DATABASE shiro; USE shiro; CREATE TABLE user( id bigint primary key auto_increment,name varchar(16),password varchar(32) )charset=utf8 ENGINE=InnoDB; create table role ( id bigint primary key auto_increment,name varchar(32) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint primary key auto_increment,name varchar(32) ) charset=utf8 ENGINE=InnoDB; create table user_role ( uid bigint,rid bigint,constraint pk_users_roles primary key(uid,rid) ) charset=utf8 ENGINE=InnoDB; create table role_permission ( rid bigint,pid bigint,constraint pk_roles_permissions primary key(rid,pid) ) charset=utf8 ENGINE=InnoDB; 複製程式碼
往資料庫中插入資料:
INSERT INTO `user` VALUES (1,'Object','123456'); INSERT INTO `user` VALUES (2,'Reader','654321'); INSERT INTO `user_role` VALUES (1,1); INSERT INTO `user_role` VALUES (2,2); INSERT INTO `role` VALUES (1,'blogManager'); INSERT INTO `role` VALUES (2,'reader'); INSERT INTO `permission` VALUES (1,'addBlog'); INSERT INTO `permission` VALUES (2,'deleteBlog'); INSERT INTO `permission` VALUES (3,'modifyBlog'); INSERT INTO `permission` VALUES (4,'readBlog'); INSERT INTO `permission` VALUES (5,'commentBlog'); INSERT INTO `role_permission` VALUES (1,1); INSERT INTO `role_permission` VALUES (1,2); INSERT INTO `role_permission` VALUES (1,3); INSERT INTO `role_permission` VALUES (1,4); INSERT INTO `role_permission` VALUES (2,5); 複製程式碼
-
工程構建
- 建立實體類
public class User { private int id; private String name; private String password; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getId() { return id; } public void setId(int id) { this.id = id; } } 複製程式碼
- 修改shiro.ini配置檔案 因為我們要從資料庫中獲取使用者、角色、許可權資訊,所以就不需要在配置檔案中配置了,但是我們要有一個Realm的概念,Realm在我的理解中,可以把它當作一個橋樑,當使用者(Subject)請求Realm時,Realm就會去尋找ini配置檔案或者資料庫中的使用者資訊,Realm就是真正對使用者做驗證的關卡。我們要在DatabaseRealm中重寫兩個方法,分別是驗證使用者和授權,那麼Shiro怎麼找到Realm呢,就是靠shiro.ini配置檔案。
[main] databaseRealm=com.shirotest.DatabaseRealm securityManager.realms=$databaseRealm 複製程式碼
- 建立ShiroDao類 這個類主要是從資料庫中取出使用者、許可權列表、角色列表。 程式碼如下:
public class ShiroDao { private static Connection connection = null; private static PreparedStatement preparedStatement = null; static { try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC","root","971103"); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 通過使用者名稱獲取密碼 * * @param username * @return */ public static String getPassword(String username) { String sql = "select password from user where name = ?"; ResultSet rs = null; try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,username); rs = preparedStatement.executeQuery(); if (rs.next()) return rs.getString("password"); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } public static Set<String> getRoles(String username) { String sql = "select role.name " + "from role,user_role,user " + "where user.id=user_role.uid " + "and user_role.rid=role.id " + "and user.name = ?"; ResultSet rs = null; Set<String> set = new HashSet<>(); try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,username); rs = preparedStatement.executeQuery(); while(rs.next()) { set.add(rs.getString("name")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return set; } public static Set<String> getPermits(String username) { String sql = "select permission.name " + "from" + " permission,role_permission,role,user " + "where " + "permission.id = role_permission.pid " + "and role_permission.rid = role.id " + "and role.id = user_role.rid " + "and user_role.uid = user.id " + "and user.name = ?"; ResultSet rs = null; Set<String> set = new HashSet<>(); try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,username); rs = preparedStatement.executeQuery(); while (rs.next()) { set.add(rs.getString("name")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return set; } public static void main(String[] args) { System.out.println("Object的角色:" + new ShiroDao().getRoles("Object")); System.out.println("Reader的角色:" + new ShiroDao().getRoles("Reader")); System.out.println("Object的許可權:"+new ShiroDao().getPermits("Object")); System.out.println("Reader的許可權:"+new ShiroDao().getPermits("Reader")); } } 複製程式碼
執行結果:
- DatabaseRealm
public class DatabaseRealm extends AuthorizingRealm{ /** *授權的方法 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { //只有認證成功了,Shiro才會呼叫這個方法進行授權 //1.獲取使用者 String username = (String) principal.getPrimaryPrincipal(); //2.獲取角色和許可權列表 Set<String> roles = ShiroDao.getRoles(username); Set<String> permissions = ShiroDao.getPermits(username); //3.授權 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } /** *驗證使用者名稱密碼是否正確的方法 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.獲取使用者名稱密碼 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //獲取使用者名稱 String username = usernamePasswordToken.getUsername(); //獲取密碼 String password = usernamePasswordToken.getPassword().toString(); //獲取資料庫中的密碼 String passwordInDatabase = ShiroDao.getPassword(username); //為空則表示沒有當前使用者,密碼不匹配表示密碼錯誤 if(null == passwordInDatabase||!password.equals(passwordInDatabase)) { throw new AuthenticationException(); } //認證資訊:放使用者名稱密碼 getName()是父類的方法,返回當前類名 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName()); return simpleAuthenticationInfo; } } 複製程式碼
- 測試類TestShiro
public class TestShiro { public static void main(String[] args) { // 使用者Object User object = new User(); object.setName("Object"); object.setPassword("123456"); // 使用者Reader User reader = new User(); reader.setName("Reader"); // 錯誤的密碼 reader.setPassword("654321"); // 不存在的使用者 User tom = new User(); tom.setName("Tom"); tom.setPassword("123456"); List<User> users = new LinkedList<User>(); users.add(object); users.add(reader); users.add(tom); // 角色:BlogManager String blogManager = "blogManager"; // 角色:SimpleReader String simpleReader = "reader"; List<String> roles = new LinkedList<String>(); roles.add(blogManager); roles.add(simpleReader); // 許可權 String addBlog = "addBlog"; String deleteBlog = "deleteBlog"; String modifyBlog = "modifyBlog"; String readBlog = "readBlog"; String commentBlog = "commentBlog"; List<String> permits = new LinkedList<String>(); permits.add(addBlog); permits.add(deleteBlog); permits.add(modifyBlog); permits.add(readBlog); permits.add(commentBlog); /**************************** 開始驗證 ****************************/ System.out.println("=========================驗證使用者是否登入成功========================="); // 驗證使用者是否登入成功 for (User u : users) { if (login(u)) { System.out.println("使用者:" + u.getName() + " 登入成功 " + "密碼為:" + u.getPassword()); } else { System.out.println("使用者:" + u.getName() + " 登入失敗 " + "密碼為:" + u.getPassword()); } } System.out.println("=========================驗證使用者角色資訊========================="); // 驗證使用者角色 for (User u : users) { for (String role : roles) { if (login(u)) { if (hasRole(u,role)) { System.out.println("使用者:" + u.getName() + " 的角色是" + role); } } } } System.out.println("=========================驗證使用者許可權資訊========================="); for(User u:users) { System.out.println("========================="+u.getName()+"許可權========================="); for(String permit:permits) { if(login(u)) { if(isPermitted(u,permit)) { System.out.println("使用者:"+u.getName() +" 有 "+permit+" 的許可權 "); } } } } } public static Subject getSubject() { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //獲取安全管理者例項 SecurityManager sm = factory.getInstance(); //將安全管理者放入全域性物件 SecurityUtils.setSecurityManager(sm); //全域性物件通過安全管理者生成Subject物件 Subject subject = SecurityUtils.getSubject(); return subject; } public static boolean login(User user) { Subject subject = getSubject(); if(subject.isAuthenticated()) { //如果登入了,就退出登入 subject.logout(); } //封裝使用者資料 AuthenticationToken token = new UsernamePasswordToken(user.getName(),user.getPassword()); try { subject.login(token); }catch(AuthenticationException e) { return false; } return subject.isAuthenticated(); } private static boolean hasRole(User user,String role) { Subject subject = getSubject(); return subject.hasRole(role); } private static boolean isPermitted(User user,String permit) { Subject subject = getSubject(); return subject.isPermitted(permit); } } 複製程式碼
最終測試結果:
Shiro加密
我們在沒有Shiro的時候,也會使用各種加密演演算法來對使用者的密碼進行加密,Shiro框架也提供了自己的一套加密服務,這裡就說說MD5+鹽。
在不加鹽的MD5中,雖然密碼也是使用非對稱演演算法加密,同樣也不能迴轉為明文,但是別人可以使用窮舉法列出最常用的密碼,例如12345 它加密後永遠都是同一個密文,一些別有用心的人就可以通過這種常見密文得知你的密碼是12345。但是加鹽就不一樣,他是在你的密碼原文的基礎上新增上一個隨機數,這個隨機數也會隨之儲存在資料庫中,但是黑客拿到你的密碼之後他並不知道哪個隨機數是多少,所以就很難再破譯密碼。
操作一番。
首先要在資料庫中加一個"鹽"欄位
ALTER TABLE user add column salt varchar(100)
同時在User實體類中加一個salt
private String salt;
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
複製程式碼
然後在ShiroDao中加一個註冊使用者的方法。
public static boolean registerUser(String username,String password) {
/***********************************Shiro加密***********************************/
//獲取鹽值
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
//加密次數
int times = 3;
//加密方式
String type = "md5";
//加密後的最終密碼
String lastPassword = new SimpleHash(type,salt,times).toString();
/***********************************加密結束***********************************/
String sql = "INSERT INTO user(name,salt)VALUES(?,?,?)";
try {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,username);
preparedStatement.setString(2,lastPassword);
preparedStatement.setString(3,salt);
if(preparedStatement.execute()) {
return true;
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
return false;
}
複製程式碼
同時加一個獲取使用者的方法:
public static User getUser(String username) {
String sql = "select * from user where name = ?";
User user = new User();
try {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,username);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()) {
user.setName(resultSet.getString("name"));
user.setPassword(resultSet.getString("password"));
user.setSalt(resultSet.getString("salt"));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return user;
}
複製程式碼
修改之前的DatabaseRealm類中的驗證使用者方法,加一個將使用者輸入的密碼加密後與資料庫中密碼進行比對的邏輯。具體邏輯如下:
//1.獲取使用者名稱密碼
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//獲取使用者名稱
String username = usernamePasswordToken.getUsername();
//獲取密碼
String password = new String(usernamePasswordToken.getPassword());
System.out.println("明文密碼:"+password);
//獲取資料庫中的使用者
User user = ShiroDao.getUser(usernamePasswordToken.getUsername());
//String passwordInDatabase = ShiroDao.getPassword(username);
//將使用者輸入的密碼做一個加密後與資料庫中的進行比對
String passwordMd5 = new SimpleHash("md5",user.getSalt(),3).toString();
System.out.println("salt:"+user.getSalt());
System.out.println("密文密碼:"+passwordMd5);
System.out.println("正在驗證中......");
//為空則表示沒有當前使用者,密碼不匹配表示密碼錯誤
if(null == user.getPassword()||!passwordMd5.equals(user.getPassword())) {
throw new AuthenticationException();
}
//認證資訊:放使用者名稱密碼 getName9()是父類的方法,返回當前類名
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,getName());
return simpleAuthenticationInfo;
複製程式碼
main測試:
ShiroDao.registerUser("Object2","321321");
User object2 = new User();
object2.setName("Object2");
object2.setPassword("321321");
if (login(object2)) {
System.out.println("登入成功");
} else {
System.out.println("登入失敗");
}
複製程式碼
最後結果:
資料庫結果:
第二種驗證使用者的方式
剛才我們是在doGetAuthenticationInfo方法中自己寫了驗證邏輯,再來捋一遍:
1.獲取使用者輸入的密碼
2.獲取資料庫中該使用者的鹽
3.將使用者輸入的密碼進行加鹽加密
4.將加密後的密碼和資料庫中的密碼進行比對
複製程式碼
大概是要經歷這麼多步驟吧。其實Shiro提供了一個HashedCredentialsMatcher ,可以自動幫我們做這些工作。
步驟: 1.修改配置檔案
[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5 #加密方式
credentialsMatcher.hashIterations=3 #剛才我們指定的加密次數
credentialsMatcher.storedCredentialsHexEncoded=true
databaseRealm=com.shirotest.DatabaseRealm
securityManager.realms=$databaseRealm
複製程式碼
2.修改doGetAuthenticationInfo方法
//1.獲取使用者名稱密碼
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//獲取使用者名稱
String username = usernamePasswordToken.getUsername();
//獲取密碼
String password = new String(usernamePasswordToken.getPassword());
System.out.println("明文密碼:"+password);
//獲取資料庫中的使用者
User user = ShiroDao.getUser(usernamePasswordToken.getUsername());
//String passwordInDatabase = ShiroDao.getPassword(username);
//將使用者輸入的密碼做一個加密後與資料庫中的進行比對
System.out.println("資料庫中密碼:"+user.getPassword());
String passwordMd5 = new SimpleHash("md5",3).toString();
System.out.println("salt:"+user.getSalt());
System.out.println("密文密碼:"+passwordMd5);
System.out.println("正在驗證中......");
/*
* //為空則表示沒有當前使用者,密碼不匹配表示密碼錯誤 if(null ==
* user.getPassword()||!passwordMd5.equals(user.getPassword())) { throw new
* AuthenticationException(); }
*/
//認證資訊:放使用者名稱密碼 getName9()是父類的方法,返回當前類名
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());
return simpleAuthenticationInfo;
複製程式碼
主要是修改了驗證資訊,將資料庫中的密碼和鹽傳入,讓它自行判斷,我們就無需再寫判斷邏輯了。SimpleAuthenticationInfo(username,getName());
執行結果:
小結
到這裡為止,Shiro關於SE的部分應該就告一段落了,之後要開始學習關於整合Web和整合框架了,我覺得對於Shiro的架構及原理,得單獨瀏覽一遍,因為到此為止我也只知道Shiro是怎麼使用的,但是其中Realm類中的那兩個方法,何時呼叫,為什麼會呼叫,還有SimpleAuthenticationInfo返回後是怎麼判斷登入成功或者失敗的,可以說是很模糊,學完整合框架後我應該會選擇再看看其中的原理。
歡迎大家訪問我的個人部落格:Object's Blog