一、Shiro框架介紹
1.什么是Shiro坝咐?
Apache Shiro是一個(gè)強(qiáng)大易用的Java安全框架铃剔,提供了認(rèn)證撒桨、授權(quán)、加密和 會(huì)話管理等功能键兜。
- Shiro應(yīng)用場景:
對(duì)于任何一個(gè)應(yīng)用程序凤类,Shiro都可以提供全面的安全管理服務(wù)。其不僅可 以用在JavaSE環(huán)境普气,也可以用在JavaEE環(huán)境谜疤。
2.Shiro的架構(gòu)圖:
-
從外部看shiro:
shiro.png -
從Shiro內(nèi)部看Shiro的框架:
Shiro.png - Shiro中常見的英文名詞:
Subject:主體;
Security:安全现诀;
Realm:領(lǐng)域夷磕,范圍;
Authenticator:認(rèn)證器赶盔;
Authentication:認(rèn)證企锌;
Authorizer:授權(quán)器;
Authorization:授權(quán)于未;
Cryptography:密碼;
Credential:證書陡鹃、憑證烘浦;
Matcher:匹配器;
Principal:身份萍鲸。
二闷叉、Shiro環(huán)境搭建
1.Shiro中的配置文件:
shiro.ini文件放在classpath下,shiro會(huì)自動(dòng)查找。其中格式是key/value 鍵值對(duì)配置脊阴。INI配置文件一般適用于用戶少且不需要在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的 情景下使用握侧。 ini文件中主要配置有四大類:main蚯瞧,users,roles品擎,urls埋合。
(1)[mian]:
main主要配置shiro的一些對(duì)象,例如securityManager 萄传,Realm甚颂, authenticator,authcStrategy 等等秀菱。
(2)[users]:
[users]允許你配置一組靜態(tài)的用戶振诬,包含用戶名,密碼衍菱,角色赶么,一個(gè)用戶 可以有多個(gè)角色,可以配置多個(gè)角色脊串。
(3)[roles]:
[roles]將角色和權(quán)限關(guān)聯(lián)起來辫呻,格式為:角色名=權(quán)限字符串1,權(quán)限字符 串2…..洪规。
(4)印屁、[roles]
[roles]將角色和權(quán)限關(guān)聯(lián)起來,格式為:角色名=權(quán)限字符串1斩例,權(quán)限字符 串2…..雄人。
2.Shiro環(huán)境搭建實(shí)現(xiàn)簡單的認(rèn)證:
認(rèn)證:驗(yàn)證用戶是否合法 在 shiro 中,用戶需要提供principals (身份)和credentials(憑證) 給shiro念赶,從而實(shí)現(xiàn)對(duì)用戶身份的驗(yàn)證础钠。
- principals:
身份,即主體的標(biāo)識(shí)屬性叉谜,可以是任何東西旗吁,如用戶名、郵箱等停局,唯一即可很钓。 例如:用戶名/郵箱/手機(jī)號(hào)等。
- .credentials
憑證董栽,即只有主體知道的安全值码倦,如密碼/數(shù)字證書等。 最常見的principals和credentials組合就是用戶名/密碼锭碳。
-
實(shí)現(xiàn)簡單的認(rèn)證:
(1)導(dǎo)入jar包:
jar
(2)shiro.ini:
[users]
root = 1234
(3)測試:
package com.zlw.test;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
//實(shí)現(xiàn)簡單認(rèn)證
public class AuthenticationTest {
@Test
public void testAuthentication() {
//1.構(gòu)建SecurityManager工廠
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.通過securityManagerFactory工廠獲取SecurityManager實(shí)例
SecurityManager securityManager = securityManagerFactory.getInstance();
//3.將securityManager設(shè)置到運(yùn)行環(huán)境中
SecurityUtils.setSecurityManager(securityManager);
//4.獲取subject實(shí)例
Subject subject = SecurityUtils.getSubject();
//5.創(chuàng)建用戶名和密碼驗(yàn)證令牌Token
UsernamePasswordToken token = new UsernamePasswordToken("root","1234");
//6.進(jìn)行身份驗(yàn)證
subject.login(token);
//7.判斷是否驗(yàn)證通過
System.out.println("驗(yàn)證是否通過:"+subject.isAuthenticated());
}
}
3.Shiro內(nèi)置的JDBCRealm:
Shiro默認(rèn)使用自帶的IniRealm袁稽,IniRealm從ini配置文件中讀取用戶的信息。 大部分情況下需要從系統(tǒng)的數(shù)據(jù)庫中讀取用戶信息擒抛,所以需要使用JDBCRealm或自定義Realm推汽;:使用JDBCRealm提供數(shù)據(jù)源补疑,從而實(shí)現(xiàn)認(rèn)證 。
在JDBCRealm中限定了表中的表名和字段歹撒。
(1)創(chuàng)建users表莲组,(字段為username,password):
(2)導(dǎo)入jar包:
(3)配置shiro.ini:
[main]
#配置Realm
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
#配置數(shù)據(jù)源
dataSource = com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass = com.mysql.jdbc.Driver
dataSource.jdbcUrl = jdbc:mysql:///shiro
dataSource.user = root
dataSource.password = root
jdbcRealm.dataSource = $dataSource
#將Realm注入給SecurityManager
securityManager.realm = $jdbcRealm
(4)測試:
package com.zlw.test;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class AuthenticationTest {
@Test
public void testAuthentication() {
//1.構(gòu)建SecurityManager工廠
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.通過securityManagerFactory工廠獲取SecurityManager實(shí)例
SecurityManager securityManager = securityManagerFactory.getInstance();
//3.將SecurityManager設(shè)置到運(yùn)行環(huán)境中
SecurityUtils.setSecurityManager(securityManager);
//4.獲取歐subject實(shí)例
Subject subject = SecurityUtils.getSubject();
//5.創(chuàng)建用戶名密碼驗(yàn)證令牌Token
UsernamePasswordToken token = new UsernamePasswordToken("victor", "123456");
//6.進(jìn)行身份驗(yàn)證
subject.login(token);
//7.判斷是否認(rèn)證通過
System.out.println(subject.isAuthenticated());
}
}
4.自定義Realm提供數(shù)據(jù)源:
自定義Realm栈妆,可以注入給securityManager更加靈活的安全數(shù)據(jù)源通過實(shí)現(xiàn)Realm接口胁编,或根據(jù)需求繼承他的相應(yīng)子類即可。使用自定義Realm提供數(shù)據(jù)源鳞尔,從而實(shí)現(xiàn)認(rèn)證 嬉橙。
(1)創(chuàng)建數(shù)據(jù)庫表(自定義表名,字段名):
CREATE TABLE `user` (
`userid` int(5) NOT NULL AUTO_INCREMENT,
`username` varchar(30) DEFAULT NULL,
`pwd` varchar(30) DEFAULT NULL,
PRIMARY KEY (`userid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
(2)導(dǎo)入jar包:
(3)自定義Realm繼承AuthenticatingRealm類:
package com.zlw.realm;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
public class CustomRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
PreparedStatement pstm = null;
Connection conn = null;
ResultSet rs = null;
String username = token.getPrincipal().toString();
String principal = null;
String credentials = null;
SimpleAuthenticationInfo simpleAuthenticationInfo = null;
try {
//加載驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/shiro", "root", "root");
//創(chuàng)建對(duì)象
pstm = conn.prepareStatement("select *from user where username=?");
pstm.setString(1,username);
//執(zhí)行sql
rs = pstm.executeQuery();
while(rs.next()) {
principal = rs.getString("username");
credentials = rs.getString("pwd");
}
simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials,"customRealm");
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
rs.close();
pstm.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return simpleAuthenticationInfo;
}
}
(4)測試:
package com.zlw.test;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class Test01 {
@Test
public void testAuthentication() {
// 1.構(gòu)建SecurityManager工廠
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory();
// 2.通過securityManagerFactory工廠獲取SecurityManager實(shí)例
SecurityManager securityManager = securityManagerFactory.getInstance();
// 3.將SecurityManager設(shè)置到運(yùn)行環(huán)境中
SecurityUtils.setSecurityManager(securityManager);
// 4.獲取歐subject實(shí)例
Subject subject = SecurityUtils.getSubject();
// 5.創(chuàng)建用戶名密碼驗(yàn)證令牌Token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "1234");
// 6.進(jìn)行身份驗(yàn)證
subject.login(token);
// 7.判斷是否認(rèn)證通過
System.out.println(subject.isAuthenticated());
}
}
三寥假、MD5加密
1.常見 的加密算法:
-
對(duì)稱加密算法(加密與解密密鑰相同):
示例 -
非對(duì)稱加密算法(加密密鑰和解密密鑰不同):
示例 -
對(duì)稱與非對(duì)稱算法比較:
示例 -
散列算法比較:
示例
2.使用DM5加密:
- 加鹽:
使用MD5存在一個(gè)問題市框,相同的password生產(chǎn)的Hash值是相同的,如 果兩個(gè)用戶設(shè)置了相同的密碼糕韧,那么數(shù)據(jù)庫當(dāng)就會(huì)存儲(chǔ)相同的值枫振,這樣是極 不安全的。
加Salt可以一定程度上解決這一問題萤彩。所謂加Salt方法粪滤,就是加點(diǎn) “佐料”。其基本想法是這樣的:當(dāng)用戶首次提供密碼時(shí)(通常是注冊時(shí))雀扶, 由系統(tǒng)自動(dòng)往這個(gè)密碼里撒一些“佐料”杖小,然后再散列。而當(dāng)用戶登錄時(shí)愚墓, 系統(tǒng)為用戶提供的代碼撒上同樣的“佐料”予权,然后散列,再比較散列值浪册,來 確定密碼是否正確扫腺。
原理 :給原文加入隨機(jī)數(shù)生成新的MD5值。
- 迭代:
對(duì)加密的動(dòng)作進(jìn)行重復(fù)反饋過程的活動(dòng)村象;加密的次數(shù)笆环。
- 代碼示例:
package com.zlw.test;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.junit.Test;
//MD5加密、加鹽以及迭代
public class MD5Test {
@Test
public void testMD5() {
//md5加密
Md5Hash md5 = new Md5Hash("1234556");
System.out.println(md5);
}
@Test
public void testMD5Hash() {
//加鹽
Md5Hash md5 = new Md5Hash("123456","aaa");
System.out.println(md5);
}
@Test
public void testMD5Hash2() {
//迭代
Md5Hash md5 = new Md5Hash("123456","sxt",3);
System.out.println(md5);
}
}
3.憑證匹配器和Shiro的授權(quán):
- 憑證匹配器:
在Realm接口的實(shí)現(xiàn)類AuthenticatingRealm中有credentialsMatcher屬性厚者。 意為憑證匹配器咧织。常用來設(shè)置加密算法及迭代次數(shù)等。
- Shiro授權(quán):
授權(quán):又稱作為訪問控制籍救,是對(duì)資源的訪問管理的過程。即對(duì)于 認(rèn)證通過的用戶渠抹,授予他可以訪問某些資源的權(quán)限蝙昙。
Shiro 支持三種方式的授權(quán):代碼觸發(fā)闪萄、注解觸發(fā)、標(biāo)簽觸發(fā) 奇颠。
4.使用MD5加密和Shiro授權(quán)實(shí)現(xiàn)菜單授權(quán):
(1)創(chuàng)建數(shù)據(jù)庫:
-
users表:
示例 -
roles角色表(角色和user是一對(duì)多關(guān)系):
示例 -
Menus菜單表(菜單和角色是多對(duì)多關(guān)系):
示例 -
角色和菜單的中間表:
示例
(2)導(dǎo)入jar包:
jar
(3)自定義Realm繼承AuthorizingRealm類败去;重寫doGetAuthenticationInfo(AuthenticationToken token):認(rèn)證的方法;doGetAuthorizationInfo(PrincipalCollection collection):授權(quán)方法烈拒;
package com.zlw.realm;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* AuthenticatingRealm:認(rèn)證使用的Realm,只包含認(rèn)證的方法圆裕,認(rèn)證時(shí)調(diào)用doGetAuthenticationInfo方法
* AuthorizingRealm:授權(quán)使用的Realm,繼承了AuthenticatingRealm, 包含認(rèn)證和授權(quán)的方法,
* 認(rèn)證時(shí)調(diào)用doGetAuthenticationInfo方法,授權(quán)時(shí)調(diào)用doGetAuthorizationInfo方法
*
* 自定義Realm的實(shí)現(xiàn)步驟: 我們通過自定義Realm從指定的數(shù)據(jù)源中獲取數(shù)據(jù)
* 1.自定義Realm,繼承AuthorizingRealm類
* 2.重寫認(rèn)證和授權(quán)的方法 doGetAuthenticationInfo(AuthenticationToken token):認(rèn)證的方法
* doGetAuthorizationInfo(PrincipalCollection arg0):授權(quán)方法
* 3.在配置文件(shiro.ini)中配置自定義的realm customRealm = com.sxt.realm.CustomRealm
* securityManager.realm = $customRealm
*
* @author zhang
*
*/
public class CustomRealm extends AuthorizingRealm {
// 授權(quán)方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
Connection conn = null;
PreparedStatement pstm = null;
ResultSet rs = null;
String username = token.getPrincipal().toString();//從令牌中獲取身份信息
String principal = null;
String credentials = null;
String password_salt = null;
SimpleAuthenticationInfo simpleAuthenticationInfo = null;
try {
// 加載驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/rbac", "root", "root");
pstm = conn.prepareStatement("select *from users where username=?");
pstm.setString(1, username);
rs = pstm.executeQuery();
while (rs.next()) {
principal = rs.getString("username");
credentials = rs.getString("userpwd");
password_salt = rs.getString("salt");//獲取鹽值
}
simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials,ByteSource.Util.bytes(password_salt), "customRealm");
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
rs.close();
pstm.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return simpleAuthenticationInfo;
}
// 響應(yīng)方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection collection) throws AuthenticationException {
Connection conn = null;
PreparedStatement pstm = null;
ResultSet rs = null;
String userName = collection.getPrimaryPrincipal().toString();//獲取主體身份信息
Set<String> roleNameSet = new HashSet<String>();//創(chuàng)建Set集合擁有封裝所有的菜單名稱
Set<String> menuNameSet = new HashSet<String>();//創(chuàng)建Set集合擁有封裝所有的菜單名稱
try {
//加載驅(qū)動(dòng)
Class.forName("com.mysql.jdbc.Driver");
//獲取鏈接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/rbac", "root", "root");
String sql = "select r.roleName,m.menuName "
+ "from users u, roles r, roles_menus rm,menus m "
+ "where u.role_id=r.roleid "
+ "and r.roleid=rm.roles_id "
+ "and rm.menus_id=m.menuid "
+ "and u.username=?";
pstm = conn.prepareStatement(sql);
pstm.setString(1, userName);
rs = pstm.executeQuery();
while (rs.next()) {
String roleName = rs.getString("roleName");
String menuName = rs.getString("menuName");
roleNameSet.add(roleName);
menuNameSet.add(menuName);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}finally {
try {
rs.close();
pstm.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(roleNameSet);
simpleAuthorizationInfo.addStringPermissions(menuNameSet);
return simpleAuthorizationInfo;
}
}
(4)配置Shiro.ini文件:
[main]
#配置憑證配置器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#設(shè)置憑證配置器的算法名稱
credentialsMatcher.hashAlgorithmName=MD5
#設(shè)置憑證匹配器的的迭代次數(shù)
credentialsMatcher.hashIterations=3
#配置Realm
customRealm =com.zlw.realm.CustomRealm
#設(shè)置Realm的憑證匹配器
customRealm.credentialsMatcher=$credentialsMatcher
#將Realm注入給SecurityManager
securityManager.realm = $customRealm
(5)測試:
package com.zlw.test;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class MD5Test {
@Test
public void TestMD5() {
//1.構(gòu)建SecurityManager工廠
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.通過securityManagerFactory工廠獲取SecurityManager實(shí)例
SecurityManager securityManager = securityManagerFactory.getInstance();
//3.將securityManager設(shè)置到運(yùn)行環(huán)境中
SecurityUtils.setSecurityManager(securityManager);
//4.獲取subject實(shí)例
Subject subject = SecurityUtils.getSubject();
//5.創(chuàng)建用戶名和密碼驗(yàn)證令牌Token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
//6.進(jìn)行身份驗(yàn)證
subject.login(token);
//7.判斷是否驗(yàn)證通過
System.out.println("驗(yàn)證是否通過"+subject.isAuthenticated());
//判斷該用戶是否擁有指定的角色
System.out.println("判斷是否擁有指定角色:"+subject.hasRole("管理員"));
//8.判斷該用戶是否擁有指定的權(quán)限
System.out.println("是否擁有指定權(quán)限:"+subject.isPermitted("客戶管理"));
}
}
5.可能會(huì)遇到的異常信息:
(1)UnknownAccountException:No account found for user
用戶主體找不到荆几,用戶名錯(cuò)誤吓妆。
(2)IncorrectCredentialsException:did not match the expected credentials
用戶認(rèn)證沒有找到相匹配的憑證;用戶密碼錯(cuò)誤吨铸。