Shiro

Author:Exchanges

Version:9.0.0

目录

一、RBAC介绍


1.1 RBAC简介

RBAC是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

在RBAC模型里面,有3个基础组成部分,分别是:用户、角色和权限。

RBAC通过定义角色的权限,并对用户授予某个角色从而来控制用户的权限,实现了用户和权限的逻辑分离,极大地方便了权限的管理,在讲解之前,先介绍一些名词:

User(用户):每个用户都有唯一的UID识别,并被授予不同的角色

Role(角色):不同角色具有不同的权限

Permission(权限):访问权限

用户-角色映射:用户和角色之间的映射关系

角色-权限映射:角色和权限之间的映射

1.2 权限管理

只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户认证和授权两部分。

二、 用户认证

用户认证,用户去访问系统,系统要验证用户身份的合法性。最常用的用户身份验证的方法:

1、用户名密码方式、2、指纹打卡机、3、基于证书验证方法。。系统验证用户身份合法,用户方可访问系统的资源。

关键对象:

subject:主体,理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证。

principal:身份信息,通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)

credential:凭证信息,可以是密码 、证书、指纹。

总结:主体在进行身份认证时需要提供身份信息和凭证信息。

三、用户授权


用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。

关键对象

授权的过程理解为:who对what(which)进行how操作。

who:主体即subject,subject在认证通过后系统进行访问控制。

what(which):资源(Resource),subject必须具备资源的访问权限才可访问该 资源。资源比如:系统用户列表页面、商品修改菜单、商品id为001的商品信息。

资源分为资源类型和资源实例:

系统的用户信息就是资源类型,相当于java类。

系统中id为001的用户就是资源实例,相当于new的java对象。

how:权限/许可(permission) ,针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除。

四、权限模型


主体(账号、密码)

角色(角色名称)

权限(权限名称、资源id)

主体和角色关系(主体id、角色id)

角色和权限关系(角色id、权限id)

资源(资源名称、访问地址)

通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源名称、访问地址)

权限(权限名称、资源id)

合并为:权限(权限名称、资源名称、资源访问地址)

上图常被称为权限管理的通用模型,不过企业在开发中根据系统自身的特点还会对上图进行修改,但是用户、角色、权限、用户角色关系、角色权限关系是需要去理解的。

分配权限:用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可,通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中,把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)。

五、Shiro介绍


shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权,功能强大、且 简单、灵活,且不跟任何的框架或者容器绑定,可以独立运行。

spring中有spring security (原名Acegi),是一个权限框架,使用起来很方便,和spring依赖过于紧密。

subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。

securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。

authenticator:认证器,主体进行认证最终通过authenticator进行的。

authorizer:授权器,主体进行授权最终通过authorizer进行的。

sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。

SessionDao:通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。

cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。

realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。

注:在realm中存储授权和认证的逻辑。

cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

比如:md5信息摘要算法。

六、认证流程


6.1 创建Maven工程并导入依赖

<!-- 导入shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.9.0</version>
</dependency>

<!-- 后期整合springboot工程时会产生接口冲突问题,会踩坑所以不用 -->
<!--        <dependency>-->
<!--            <groupId>org.apache.shiro</groupId>-->
<!--            <artifactId>shiro-spring-boot-web-starter</artifactId>-->
<!--            <version>1.9.0</version>-->
<!--        </dependency>-->

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>

6.2 在resources目录下创建shiro.ini文件,IEDA需要安装*.ini插件并重启

#设置用户
[users]
#用户名和密码
jack=123
tom=456

6.3 创建ShiroTest进行认证测试

package com.qf.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class ShiroTest {

    //认证
    @Test
    public void testAuthentication(){
        //构建环境,加载配置文件
        IniSecurityManagerFactory iniSecurityManagerFactory =
                new IniSecurityManagerFactory("classpath:shiro.ini");

        //安全管理器
        SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
        //设置环境
        SecurityUtils.setSecurityManager(securityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //获取用户名和密码(Token令牌)
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        //登录
        subject.login(token);
        //认证
        boolean authenticated = subject.isAuthenticated();
        //判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }
        //退出
        subject.logout();
        //再次认证
        authenticated = subject.isAuthenticated();
        //再次判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }

    }

}

6.4 执行流程

1、通过ini配置文件创建securityManager

2、调用subject.login方法主体提交认证,提交的token

3、securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。

4、ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息

5、IniRealm根据输入的token(UsernamePasswordToken)从 shiro.ini查询用户信息,根据账号查询用户信息	(账号和密码)

	如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)

	如果查询不到,就给ModularRealmAuthenticator返回null

