因爲最近在作一個項目,剛完成到登陸註冊,不想和之前的項目搬一樣的磚了,想完成點不那麼low的功能,像單點登陸、權限控制等,因而就想起了Shiro框架。mysql
任何一種技術總有個開始,又老是這麼巧,每一個開始老是個HelloWorld。 官方給出的依賴:算法
示例代碼:sql
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的總結。apache
Shiro不只不依賴任何容器,能夠在EE環境下運行,也能夠在SE環境下運行,在快速入門中,我在SE環境下體驗了Shiro的登陸驗證、角色驗證、權限驗證功能。安全
[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() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
複製代碼
/**
* 獲取當前用戶(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);
}
複製代碼
判斷用戶是否擁有某項權限/**
* 判斷用戶是否擁有某種權限
*
* @param user
* @param permit
* @return
*/
public static boolean isPermit(User user, String permit) {
Subject subject = getSubject();
return subject.isPermitted(permit);
}
複製代碼
有了這四個方法,咱們就能夠開始寫測試類了。我會建立兩個在配置文件中的用戶 —— Object and Reader 和一個不在配置文件中的用戶 —— Tompublic 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+" 的權限 ");
}
}
}
}
}
複製代碼
運行結果以下(紅字是因爲缺乏部分jar,暫不解決):到這裏爲止,已經完成了Shiro的入門。可是在實際項目中,咱們不可能用配置文件配置用戶權限,因此仍是得結合數據庫進行開發。bash
要結合數據庫進行開發,得先理解一個概念 —— RABC。架構
RBAC 是當下權限系統的設計基礎,同時有兩種解釋: 一: Role-Based Access Control,基於角色的訪問控制。 即:你要可以增刪改查博客,那麼當前用戶就必須擁有博主這個角色。 二:Resource-Based Access Control,基於資源的訪問控制。 即,你要可以讀博客、評論博客,那麼當前用戶就必須擁有讀者這樣的權限。框架
因此,基於這個概念,咱們的數據庫將有:用戶表、角色表、權限表、用戶——角色關係表、權限——角色關係表,其中用戶與角色關係爲多對多,即一個用戶能夠對應多個角色,一個角色也能夠由多個用戶扮演,權限與角色關係也爲多對多,即一個角色能夠有多個權限,一個權限也能夠賦予多個角色。dom
我使用的是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,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;
}
}
複製代碼
[main]
databaseRealm=com.shirotest.DatabaseRealm
securityManager.realms=$databaseRealm
複製代碼
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_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"));
}
}
複製代碼
運行結果:
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;
}
}
複製代碼
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框架也提供了本身的一套加密服務,這裏就說說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, password, salt, times).toString();
/***********************************加密結束***********************************/
String sql = "INSERT INTO user(name,password,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", password, 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,password,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", password, 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,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());
return simpleAuthenticationInfo;
複製代碼
主要是修改了驗證信息,將數據庫中的密碼和鹽傳入,讓它自行判斷,咱們就無需再寫判斷邏輯了。SimpleAuthenticationInfo(username,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());
運行結果:
到這裏爲止,Shiro關於SE的部分應該就告一段落了,以後要開始學習關於集成Web和集成框架了,我以爲對於Shiro的架構及原理,得單獨瀏覽一遍,由於到此爲止我也只知道Shiro是怎麼使用的,可是其中Realm類中的那兩個方法,什麼時候調用,爲何會調用,還有SimpleAuthenticationInfo返回後是怎麼判斷登陸成功或者失敗的,能夠說是很模糊,學完集成框架後我應該會選擇再看看其中的原理。
歡迎你們訪問個人我的博客:Object's Blog