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字段了

相关博客1

相关博客2

相关博客3

|

|

新建一个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相关博客

shiro相关博客

shiro论坛

github相关demo

简单实用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>

 

[shiro] - 怎样使用shiro?的更多相关文章

  1. shiro开发,shiro的环境配置(基于spring+springMVC+redis)

    特别感谢lhacker分享的文章,对我帮助很大 http://www.aiuxian.com/article/p-1913280.html 基本的知识就不在这里讲了,在实战中体会shiro的整体设计理 ...

  2. 细说shiro之一:shiro简介

    官网:https://shiro.apache.org/ 一. Shiro是什么Shiro是一个Java平台的开源权限框架,用于认证和访问授权.具体来说,满足对如下元素的支持: 用户,角色,权限(仅仅 ...

  3. 转:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法、shiro认证与shiro授权

    原文地址:JAVAWEB开发之权限管理(二)——shiro入门详解以及使用方法.shiro认证与shiro授权 以下是部分内容,具体见原文. shiro介绍 什么是shiro shiro是Apache ...

  4. Apache Shiro学习-2-Apache Shiro Web Support

     Apache Shiro Web Support  1. 配置 将 Shiro 整合到 Web 应用中的最简单方式是在 web.xml 的 Servlet ContextListener 和 Fil ...

  5. 【Shiro】Apache Shiro架构之自定义realm

    [Shiro]Apache Shiro架构之身份认证(Authentication) [Shiro]Apache Shiro架构之权限认证(Authorization) [Shiro]Apache S ...

  6. 【Shiro】Apache Shiro架构之集成web

    Shiro系列文章: [Shiro]Apache Shiro架构之身份认证(Authentication) [Shiro]Apache Shiro架构之权限认证(Authorization) [Shi ...

  7. 【Shiro】Apache Shiro架构之权限认证(Authorization)

    Shiro系列文章: [Shiro]Apache Shiro架构之身份认证(Authentication) [Shiro]Apache Shiro架构之集成web [Shiro]Apache Shir ...

  8. 【Shiro】Apache Shiro架构之身份认证(Authentication)

    Shiro系列文章: [Shiro]Apache Shiro架构之权限认证(Authorization) [Shiro]Apache Shiro架构之集成web [Shiro]Apache Shiro ...

  9. [转载] 【Shiro】Apache Shiro架构之实际运用(整合到Spring中)

    写在前面:前面陆陆续续对Shiro的使用做了一些总结,如题,这篇博文主要是总结一下如何将Shiro运用到实际项目中,也就是将Shiro整到Spring中进行开发.后来想想既然要整,就索性把Spring ...

随机推荐

  1. Spring Boot打war包

    然后修改下入口: 这样程序既可以以war也可以以jar的形式run. 右键项目properties,找到项目位置,然后: 然后放到tomcat的webapps的目录下: 然后启动tomcat:star ...

  2. XtraBackup完整备份与增量备份的原理

    MySQL数据库实现备份的操作包括完整备份和增量备份等,本文我们主要介绍一下增量备份和完整备份的原理,接下来我们就一起来了解一下这部分内容. 完整备份的原理: 对于InnoDB,XtraBackup基 ...

  3. 第k小数据

    给定两个整型数组A和B(未排序).我们将A和B中的元素两两相加可以得到数组C. 譬如A为[1,2],B为[3,4].那么由A和B中的元素两两相加得到的数组C为[4,5,5,6]. 现在给你数组A和B, ...

  4. U盘安装win10操作系统

    https://www.zhihu.com/question/39207359   1:进入微软官方网站,点击立即下载工具,下载完成mediacreationtool,双击打开,接受协议  https ...

  5. [py][mx]django邮箱注册的验证码部分-django-simple-captcha库使用

    邮箱注册-验证码 验证码使用第三方库django-simple-captcha 这个安装图形插件步骤官网有哦 - 1.Install django-simple-captcha via pip: pi ...

  6. 机器学习 python库 介绍

    开源机器学习库介绍 MLlib in Apache Spark:Spark下的分布式机器学习库.官网 scikit-learn:基于SciPy的机器学习模块.官网 LibRec:一个专注于推荐算法的j ...

  7. GET 'https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.1.2/gradle-3

    Could not GET 'https://dl.google.com/dl/android/maven2/com/android/tools/build/gradle/3.1.2/gradle-3 ...

  8. Catch all the latest Jordan Release Dates

    In case y'all missed yesterday's news, Air Jordan 13 Olive 2018 officially unveiled their 2017 Holid ...

  9. php传值,传地址,传引用的区别

    传值,   是把实参的值赋值给行参   那么对行参的修改,不会影响实参的值 传地址   是传值的一种特殊方式,只是他传递的是地址,不是普通的如int   那么传地址以后,实参和行参都指向同一个对象 传 ...

  10. JS中的对象数组

    <html> <head> <title>对象数组的字符串表示</title> <script type="text/javascrip ...