6、ModularRealmAuthenticator接收IniRealm返回Authentication认证信息

	如果返回的认证信息是null,ModularRealmAuthenticator抛出异(org.apache.shiro.authc.UnknownAccountException)

	如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在)和 token中的密码 进行对	   
	比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)

6.5 总结

ModularRealmAuthenticator作用是进行认证,需要调用realm来查询用户信息(在数据库中存在用户信息)并且进行密码对比(认证过程)

realm:需要根据token中的身份信息去查询数据库(入门程序使用ini配置文件),如果查到用户返回认证信息,如果查询不到返回null

七、使用realm认证


7.1 创建自定义ShiroRealm

package com.qf.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class ShiroRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("ShiroRealm认证方法执行");

        //声明用户名和密码
        String username = null;
        String password = null;

        //获取前端传过来的用户名和密码
        //身份信息(用户名)
        Object principal = token.getPrincipal();
        if(principal !=null){
            username = principal.toString();
        }

        //凭证信息(密码)
        Object credentials = token.getCredentials();
        if(credentials !=null){
            password = new String( (char[])credentials );
        }

        System.out.println(username + " -- " + password);

        //模拟数据库查询的用户名的密码
        String db_username = "jack";
        String db_password= "123";

        //判断
        if(db_username.equals(username) && db_password.equals(password)){
            //认证成功,返回一个对象
            SimpleAuthenticationInfo simpleAuthenticationInfo =
                    new SimpleAuthenticationInfo(username,password,"ShiroRealm");

            return simpleAuthenticationInfo;
        }

        throw new RuntimeException("认证失败");
    }
}

7.2 在resource目录下创建shiro-realm.ini

#自定义realm
[main]
ShiroRealm = com.qf.realm.ShiroRealm
#引用声明的realm(注意:必须分开写,不能写成:securityManager.realms = com.qf.realm.ShiroRealm)
securityManager.realms = $ShiroRealm

7.3 修改ShiroTest中的方法,配置加载shiro-realm.ini文件即可

package com.qf.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class ShiroTest {

    //认证
    @Test
    public void testAuthentication(){
        //构建环境,加载配置文件
//        IniSecurityManagerFactory iniSecurityManagerFactory =
//                new IniSecurityManagerFactory("classpath:shiro.ini");

        IniSecurityManagerFactory iniSecurityManagerFactory =
                new IniSecurityManagerFactory("classpath:shiro-realm.ini");

        //安全管理器
        SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
        //设置环境
        SecurityUtils.setSecurityManager(securityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //获取用户名和密码(Token令牌)
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        //登录
        subject.login(token);
        //认证
        boolean authenticated = subject.isAuthenticated();
        //判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }
        //退出
        subject.logout();
        //再次认证
        authenticated = subject.isAuthenticated();
        //再次判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }

    }

}

八、MD5加密


通常需要对密码 进行散列,常用的有MD5、SHA,

对MD5密码,如果知道散列后的值可以通过穷举算法,得到md5密码对应的明文(指加密前的密码)。

建议对MD5进行散列时加salt(盐),进行加密相当于对原始密码+盐进行散列。

正常使用时散列方法:

在程序中对原始密码+盐进行散列,将散列值存储到数据库中,并且还要将盐也要存储在数据库中。

如果进行密码对比时,使用相同方法,将原始密码+盐进行散列,进行比对。

8.1.创建MD5Test测试类,进行测试

package com.qf.md5;

import org.apache.shiro.crypto.hash.Md5Hash;

public class MD5Test {
    public static void main(String[] args) {

        //明文:原始密码
        String password = "123";
        //加盐
        String salt = "qwer";
        //散列次数
        int hashIterations = 3;
        
        Md5Hash md5Hash = new Md5Hash(password,salt,hashIterations);
        System.out.println(md5Hash);
    }
}

8.2结合realm进行测试,创建MD5Realm

package com.qf.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

//关联数据库
public class MD5Realm extends AuthorizingRealm {

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("MD5Realm认证方法执行");

        //声明用户名和密码
        String username = null;
        String password = null;

        //获取前端传过来的用户名和密码
        //身份信息(用户名)
        Object principal = token.getPrincipal();
        if(principal !=null){
            username = principal.toString();
        }

        //凭证信息(密码)
        Object credentials = token.getCredentials();
        if(credentials !=null){
            password = new String((char[])credentials );
        }

        System.out.println(username + " -- " + password);

        //模拟数据库查询的用户名的密码
        String db_username = "jack";
        String db_password= "202cb962ac59075b964b07152d234b70";//不加盐
        //String salt = "qwer";

        //对前端密码进行加密(明文密码可以加盐加散列次数)
        Md5Hash md5HashPassword = new Md5Hash(password);

        //判断
        if(db_username.equals(username) && db_password.equals(md5HashPassword.toString())){
            //认证成功,返回一个对象
            SimpleAuthenticationInfo simpleAuthenticationInfo =
                    new SimpleAuthenticationInfo(username,password,"ShiroRealm");

            return simpleAuthenticationInfo;
        }

        throw new RuntimeException("认证失败");
    }


    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

}

