[shiro] - 怎样使用shiro?

2019/01/03 09:41
阅读数 980

 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','12345');
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>

 

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部