shiro是什么?
Shiro是apache旗下的一个开源框架, 它将软件系统的安全认证相关的功能抽取出来, 实现用户身份认证, 权限授权, 加密, 会话管理等功能, 组成一个通用的安全认证框架.
为什么用它?
使用shiro就可以非常快速地完成认证,授权等功能的开发,降低系统成本时间.
shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
模块组成
Subject
Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序.
Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权.
SecurityManager
SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有subject进行安全管理, 通过SecurityManager可以完成subject的认证,授权等,
实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等.
SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口.
Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足
大多数需求, 也可以自定义认证器.
Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限.
Realm
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如: 如果用户身份数据在数据库那么realm就需要从
数据库获取用户身份信息.
注意: 不要把realm理解成只是从数据源取数据, 在realm中还有认证权限校验的相关的代码.
sessionManager
sessionManager即会话管理,shiro框架定义了一套会话管理, 它不依赖web容器的session, 所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中
在一点管理,此特性可使它实现单点登录.
SessionDAO
SessionDAO即会话dao,使对session会话操作的一套接口, 比如要将session存储到数据库, 可以通过jdbc将会话存储到数据库.
CacheManager
CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能.
Cryptography
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发. 比如提供常用的散列, 加/解密等功能.
下面这个表是某系统的所有可操控菜单的数据,存储了对应的访问路径.
下面的是角色表:表名了哪个id干什么事,比如id为4时,是只能使用采购员的事情,而不能修改管理员密码的.因为那是系统管理员的操作.(1)
下面这个表是角色操控菜单表: 即哪些角色(id)操作哪些菜单menu(id)
跟左侧的id自增无关,我们查看menu_id对应的role_id. 这两张表(menu,role)在上面都已经存在了,即表示通过这个role-menu表进行哪个角色可操作哪些菜单的设定.
菜单menu表中有对应的url, 将可以访问的角色进行放行,否则进行拦截. (在前端页面直接不予显示)
下面是用户表,没有什么奇特的地方,我们在权限中将使用到它们的id
下面这个是比较关键的用户角色表,该表定义了哪个用户它属于什么角色.可以看到有的用户分饰多角,比如上面表的主管王大锤分饰了role(2,4,5){主管,采购员,销售经理}
而王大锤可以操作哪些菜单?通过t-role-menu就可以查看到了.具体菜单是在哪些路径呢?通过再查询t_menu就可以获取url字段了
|
|
新建一个springboot项目,引入shiro依赖
<!--shiro权限-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency> <!--shiro权限-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
简单实用shiro
新建project
需要导入shiro-all的jar包
创建的shiro.ini为shiro的配置文件
shiro.ini内容:
#定义用户
[users]
#用户名 zhang3 密码是 12345, 角色是 admin
zhang3 = 12345, admin
#用户名 li4 密码是 abcde, 角色是 产品经理
li4 = abcde,productManager
#定义角色
[roles]
#管理员什么都能做
admin = *
#产品经理只能做产品管理
productManager = addProduct,deleteProduct,editProduct,updateProduct,listProduct
#订单经理只能做订单管理
orderManager = addOrder,deleteOrder,editOrder,updateOrder,listOrder
用户实体:
package com.how2java; 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;
} }
测试类:
package com.how2java; import java.util.ArrayList;
import java.util.List; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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.apache.shiro.util.Factory; public class TestShiro {
public static void main(String[] args) {
//用户们
User zhang3 = new User();
zhang3.setName("zhang3");
zhang3.setPassword("12345"); User li4 = new User();
li4.setName("li4");
li4.setPassword("abcde"); User wang5 = new User();
wang5.setName("wang5");
wang5.setPassword("wrongpassword"); List<User> users = new ArrayList<>(); users.add(zhang3);
users.add(li4);
users.add(wang5);
//角色们
String roleAdmin = "admin";
String roleProductManager ="productManager"; List<String> roles = new ArrayList<>();
roles.add(roleAdmin);
roles.add(roleProductManager); //权限们
String permitAddProduct = "addProduct";
String permitAddOrder = "addOrder"; List<String> permits = new ArrayList<>();
permits.add(permitAddProduct);
permits.add(permitAddOrder); //登陆每个用户
for (User user : users) {
if(login(user))
System.out.printf("%s \t成功登陆,用的密码是 %s\t %n",user.getName(),user.getPassword());
else
System.out.printf("%s \t成功失败,用的密码是 %s\t %n",user.getName(),user.getPassword());
} System.out.println("-------how2j 分割线------"); //判断能够登录的用户是否拥有某个角色
for (User user : users) {
for (String role : roles) {
if(login(user)) {
if(hasRole(user, role))
System.out.printf("%s\t 拥有角色: %s\t%n",user.getName(),role);
else
System.out.printf("%s\t 不拥有角色: %s\t%n",user.getName(),role);
}
}
}
System.out.println("-------how2j 分割线------"); //判断能够登录的用户,是否拥有某种权限
for (User user : users) {
for (String permit : permits) {
if(login(user)) {
if(isPermitted(user, permit))
System.out.printf("%s\t 拥有权限: %s\t%n",user.getName(),permit);
else
System.out.printf("%s\t 不拥有权限: %s\t%n",user.getName(),permit);
}
}
}
} private static boolean hasRole(User user, String role) {
Subject subject = getSubject(user);
return subject.hasRole(role);
} private static boolean isPermitted(User user, String permit) {
Subject subject = getSubject(user);
return subject.isPermitted(permit);
} private static Subject getSubject(User user) {
//加载配置文件,并获取工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//获取安全管理者实例
SecurityManager sm = factory.getInstance();
//将安全管理者放入全局对象
SecurityUtils.setSecurityManager(sm);
//全局对象通过安全管理者生成Subject对象
Subject subject = SecurityUtils.getSubject(); return subject;
} private static boolean login(User user) {
Subject subject= getSubject(user);
//如果已经登录过了,退出
if(subject.isAuthenticated())
subject.logout(); //封装用户的数据
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
try {
//将用户的数据token 最终传递到Realm中进行对比
subject.login(token);
} catch (AuthenticationException e) {
//验证错误
return false;
} return subject.isAuthenticated();
} }
测试输出:
zhang3 成功登陆,用的密码是 12345
li4 成功登陆,用的密码是 abcde
wang5 成功失败,用的密码是 wrongpassword
-------how2j 分割线------
zhang3 拥有角色: admin
zhang3 不拥有角色: productManager
li4 不拥有角色: admin
li4 拥有角色: productManager
-------how2j 分割线------
zhang3 拥有权限: addProduct
zhang3 拥有权限: addOrder
li4 拥有权限: addProduct
li4 不拥有权限: addOrder
|
|
在上面的shiro简单测试后,可以通过输出查看到shiro究竟是用来做什么的.
但是在实际工作中,我们都会把权限相关的内容放在数据库中,所以本知识点讲解如何放在数据库里来跑shiro.
[RBAC概念] : RBAC是当下权限系统的设计基础,同时有两种解释:
一 : Role-Based Access Control , 基于角色的访问控制, 即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二 : Resource-Based Access Control , 基于资源的访问控制, 即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限
基于角色的权限访问控制(Role-Based Access Control) 作为传统的访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注.
在RBAC中,权限与角色相关联, 用户通过成为适当角色的成员而得到这些角色的权限. 这就极大地简化了权限的管理.
在一个组织中, 角色就是为了完成各种工作而创造, 用户则依据它的责任和资格来被指派相应的角色,用户可以很容器地从一个角色
被派到另一个角色. 角色可依新的需求和系统的合并而赋予新的权限, 而权限也可根据需要而从某角色中回收. 角色与角色的关系
可以建立起来以囊括更广泛的客观情况.
[表结构] : 基于RBAC概念, 就会存在3张基础表: 用户,角色,权限, 以及2张中间表来建立用户与角色的多对多关系, 角色与权限的多对多关系.
用户与权限之间也是多对多关系,但是是通过角色间接建立的.
注: 补充多对多概念: 用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户.
一个角色可以包含多种权限,一种权限也可以赋予多个角色.
建立shiro数据库: 内含权限5表 (用户,角色,权限,用户角色,角色权限)
DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro; drop table if exists user;
drop table if exists role;
drop table if exists permission;
drop table if exists user_role;
drop table if exists role_permission; create table user (
id bigint auto_increment,
name varchar(100),
password varchar(100),
constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB; create table role (
id bigint auto_increment,
name varchar(100),
constraint pk_roles primary key(id)
) charset=utf8 ENGINE=InnoDB; create table permission (
id bigint auto_increment,
name varchar(100),
constraint pk_permissions primary key(id)
) 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;
之后基于shiro.ini文件,插入一样的用户,角色和权限数据.
INSERT INTO `permission` VALUES (1,'addProduct');
INSERT INTO `permission` VALUES (2,'deleteProduct');
INSERT INTO `permission` VALUES (3,'editProduct');
INSERT INTO `permission` VALUES (4,'updateProduct');
INSERT INTO `permission` VALUES (5,'listProduct');
INSERT INTO `permission` VALUES (6,'addOrder');
INSERT INTO `permission` VALUES (7,'deleteOrder');
INSERT INTO `permission` VALUES (8,'editOrder');
INSERT INTO `permission` VALUES (9,'updateOrder');
INSERT INTO `permission` VALUES (10,'listOrder');
INSERT INTO `role` VALUES (1,'admin');
INSERT INTO `role` VALUES (2,'productManager');
INSERT INTO `role` VALUES (3,'orderManager');
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 (1,5);
INSERT INTO `role_permission` VALUES (1,6);
INSERT INTO `role_permission` VALUES (1,7);
INSERT INTO `role_permission` VALUES (1,8);
INSERT INTO `role_permission` VALUES (1,9);
INSERT INTO `role_permission` VALUES (1,10);
INSERT INTO `role_permission` VALUES (2,1);
INSERT INTO `role_permission` VALUES (2,2);
INSERT INTO `role_permission` VALUES (2,3);
INSERT INTO `role_permission` VALUES (2,4);
INSERT INTO `role_permission` VALUES (2,5);
INSERT INTO `role_permission` VALUES (3,6);
INSERT INTO `role_permission` VALUES (3,7);
INSERT INTO `role_permission` VALUES (3,8);
INSERT INTO `role_permission` VALUES (3,9);
INSERT INTO `role_permission` VALUES (3,10);
INSERT INTO `user` VALUES (1,'zhang3','');
INSERT INTO `user` VALUES (2,'li4','abcde');
INSERT INTO `user_role` VALUES (1,1);
INSERT INTO `user_role` VALUES (2,2);
在原有的User类上加一个id字段,方便数据库操作.
设置一个Dao层:
package com.how2java; 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; public class DAO {
public DAO() {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root",
"yoursqlpassword");
} public String getPassword(String userName) {
String sql = "select password from user where name = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next())
return rs.getString("password"); } catch (SQLException e) { e.printStackTrace();
}
return null;
} public Set<String> listRoles(String userName) { Set<String> roles = new HashSet<>();
String sql = "select r.name from user u "
+ "left join user_role ur on u.id = ur.uid "
+ "left join Role r on r.id = ur.rid "
+ "where u.name = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
ps.setString(1, userName);
ResultSet rs = ps.executeQuery(); while (rs.next()) {
roles.add(rs.getString(1));
} } catch (SQLException e) { e.printStackTrace();
}
return roles;
}
public Set<String> listPermissions(String userName) {
Set<String> permissions = new HashSet<>();
String sql =
"select p.name from user u "+
"left join user_role ru on u.id = ru.uid "+
"left join role r on r.id = ru.rid "+
"left join role_permission rp on r.id = rp.rid "+
"left join permission p on p.id = rp.pid "+
"where u.name =?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) {
permissions.add(rs.getString(1));
} } catch (SQLException e) { e.printStackTrace();
}
return permissions;
}
public static void main(String[] args) {
System.out.println(new DAO().listRoles("zhang3"));
System.out.println(new DAO().listRoles("li4"));
System.out.println(new DAO().listPermissions("zhang3"));
System.out.println(new DAO().listPermissions("li4"));
}
}
Dao的主方法测试:
[admin]
[productManager]
[editOrder, addProduct, updateProduct, listProduct, listOrder, addOrder, updateOrder, deleteOrder, deleteProduct, editProduct]
[addProduct, updateProduct, listProduct, deleteProduct, editProduct]
可以看到zhang3的Roles为admin,li4的为productManager,zhang3的权限为editOrder.../ li4的权限为addProduct......
其中搜索用户角色时进行3表联查:
String sql = "select r.name from user u "
+ "left join user_role ur on u.id = ur.uid "
+ "left join Role r on r.id = ur.rid "
+ "where u.name = ?";
这里都使用了左连接,曾经有个面试官问我inner join和outer join的区别我没答上来. 这里有篇博客整理得不错. 注意是outer join
下面查到的编辑订单,添加商品,更新商品等一系列操作权限集合通过5表联查查询:
String sql =
"select p.name from user u "+
"left join user_role ru on u.id = ru.uid "+
"left join role r on r.id = ru.rid "+
"left join role_permission rp on r.id = rp.rid "+
"left join permission p on p.id = rp.pid "+
"where u.name =?";
其实这里如果牵扯到表过多,要做个小样测试,进行记录:
上图使用了战德臣老师的关系代表达式.如果多个表关系复杂时可以通过代数式清晰地展示出来.
[Realm概念] : 在Shiro中存在Realm这个概念,Realm这个单词翻译为 域, 其实是非常难以理解的.
域 是什么? 和权限有什么关系? 这个单词挺让人费解.
Realm 在 Shiro 里到底扮演什么角色呢?
当应用程序向Shiro提供了账号和密码之后,Shiro就会问Realm这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限.
所以Realm是什么? 其实就是个中介, Realm得到了Shiro给的用户和密码后, 有可能去找ini文件,就像之前的shiro.ini,也可以去找数据库,
就如同上面Dao查询信息.
Realm就是干这个用的,它才是真正进行用户认证和授权的关键地方.
再看另一个类:
DatabaseRealm 它就是用来通过数据库验证用户,和相关授权的类.
两个方法分别做验证和授权:
doGetAuthenticationInfo(), doGetAuthorizationInfo()
package com.how2java; 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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection; public class DatabaseRealm extends AuthorizingRealm { @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//能进入到这里,表示账号已经通过验证了
String userName =(String) principalCollection.getPrimaryPrincipal();
//通过DAO获取角色和权限
Set<String> permissions = new DAO().listPermissions(userName);
Set<String> roles = new DAO().listRoles(userName); //授权对象
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
//把通过DAO获取到的角色和权限放进去
s.setStringPermissions(permissions);
s.setRoles(roles);
return s;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
String password= new String( t.getPassword());
//获取数据库中的密码
String passwordInDB = new DAO().getPassword(userName); //如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
if(null==passwordInDB || !passwordInDB.equals(password))
throw new AuthenticationException(); //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());
return a;
} }
注: DatabaseRealm这个类,用户提供,但是不由用户自己调用, 而是由Shiro去调用,就像Servlet的doPost方法,是被Tomcat调用一样.
那么Shiro怎么找到这个Realm呢? 那么需要修改shiro.ini
[main]
databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm
[JdbcRealm]
Shiro提供了一个JdbcRealm, 它会默认去寻找 users, roles, permissions 三张表做类似于 Dao 中的查询.
但是这里没有使用,因为实际工作通常都会有更复杂的权限需要,以上3个表不够用. JdbcRealm又封装得太严实了.
[md5加密]
在之前,用户密码都是明文的,这样有巨大的风险,一旦泄露,就不好了.
所以,通常都会采用非对称加密,什么是非对称呢?就是不可逆的,而md5就是这样的一个算法(在之前接触到的微信平台开发与支付中同样也会用到)
如TestEncryption
package com.how2java; import org.apache.shiro.crypto.hash.Md5Hash; public class TestEncryption { public static void main(String[] args) {
String password = "123";
String encodedPassword = new Md5Hash(password).toString(); System.out.println(encodedPassword);
}
}
123字符串通过md5加密后得到字符串202CB962AC59075B964B07152D234B70
这个字符串,却无法通过计算,反过来得到源密码 123
这个加密后的字符串就存在数据库里了, 下次用户再登录,输入密码123, 同样用md5加密后, 再和这个字符串一比较, 就知道密码是否正确了.
如此这样,既能保证用户密码校验的功能, 又能保证不暴露密码.
但是md5加密又有一些缺陷:
1.如果A的密码是123, B的也是123, 那么md5的值是一样的, 那么通过比较加密后的字符串, 我就可以反推过来, 原来你的密码也是123.
2.与上述相同,虽然md5不可逆,但是可以进行穷举法暴力破解. 为了解决这个问题,引入了盐的概念, 盐是什么呢? 比如炒菜,直接使用md5,
就是对食材(源密码)进行炒菜,因为食材是一样的,所以炒出来的味道都一样,可是如果加了不同分量的盐,那么即便食材一样,炒出来的味道就
不一样了.
所以,虽然每次123md5之后的密文都是202CB962AC59075B964B07152D234B70, 但是我加上盐,即123+随机数,那么md5的值就不一样了~
这个随机数,就是盐,而这个随机数也会在数据库里保存下来,每个不同的用户,随机数也是不一样的.
再就是加密次数,加密一次是202CB962AC59075B964B07152D234B70,我也可以加密两次,就是另一个数了,而黑客即便是拿到了加密后的密码,
如果不知道到底加密了多少次,也是很难办的.
package com.how2java; import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash; public class TestEncryption { public static void main(String[] args) {
String password = "123";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2;
String algorithmName = "md5"; String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString(); System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword); }
}
运行这些代码:得到输出:
原始密码是 123 , 盐是: Qf7STdEccXZtFkAujUDwSA==, 运算次数是: 2, 运算出来的密文是:e020da0e276e9f4f30f520fb3a225935
原始密码是 123 , 盐是: OlYiWbzKmyccksMQLMOcPg==, 运算次数是: 2, 运算出来的密文是:236d91c0a2e9d2ecb77e60f0d4050fef
原始密码是 123 , 盐是: 75MZpCeMIgf0F/v+RnCsSA==, 运算次数是: 2, 运算出来的密文是:46d77dc05d2fa0e48035ceed4333079f
像上面得到了盐的加密(作者比喻比较形象),这样的字符串基本很难推算出来,从而加大了破译密码的难度.
而使用的生成随机盐的方法也是Shiro自带的工具类.new SecureRandomNumberGenerator().nextBytes().toString();
[数据库调整] : 有了以上基础,那么就可以开始在原来的教程里加入对加密的支持了. 在开始之前, 要修改一下user表,
加上盐字段: salt. 因为盐是随机数,得保留下来, 如果不知道盐是多少, 我们也就没法判断密码是否正确了.
alert table user add (salt varchar(100));
package com.how2java; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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.apache.shiro.util.Factory; public class TestShiro {
public static void main(String[] args) {
//这里要释放注释,先注册一个用户
new DAO().createUser("tom", "123"); User user = new User();
user.setName("tom");
user.setPassword("123"); if(login(user))
System.out.println("登录成功");
else
System.out.println("登录失败"); } private static boolean hasRole(User user, String role) {
Subject subject = getSubject(user);
return subject.hasRole(role);
} private static boolean isPermitted(User user, String permit) {
Subject subject = getSubject(user);
return subject.isPermitted(permit);
} private static Subject getSubject(User user) {
//加载配置文件,并获取工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//获取安全管理者实例
SecurityManager sm = factory.getInstance();
//将安全管理者放入全局对象
SecurityUtils.setSecurityManager(sm);
//全局对象通过安全管理者生成Subject对象
Subject subject = SecurityUtils.getSubject(); return subject;
} private static boolean login(User user) {
Subject subject= getSubject(user);
//如果已经登录过了,退出
if(subject.isAuthenticated())
subject.logout(); //封装用户的数据
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
try {
//将用户的数据token 最终传递到Realm中进行对比
subject.login(token);
} catch (AuthenticationException e) {
//验证错误
return false;
} return subject.isAuthenticated();
} }
通过上述代码,先注册了一个用户,然后进行登录验证
在Dao中,增加了两个方法 createUser, getUser
package com.how2java; 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.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash; public class DAO {
public DAO() {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root",
"yourpassword");
} public String createUser(String name, String password) { String sql = "insert into user values(null,?,?,?)"; String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //盐量随机
String encodedPassword= new SimpleHash("md5",password,salt,2).toString(); try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, name);
ps.setString(2, encodedPassword);
ps.setString(3, salt);
ps.execute();
} catch (SQLException e) { e.printStackTrace();
}
return null; } public String getPassword(String userName) {
String sql = "select password from user where name = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next())
return rs.getString("password"); } catch (SQLException e) { e.printStackTrace();
}
return null;
}
public User getUser(String userName) {
User user = null;
String sql = "select * from user where name = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
user.setSalt(rs.getString("salt"));
} } catch (SQLException e) { e.printStackTrace();
}
return user;
} public Set<String> listRoles(String userName) { Set<String> roles = new HashSet<>();
String sql = "select r.name from user u "
+ "left join user_role ur on u.id = ur.uid "
+ "left join Role r on r.id = ur.rid "
+ "where u.name = ?";
try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
ps.setString(1, userName);
ResultSet rs = ps.executeQuery(); while (rs.next()) {
roles.add(rs.getString(1));
} } catch (SQLException e) { e.printStackTrace();
}
return roles;
}
public Set<String> listPermissions(String userName) {
Set<String> permissions = new HashSet<>();
String sql =
"select p.name from user u "+
"left join user_role ru on u.id = ru.uid "+
"left join role r on r.id = ru.rid "+
"left join role_permission rp on r.id = rp.rid "+
"left join permission p on p.id = rp.pid "+
"where u.name =?"; try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) { ps.setString(1, userName); ResultSet rs = ps.executeQuery(); while (rs.next()) {
permissions.add(rs.getString(1));
} } catch (SQLException e) { e.printStackTrace();
}
return permissions;
}
}
继而修改DatabaseRealm,把用户通过UsernamePasswordToken传进来的密码,以及数据库里取出来的salt进行加密,加密之后再与数据库里的
密文进行比较,判断用户是否能够通过验证.
package com.how2java; 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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource; public class DatabaseRealm extends AuthorizingRealm { @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //能进入到这里,表示账号已经通过验证了
String userName =(String) principalCollection.getPrimaryPrincipal();
//通过DAO获取角色和权限
Set<String> permissions = new DAO().listPermissions(userName);
Set<String> roles = new DAO().listRoles(userName); //授权对象
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
//把通过DAO获取到的角色和权限放进去
s.setStringPermissions(permissions);
s.setRoles(roles);
return s;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
String password =new String(t.getPassword());
//获取数据库中的密码 User user = new DAO().getUser(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
String passwordEncoded = new SimpleHash("md5",password,salt,2).toString(); if(null==user || !passwordEncoded.equals(passwordInDB))
throw new AuthenticationException(); //认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,password,getName());
return a;
} }
通过刚才的增加新用户:
可以看到tom的密码加密了,它的盐在后面
另一个做法的DatabaseRealm中只是提供了密文和盐,具体操作:
修改shiro.ini :
为DatabaseRealm指定credentialsMatcher, 其中就指定了算法是md5, 次数为2, storedCredentialsHexEncoded这个表示计算之后以密文为16进制.
这样Shiro就拿着在subject.log()时传入的UsernamePasswordToken中的源密码,数据库里的密文和盐,以及配置文件里指定的算法参数,
自己去进行相关匹配了.
以下是shiro.ini
[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true databaseRealm=com.how2java.DatabaseRealm
databaseRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$databaseRealm
好了,除了能看到md5加密和随机盐,shiro.ini马上就不会用到了,所以上面的也不用太在意,可以查看下storedCredentialsHexEncoded,不过接下来接入jsp页面查看下shiro的再实际点的作用.
既然是Web项目,那还是配置下web.xml :
<web-app>
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value><!-- 默认先从/WEB-INF/shiro.ini,如果没有找classpath:shiro.ini -->
</context-param>
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini</param-value>
</context-param>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
这里有个Filter的DEMO,Filter说白了就是当调用一个url时之前要有一道关口,这个关口就是Filter.
其它的如User,DAO,DatabaseRealm都和之前的没啥区别.
这里有一个Servlet
package com.how2java; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject; @WebServlet(name = "loginServlet", urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String name = req.getParameter("name");
String password = req.getParameter("password");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
subject.login(token);
Session session=subject.getSession();
session.setAttribute("subject", subject); resp.sendRedirect("");
} catch (AuthenticationException e) {
req.setAttribute("error", "验证失败");
req.getRequestDispatcher("login.jsp").forward(req, resp);
}
}
}
LoginServlet映射路径/login的访问.
获取账号和密码,然后组成UsernamePasswordToken对象,扔给Shiro进行判断, 如果判断不报错, 即表示成功, 客户端跳转到根目录,
否则返回login.jsp,并带上错误信息
登录成功之后还会把subject放在shiro的session对象里, shiro的这个session和httpsession是串通好了的, 所以在这里放了, 它会自动放在
httpsession里,它们之间是同步的. 可以看到这里登录成功后,将用户名和密码通过Shiro的UsernamePasswordToken包装为一个token对象
之后通过login方法进行用户登录,如果没有异常则表示用户名密码正确, resp响应到了"",(并不是index.jsp,或mian.jsp不知道为什么)
下面是shiro.ini,之后到springboot应该不用配置了
[main]
#使用数据库进行验证和授权
databaseRealm=com.how2java.DatabaseRealm
securityManager.realms=$databaseRealm #当访问需要验证的页面,但是又没有验证的情况下,跳转到login.jsp
authc.loginUrl=/login.jsp
#当访问需要角色的页面,但是又不拥有这个角色的情况下,跳转到noroles.jsp
roles.unauthorizedUrl=/noRoles.jsp
#当访问需要权限的页面,但是又不拥有这个权限的情况下,跳转到noperms.jsp
perms.unauthorizedUrl=/noPerms.jsp #users,roles和perms都通过前面知识点的数据库配置了
[users] #urls用来指定哪些资源需要什么对应的授权才能使用
[urls]
#doLogout地址就会进行退出行为
/doLogout=logout
#login.jsp,noroles.jsp,noperms.jsp 可以匿名访问
/login.jsp=anon
/noroles.jsp=anon
/noperms.jsp=anon #查询所有产品,需要登录后才可以查看
/listProduct.jsp=authc
#增加商品不仅需要登录,而且要拥有 productManager 权限才可以操作
/deleteProduct.jsp=authc,roles[productManager]
#删除商品,不仅需要登录,而且要拥有 deleteProduct 权限才可以操作
/deleteOrder.jsp=authc,perms["deleteOrder"]
下面是index.jsp, 通过${subject.principal}来判断用户是否登录,if(登录了){ 显示退出选项 } else { 显示登录按钮 } (与大多数的网站登录一致)
index里提供了3个超链接,分别要登录后才可以查看,有角色,有权限才能看,便于进行测试.
注: subject是在LoginServlet里放进session的
index.jsp使用了JSTL表达式,(说真的JSTL,EL表达式远不及用ThymeLeaf前端模板来得好,JSTL,EL如果不在tomcat启动就访问不了了,而ThymeLeaf不同)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> </head>
<body> <div class="workingroom">
<div class="loginDiv"> <c:if test="${empty subject.principal}">
<a href="login.jsp">登录</a><br>
</c:if>
<c:if test="${!empty subject.principal}">
<span class="desc">你好,${subject.principal},</span>
<a href="doLogout">退出</a><br>
</c:if> <a href="listProduct.jsp">查看产品</a><span class="desc">(登录后才可以查看) </span><br>
<a href="deleteProduct.jsp">删除产品</a><span class="desc">(要有产品管理员角色, zhang3没有,li4 有) </span><br>
<a href="deleteOrder.jsp">删除订单</a><span class="desc">(要有删除订单权限, zhang3有,li4没有) </span><br>
</div> </body>
</html>
登录 login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> <div class="errorInfo">${error}</div>
<form action="login" method="post">
账号: <input type="text" name="name"> <br>
密码: <input type="password" name="password"> <br>
<br>
<input type="submit" value="登录">
<br>
<br>
<div>
<span class="desc">账号:zhang3 密码:12345 角色:admin</span><br>
<span class="desc">账号:li4 密码:abcde 角色:productManager</span><br>
</div> </form>
</div>
大体运行后是这样一个干枯枯燥的页面
在demo中,当用户zhang3登录后如果查看删除产品的链接,就会提示错误,路径转到了http://localhost:8184/shiro/noRoles.jsp
而进入删除订单,则没有问题,因为zhang3用户有该权限.
{经历了一夜的休息,继续跟shiro战斗,将博客下方加上>>笔耕不辍<<,希望今年是个丰收年}
在登录了li4这个角色后,可以看到删除产品的可以进入,而删除订单的权限不足.
也就是说Shiro提供的这些Subject,SecurityManager,Authenticator(身份认证),Authorizer(授权认证),Realm,,,sessionManager等都是为了做这个工作,
即让有权限的,有角色的才可能进入规定的地方. (其实就像现在编程这个行业,许多都是培训班出来的,半路出家,而很多公司都是卡学历的,如果你学历不够,那么就说明你没有这个Role,继而也就没有这个权限)
listProduct.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> listProduct.jsp ,能进来,就表示已经登录成功了
<br>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>
deleteOrder.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteOrder.jsp ,能进来,就表示有deleteOrder权限
<br>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>
deleteProduct.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteProduct.jsp,能进来<br>就表示拥有 productManager 角色
<br>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>
noRoles.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 角色不匹配
<br>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>
noPerms.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> 权限不足
<br>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>