8.3创建shiro-realm-md5.ini

#自定义realm
[main]
#配置对应的realm(注意:不能连在一起写:securityManager.realms = com.qf.realm.ShiroRealm)
MD5Realm = com.qf.realm.MD5Realm
securityManager.realms = $MD5Realm

8.4修改ShiroTest中的方法,配置加载shiro-realm-md5.ini文件即可

package com.qf.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class ShiroTest {

    //认证
    @Test
    public void testAuthentication(){
        //构建环境,加载配置文件
//        IniSecurityManagerFactory iniSecurityManagerFactory =
//                new IniSecurityManagerFactory("classpath:shiro.ini");

//        IniSecurityManagerFactory iniSecurityManagerFactory =
//                new IniSecurityManagerFactory("classpath:shiro-realm.ini");

        IniSecurityManagerFactory iniSecurityManagerFactory =
                new IniSecurityManagerFactory("classpath:shiro-realm-md5.ini");

        //安全管理器
        SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
        //设置环境
        SecurityUtils.setSecurityManager(securityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //获取用户名和密码(Token令牌)
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        //登录
        subject.login(token);
        //认证
        boolean authenticated = subject.isAuthenticated();
        //判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }
        //退出
        subject.logout();
        //再次认证
        authenticated = subject.isAuthenticated();
        //再次判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }

    }

}

九、授权流程


9.1创建shiro-permission.ini(shiro-permission.ini里边的内容相当于在数据库)

#对用户的配置
[users]
#对用户的用户名和密码的配置
jack=123,role1,role2
tom=456,role1,role3

#对角色的配置
[roles]
role1 = user:add
role2 = user:update
role3 = user:delete
role4 = user:query

9.2在ShiroTest中测试授权

package com.qf.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

import java.util.Arrays;

public class ShiroTest {

    //认证
    @Test
    public void testAuthentication(){
        //构建环境,加载配置文件
//        IniSecurityManagerFactory iniSecurityManagerFactory =
//                new IniSecurityManagerFactory("classpath:shiro.ini");

//        IniSecurityManagerFactory iniSecurityManagerFactory =
//                new IniSecurityManagerFactory("classpath:shiro-realm.ini");

        IniSecurityManagerFactory iniSecurityManagerFactory =
                new IniSecurityManagerFactory("classpath:shiro-realm-md5.ini");

        //安全管理器
        SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
        //设置环境
        SecurityUtils.setSecurityManager(securityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //获取用户名和密码(Token令牌)
        UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
        //登录
        subject.login(token);
        //认证
        boolean authenticated = subject.isAuthenticated();
        //判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }
        //退出
        subject.logout();
        //再次认证
        authenticated = subject.isAuthenticated();
        //再次判断
        if(authenticated){
            System.out.println("认证成功");
        }else{
            System.out.println("认证失败");
        }

    }


    //授权
    @Test
    public void testAuthorization() {
        //构建环境
        IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");

        //获取securityManager对象
        SecurityManager securityManager = securityManagerFactory.getInstance();
        //设置securityManager
        SecurityUtils.setSecurityManager(securityManager);
        //获取主体
        Subject subject = SecurityUtils.getSubject();
        //创建Token(令牌)
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123");
        //认证
        subject.login(usernamePasswordToken);
        //判断是否认证通过
        boolean authenticated = subject.isAuthenticated();
        //判断
        if (authenticated) {
            System.out.println("认证成功");

            //------------角色-------------
            //判断是否拥有某个角色
            boolean role1 = subject.hasRole("role1");
            System.out.println("某个角色:" + role1);

            //判断是否拥有多个角色
            boolean hasAllRoles = subject.hasAllRoles(Arrays.asList("role1", "role2"));
            System.out.println("多个角色:" + hasAllRoles);

            //检查是否拥有对应的角色(没有会报异常)
            //subject.checkRole("role1");//单个角色
            //subject.checkRoles(Arrays.asList("role1", "role2"));//多个角色

            //------------权限-------------
            //判断单个权限
            //boolean permitted = subject.isPermitted("user:add");
            //System.out.println("单个权限:" + permitted);

            //boolean permittedAll = subject.isPermittedAll("user:add", "user:update");
            //System.out.println("多个权限:" + permittedAll);

            //检查是否拥有对应的权限(没有会报异常)
            //subject.checkPermission("user:add");
            //subject.checkPermissions("user:add", "user:query");
        }
    }

}

十、使用realm授权


上边的程序通过shiro-permission.ini对权限信息进行静态配置,实际开发中从数据库中获取权限数据。就需要自定义realm,由realm从数据库查询权限数据,realm会根据用户身份查询权限数据,将权限数据返回给authorizer(授权器)。

10.1在ShiroRealm中编写授权代码

package com.qf.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.Set;

public class ShiroRealm extends AuthorizingRealm {
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        System.out.println("ShiroRealm授权方法执行");

        //获取用户信息
        Object primaryPrincipal = principals.getPrimaryPrincipal();
        String usercode = primaryPrincipal.toString();

        System.out.println(usercode);

        //根据usercode查询数据库,获取对应的权限
        //拿到权限....

        Set<String> roles = new HashSet<>();
        roles.add("role1");
        roles.add("role2");


//        Set<String> permissions = new HashSet<>();
//        permissions.add("user:add");
//        permissions.add("user:update");
//        permissions.add("user:query");

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加角色
        simpleAuthorizationInfo.addRoles(roles);
        //添加权限
        //simpleAuthorizationInfo.addStringPermissions(permissions);

        return simpleAuthorizationInfo;
    }
    
    
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("ShiroRealm认证方法执行");

        //声明用户名和密码
        String username = null;
        String password = null;

        //获取前端传过来的用户名和密码
        //身份信息(用户名)
        Object principal = token.getPrincipal();
        if(principal !=null){
            username = principal.toString();
        }

        //凭证信息(密码)
        Object credentials = token.getCredentials();
        if(credentials !=null){
            password = new String( (char[])credentials );
        }

        System.out.println(username + " -- " + password);

        //模拟数据库查询的用户名的密码
        String db_username = "jack";
        String db_password= "123";

        //判断
        if(db_username.equals(username) && db_password.equals(password)){
            //认证成功,返回一个对象
            SimpleAuthenticationInfo simpleAuthenticationInfo =
                    new SimpleAuthenticationInfo(username,password,"ShiroRealm");

            return simpleAuthenticationInfo;
        }

        throw new RuntimeException("认证失败");
    }
}

10.2配置shiro-realm.ini文件,之前已经配置过,该步骤可以省略

10.3修改ShiroTest中的授权测试方法,只需要修改加载配置文件即可

//授权
@Test
public void testAuthorization() {
    //构建环境
    //IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");
    IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro-realm.ini");

    //获取securityManager对象
    SecurityManager securityManager = securityManagerFactory.getInstance();
    //设置securityManager
    SecurityUtils.setSecurityManager(securityManager);
    //获取主体
    Subject subject = SecurityUtils.getSubject();
    //创建Token(令牌)
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123");
    //认证
    subject.login(usernamePasswordToken);
    //判断是否认证通过
    boolean authenticated = subject.isAuthenticated();
    //判断
    if (authenticated) {
        System.out.println("认证成功");

        //------------角色-------------
        //判断是否拥有某个角色
        boolean role1 = subject.hasRole("role1");
        System.out.println("某个角色:" + role1);

        //判断是否拥有多个角色
        boolean hasAllRoles = subject.hasAllRoles(Arrays.asList("role1", "role2"));
        System.out.println("多个角色:" + hasAllRoles);

        //检查是否拥有对应的角色(没有会报异常)
        //subject.checkRole("role1");//单个角色
        //subject.checkRoles(Arrays.asList("role1", "role2"));//多个角色

        //------------权限-------------
        //判断单个权限
        //boolean permitted = subject.isPermitted("user:add");
        //System.out.println("单个权限:" + permitted);

        //boolean permittedAll = subject.isPermittedAll("user:add", "user:update");
        //System.out.println("多个权限:" + permittedAll);

        //检查是否拥有对应的权限(没有会报异常)
        //subject.checkPermission("user:add");
        //subject.checkPermissions("user:add", "user:query");
    }
}

10.4 授权流程

1、对subject进行授权,调用方法isPermitted("permission串")
2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3、ModularRealmAuthorizer执行realm(自定义的Realm)从数据库查询权限数据,调用realm的doGetAuthorizationInfo授权方法
4、realm从数据库查询权限数据,返回ModularRealmAuthorizer
5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6、如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。

十一、SpringBoot整合Shiro


11.1导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.9.0</version>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.9</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

11.2配置application.yml

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

  thymeleaf:
    servlet:
      content-type: text/html
    prefix: classpath:/templates
    encoding: UTF-8


server:
  servlet:
    session:
      #去除访问路径后携带sessionid
      tracking-modes: cookie

11.3编写实体类

package com.qf.pojo;

import lombok.Data;

@Data
public class User {

    private String usercode;//用户账号,唯一性
    private String password;
}

11.4编写Controller

package com.qf.controller;

import com.qf.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("user")
public class UserController {

    @RequestMapping("login")
    public String login(User user){
        System.out.println(user);

        //设置用户名和密码
        UsernamePasswordToken token =
                new UsernamePasswordToken(user.getUsercode(), user.getPassword());

        //主体
        Subject subject = SecurityUtils.getSubject();
        //登录
        try{
            subject.login(token);
        }catch (Exception e){
            System.out.println("异常信息:"+e.getMessage());
            return "redirect:/user/toLogin";
        }

        return "/index.html";
    }

    @RequestMapping("index")
    public String index(){
        return "/index.html";
    }

    @RequestMapping("toLogin")
    public String toLogin(){
        return "/login.html";
    }

    @RequestMapping("refuse")
    public String refuse(){
        return "/refuse.html";
    }

}

11.5编写Service

package com.qf.service;

import com.qf.pojo.User;

public interface UserService {

    /**
     * 登录
     * @param usercode
     * @param password
     * @return
     */
    User findUser(String usercode, String password);
}

11.6编写ServiceImpl

package com.qf.service.impl;

import com.qf.mapper.UserMapper;
import com.qf.pojo.User;
import com.qf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;


    @Override
    public User findUser(String usercode, String password) {
        return userMapper.findUser(usercode,password);
    }
}

11.7编写Mapper

package com.qf.mapper;

import com.qf.pojo.User;
import org.springframework.stereotype.Repository;


@Repository
public interface UserMapper {
    /**
     * 查询用户
     * @param usercode
     * @param password
     * @return
     */
    User findUser(String usercode, String password);
}

11.8编写Mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qf.mapper.UserMapper">

    <select id="findUser" resultType="com.qf.pojo.User">
        select id,usercode,username,password,salt from sys_user
        <where>
            usercode = #{usercode} and password=#{password}
        </where>
    </select>

</mapper>

11.9编写LoginRealm

package com.qf.realm;

import com.qf.pojo.User;
import com.qf.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class LoginRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //获取用户账号和密码
        String usercode = token.getPrincipal().toString();
        Object credentials = token.getCredentials();
        String password = new String((char[])credentials);


        //对前端传过来的密码加密
        String md5HashPassword = new Md5Hash(password).toString();
        System.out.println(md5HashPassword);

        //从数据库查询当前登录的用户
        User db_user = userService.findUser(usercode,md5HashPassword);
        System.out.println(db_user);

        //判断
        if(db_user!=null) {
            //和数据库查询的密码比对
            if (db_user.getPassword().equals(md5HashPassword)) {
                //登录成功返回的对象
                SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(usercode, password, "LoginRealm");
                return simpleAuthenticationInfo;
            }
        }

        throw new RuntimeException("认证异常");
    }
}

11.10编写ShiroConfig

package com.qf.config;

import com.qf.realm.LoginRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {

    //配置SecurityManager环境
    @Bean
    public WebSecurityManager getSecurityManager(LoginRealm loginRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //配置realm
        securityManager.setRealm(loginRealm);
        return securityManager;
    }

    //Shiro过滤器
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(WebSecurityManager securityManager){
        //过滤器对象
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置信息(顺序很重要)
        //认证成功后跳转到指定页面(一般不配置,不配置跳转到当前正在访问的页面)
        //shiroFilterFactoryBean.setSuccessUrl("/user/index");
        //未认证访问的页面
        shiroFilterFactoryBean.setLoginUrl("/user/toLogin");
        //未授权访问的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/refuse");

        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        //静态资源设置匿名访问
        map.put("/css/**","anon");
        map.put("/js/**","anon");
        map.put("/font/**","anon");

        //配置匿名访问的资源
        map.put("/user/login","anon");
        map.put("/user/toLogin","anon");

        //配置认证后访问的资源
        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        return shiroFilterFactoryBean;
    }

}
过滤器简称 对应的java类
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
anon:admins/**=anon 没有参数,表示可以匿名使用。
authc:/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数 

roles:/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。

perms:/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。

port:/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。

authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl:/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user:/admins/user/**=user没有参数表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查
anon,authcBasic,authc,user是认证过滤器

perms,roles,ssl,rest,port是授权过滤器

11.11编写相关页面

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<center>

    <form action="/user/login">

        用户账号:<input type="text" name="usercode"><br>
        密码:<input type="password" name="password"><br>
   
        <input type="submit" value="登录">

    </form>

</center>

</body>
</html>

refuse.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1><font color="red">无权访问!</font></h1>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<center>

    <h1>主页面</h1>
    <a href="/user/logout">退出</a>
    <a href="/product/add">商品添加</a>
    <a href="/product/update">商品修改</a>
    <a href="/product/findAll">商品查询</a>

</center>

</body>
</html>

productAdd.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
商品添加页面
</body>
</html>

productFindAll.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
商品查询页面
</body>
</html>

productUpdate.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
商品修改页面
</body>
</html>

11.12 创建ProductController,启动服务器测试即可!

package com.qf.controller;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("product")
public class ProductController {

    @RequestMapping("add")
    public String add(){
        return "/productAdd.html";
    }

    @RequestMapping("update")
    public String update(){
        return "/productUpdate.html";
    }

    @RequestMapping("findAll")
    public String findAll(){
        return "/productFindAll.html";
    }
}

11.13在ShiroConfig中配置授权

//配置授权才能访问
map.put("/product/add","perms[product:create]");
map.put("/product/update","perms[product:update]");

再次启动测试,此时没有对应的权限则会跳转到无权访问页面!

11.14配置没有权限则不显示该连接

11.14.1导入依赖
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
11.14.2在ShiroConfig添加代码
//配置无权限不显示该连接
@Bean
public ShiroDialect getShiroDialect(){
    return new ShiroDialect();
}
11.14.3修改index.html
<!DOCTYPE html>
<html lang="en" xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<center>
    <h1>主页面</h1>
    <a href="/product/add">商品添加</a>
    <a href="/product/update" shiro:hasPermission="product:update">商品修改</a>
    <a href="/product/findAll">商品查询</a>
	<a href="/user/logout">退出</a>
</center>

</body>
</html>

再次启动测试,发现对应的连接则不显示

11.15查询对应权限,在UserMapper.xml中添加代码

<select id="findPermissionByUserCode" resultType="com.qf.dto.UserDto">
    SELECT su.usercode,sur.sys_role_id,srp.sys_permission_id,sp.percode
    FROM sys_user su
    INNER JOIN sys_user_role sur
    ON su.id = sur.sys_user_id
    INNER JOIN sys_role_permission srp
    ON sur.sys_role_id = srp.sys_role_id
    INNER JOIN sys_permission sp
    ON srp.sys_permission_id = sp.id
    WHERE su.usercode = #{usercode}
</select>

11.16创建UserDto

package com.qf.dto;

import lombok.Data;

@Data
public class UserDto {
    private String usercode;
    private Integer sys_role_id;
    private Integer sys_permission_id;
    private String percode;
}

11.17在LoginRealm中编写授权代码

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //用户账号
    String usercode = principals.getPrimaryPrincipal().toString();
    System.out.println(usercode);
    //从数据库查出来的权限
    List<UserDto> permissionByUserCode =
        userService.findPermissionByUserCode(usercode);

    Set<String> permissions = new HashSet<>();
    //遍历集合并赋值
    for(UserDto userDto: permissionByUserCode){
        permissions.add(userDto.getPercode());
    }

    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.setStringPermissions(permissions);

    return simpleAuthorizationInfo;
}

11.18在UserService中添加方法

	/**
     * 根据用户账号查询对应的权限
     * @param usercode
     * @return
     */
public List<UserDto> findPermissionByUserCode(String usercode);

11.19在UserServiceImpl中实现对应方法

@Override
public List<UserDto> findPermissionByUserCode(String usercode) {
    return userMapper.findPermissionByUserCode(usercode);
}

11.20在UserMapper中添加方法

	/**
     * 根据用户账号查询对应的权限
     * @param usercode
     * @return
     */
public List<UserDto> findPermissionByUserCode(String usercode);

然后再次测试

11.21退出操作,修改index.html中a标签退出路径

<a href="/user/logout">退出</a>

11.22在ShiroConfig中配置退出,路径要和a标签中的一致

//配置退出
linkedHashMap.put("/user/logout","logout");

11.23配置记住我rememberMe,在login.html页面中添加多选框

<input type="checkbox" name="rememberMe">记住我<br> <br>

11.24修改UserController中的login方法配置rememberMe参数

//记住我
@RequestMapping("login")
public String login(User user,boolean rememberMe){
    //获取token
    UsernamePasswordToken token = new UsernamePasswordToken(user.getUsercode(), user.getPassword(),rememberMe);
    //获取主体信息
    Subject subject = SecurityUtils.getSubject();
    try{
        subject.login(token);
    }catch (Exception e){
        System.out.println("异常信息:" + e.getMessage());
        //跳转到登录页面
        return "redirect:/user/toLogin";
    }
    //跳转到主页面
    return "/index.html";
}

11.25在ShiroConfig类中添加相关方法以及代码

package com.qf.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.qf.realm.LoginRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {

    @Bean
    public CookieRememberMeManager getCookieRememberMeManager(){
        //自定义Cookie生存时间(免密访问时间)
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setMaxAge(3600*24*30);//cookie有效期,单位:秒

        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(simpleCookie);//设置cookie,如果不设置,默认一年
        return cookieRememberMeManager;
    }

    @Bean
    public WebSecurityManager getWebSecurityManager(LoginRealm loginRealm,CookieRememberMeManager cookieRememberMeManager){
        //获取构建环境对象SecurityManager
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(loginRealm);
        //设置记住我
        defaultWebSecurityManager.setRememberMeManager(cookieRememberMeManager);

        return defaultWebSecurityManager;
    }

    //配置无权限不显示连接
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(WebSecurityManager securityManager){
        //shiro过滤器
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置环境
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设置未登录(未认证)访问的路径
        shiroFilterFactoryBean.setLoginUrl("/user/toLogin");
        //设置未授权访问的路径
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/refuse");
        //认证成功后访问路径,可以设置跳转到主页面,但是一般不配置,如果不配置则跳转到正在访问的页面
        //shiroFilterFactoryBean.setSuccessUrl("/user/toIndex");

        //配置访问权限(顺序很重要)
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        //先配置可以匿名访问的资源
        linkedHashMap.put("/js/**","anon");
        linkedHashMap.put("/css/**","anon");
        linkedHashMap.put("/images/**","anon");

        //匿名访问的路径
        linkedHashMap.put("/user/toLogin","anon");
        linkedHashMap.put("/user/login","anon");

        //配置授权
        linkedHashMap.put("/product/query","perms[product:query]");
        linkedHashMap.put("/product/add","perms[product:add]");

        //配置退出
        linkedHashMap.put("/user/logout","logout");

        //配置记住我,二次访问该路径无需登录
        linkedHashMap.put("/user/toIndex","user");

        //配置需要认证后才能访问的路径
        linkedHashMap.put("/**","authc");

        //设置linkedHashMap
        shiroFilterFactoryBean.setFilterChainDefinitionMap(linkedHashMap);

        return shiroFilterFactoryBean;
    }
}

登录页面勾选 记住我 进行测试

posted @ 2022-07-10 20:07  qtyanan  阅读(37)  评论(0编辑  收藏  举报