Shiro学习笔记


shiro官方文档:传送门

1. 权限的管理

目前主流的解决权限管理的方案:①Apache基金会开源的Shiro框架;②Spring技术栈中的SpringSecurity

1.1 什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户身份认证授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

1.2 什么是身份认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

1.3 什么是授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

2. 什么是shiro

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序。

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

3.shiro的核心架构

3.1 简介

3.1.1 Subject

Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。

Subjectshiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权。

3.1.2 Security Manager

Security Manager(安全管理器)是架构图中最重要的部分,在使用shiro进行权限管理时,首先就要拿到Security ManagerSecurity Manager包裹了整个shiro中的所有功能。

SecurityManager对全部的subject进行安全管理,它是shiro的核心。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。

下面是对Security Manager中各个模块的介绍:

Authenticator:用于做认证Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。

Authorizer:用于实现授权。用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。

Session Manager:在web环境下。因为shiro要帮我们解决用户认证和用户授权。用户在认证通过之后,原来在没有用权限管理时,需要把用户标志存储在对应的服务器上(以会话的形式存储用户(典型的就是session对象)),用来对用户的会话进行追踪。如果用shiro进行用户认证,就需要把会话交给Session Manager进行管理。所以Session Manager是管理整个shiroweb环境下的核心会话的。注意:在写本地案例的时候是不具有web功能的。

SessionDAO:对会话数据进行crud的一组操作接口。用于操作Session Manager中的数据,实现数据的持久化,以及内存的存储。

Cache Manager:用于缓存用户认证和授权的数据。e.g.:用户认证通过之后,需要对用户进行授权。用户的每一次操作,都需要拿当前用户的标志去对应数据库中查看是否有操作权限。如果用户每操作一次,就调数据库验证一次权限,就无疑增加了磁盘IO的次数,数据库的压力也会变大。在数据不发生变化的情况下,用户授权仅在第一次会调用数据库,拿到数据之后统一放到Cache Manager中,以后再对用户进行授权,直接在Cache Manager中获取数据,减少了数据库磁盘IO的次数。

Pluggable Realms:具体做认证和授权的数据的调配。获取认证和授权的相应数据,并完成相应的认证和授权操作。Pluggable Realms相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

3.1.3 Cryptography

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

4. shiro中的认证

4.1 认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。

4.2 shiro中认证的关键对象

  • Subject:主体

    访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;

  • Principal:身份信息

    是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。

  • credential:凭证信息

    是只有主体自己知道的安全信息,如密码、证书等。

4.3 认证流程

4.4 认证的开发

4.4.1 创建项目并引入依赖

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>

4.4.2 shiro配置文件

注意:shiro的配置文件是以.ini结尾的,相当于.txt.ini是专门用于做配置的文件,其中开以写比较复杂的数据格式。

注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手。

.ini配置文件是用来学习shiro时书写系统中相关的权限数据。

[users]
prannt=123
mano=456
lucy=789

4.4.3 开发认证代码

public class TestAuthenticator {
    public static void main(String[] args) {
        // 1.创建安全管理器对象
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.给安全管理器设置Realms
        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        // 3.SecurityUtils  全局安全工具类,该工具类提供了认证、退出等方法
        // 给全局安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.关键对象:subject:主体
        Subject subject = SecurityUtils.getSubject();
        // 5.创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("prannt","123");
        try {
            System.out.println("认证状态:" + subject.isAuthenticated());
            subject.login(token);    // 用户认证
            System.out.println("认证状态:" + subject.isAuthenticated());
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("认证失败,用户名不存在!");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码不存在!");
        }
    }
}

4.4.4 常见的异常类型

  • DisabledAccountException(帐号被禁用)
  • LockedAccountException(帐号被锁定)
  • ExcessiveAttemptsException(登录失败次数过多)
  • ExpiredCredentialsException(凭证过期)等

4.5 认证流程源码

首先在subject.login(token);这一行打个断点,使用debug方式启动。

查看流程:

subject.login(token);this.clearRunAsIdentitiesInternal();Subject subject = this.securityManager.login(this, token);info = this.authenticate(token);return this.authenticator.authenticate(token);if (token == null)info = this.doAuthenticate(token);this.assertRealmsConfigured();Collection<Realm> realms = this.getRealms();return realms.size() == 1 ?...AuthenticationInfo info = realm.getAuthenticationInfo(token);AuthenticationInfo info = this.getCachedAuthenticationInfo(token);if (info == null)info = this.doGetAuthenticationInfo(token);UsernamePasswordToken upToken = (UsernamePasswordToken)token;SimpleAccount account = this.getUser(upToken.getUsername());this.USERS_LOCK.readLock().lock();var2 = (SimpleAccount)this.users.get(username);SimpleAccount account = this.getUser(upToken.getUsername());if (account != null)if (account.isLocked())if (account.isCredentialsExpired())

总结:认证最终执行用户名比较,是在SimpleAccountRealm类中的doGetAuthenticationInfo()方法,在这个方法中完成了用户名的校验。

return account;info = this.doGetAuthenticationInfo(token);CredentialsMatcher cm = this.getCredentialsMatcher();

总结:最终的密码校验是在AuthenticatingRealm中的assertCredentialsMatch()方法

4.6 自定义Realm

通过分析源码可得:

  1. 最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
  2. 最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法中

总结:

AuthenticatingRealm 认证realm doGetAuthenticationInfo

AuthorizingRealm 授权realm doGetAuthorizationInfo

自定义Realm的作用:放弃使用.ini文件,使用数据库查询

上边的程序使用的是Shiro自带的IniRealmIniRealm.ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm

源码:

public class SimpleAccountRealm extends AuthorizingRealm {
		// ...省略
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {
            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }

        return account;
    }

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
}
package com.prannt.realm;

/**
 * @Description 自定义realm实现,将认证/授权数据的来源转换为数据库的实现
 * @Date 2022/1/21 14:39
 * @Created Prannt
 */
public class CustomerRealm extends AuthorizingRealm {
    // 做授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 做认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 在token中获取用户名
        String principal = (String) token.getPrincipal();   // return this.getUsername();
        System.out.println(principal);
        // 根据身份信息使用mybatis查询相关数据库
        if ("prannt".equals(principal)){
            // 参数1:返回数据库中正确的用户名;参数2:返回数据库中的正确密码;参数3:提供当前realm的名字  this.getName())
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,"123",this.getName());
            return info;
        }
        return null;
    }
}
// 使用自定义realm
public class TestCustomerRealmAuthenticator {
    public static void main(String[] args) {
        // 1.创建安全管理器对象:SecurityManager
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.设置自定义realm
        securityManager.setRealm(new CustomerRealm());
        // 3.将安全工具类设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);
        // 4.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 5.创建token(用户在浏览器上输入的用户名、密码)
        UsernamePasswordToken token = new UsernamePasswordToken("prannt", "123");
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败,用户名错误!");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误!");
        }
    }
}
// 结果:
prannt

4.7 MD5+Salt+Hash

4.7.1 MD5算法

作用:一般用来加密或者签名(检验和,校验两个文件内容是否一致)

特点:

  • MD5算法不可逆,即只能根据明文算出密文,不能根据密文反推出明文
  • 如果内容相同,无论执行多少次MD5,生成的结果始终是相同的

面试题:如何比较aa.txtbb.txt的内容是否一致?可以使用MD5,MD5可以对文件的内容进行校验,检验之后如果两个文件生成的值相等,那么这两个文件的内容一定相同。

“123” → fe1dsadsad…

有些破解MD5的网站,实际上是使用了穷举的做法。把一些常用的字符串转换为MD5,有人搜索的时候,直接从数据库中查询该数据。

生成结果:始终是一个16进制32位长度字符串。

4.7.2 随机盐

在原来明文的基础上随机拼接一个字符串,再进行MD5加密。

4.7.3 MD5的基本使用

public class TestShiroMD5 {
    // MD5
    @Test
    public void md5Test(){
        Md5Hash md5Hash = new Md5Hash("123");
        String s = md5Hash.toHex(); // 转为16进制
        System.out.println(s);      // 202cb962ac59075b964b07152d234b70
    }

    // MD5 + Salt
    @Test
    public void md5SaltTest(){
        Md5Hash md5Hash = new Md5Hash("123","X0*7ps");
        System.out.println(md5Hash.toHex());    // 8a83592a02263bfe6752b2b5b03a4799
    }

    // MD5 + Salt + Hash散列(参数代表要散列多少次,一般是1024或2048)
    @Test
    public void md5SaltHashTest(){
        Md5Hash md5Hash = new Md5Hash("123","X0*7ps",1024);
        System.out.println(md5Hash.toHex());    // e4f9bf3e0c58f045e62c23c533fcf633
    }
}

4.7.4 MD5 + Salt

package com.prannt.realm;

// 使用自定义realm加入MD5 + Salt + Hash散列
public class CustomerMD5Realm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取用户名
        String principal = (String) token.getPrincipal();

        String username="prannt";
        String password="e4f9bf3e0c58f045e62c23c533fcf633"; // 123加密后
        // 根据用户名查询数据库(模拟)
        if (username.equals(principal)){
            // 参数1:数据库用户名
            // 参数2:数据库md5+salt之后的密码
            // 参数3:注册时的随机盐
            // 参数4:realm的名字
            return new SimpleAuthenticationInfo(principal,
                    password,
                    ByteSource.Util.bytes("X0*7ps"),
                    this.getName());
        }
        return null;
    }
}
package com.prannt;

public class TestCustomerMD5RealmAuthenticator {
    public static void main(String[] args) {
        // 1.创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.注入realm
        CustomerMD5Realm realm = new CustomerMD5Realm();
        // 3.设置realm使用Hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        // 声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        securityManager.setRealm(realm);
        // 4.将安全管理器注入安全工具类
        SecurityUtils.setSecurityManager(securityManager);
        // 5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("prannt", "123");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败,用户名错误!");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误!");
        }
    }
}

5.shiro中的授权

5.1 授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

5.2 关键对象

授权可简单理解为who对what(which)进行How操作:

Who,即主体(Subject),主体需要访问系统中的资源。

What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例

How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

5.3 授权流程

5.4 授权方式

5.4.1 基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

// 伪代码
if(subject.hasRole("admin")){	// 如果系统中具有admin角色 
   // 操作什么资源
}

5.4.2 基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

if(subject.isPermission("user:update:01")){ // 资源实例,谁(user)对什么资源(01)有什么样的操作权限(update)
  // user对资源01用户具有修改的权限
}
if(subject.isPermission("user:update:*")){  // 资源类型
  // 对所有的资源 用户具有更新的权限
}

5.5 权限字符串

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。

e.g.

  • 用户创建权限:user:create,或user:create:*
  • 用户修改实例001的权限:user:update:001
  • 用户实例001的所有权限:user:*:001

5.6 shiro中授权编程实现方式

5.6.1 编程式

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
	// 有权限
} else {
	// 无权限
}

5.6.2 注解式

@RequiresRoles("admin")
public void hello() {
	// 有权限
}

5.6.3 标签式

JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
	<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!

5.7 开发授权

CustomerMD5Realm

package com.prannt.realm;

// 使用自定义realm加入MD5 + Salt + Hash散列
public class CustomerMD5Realm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
        System.out.println("==================");
        String primaryPrincipal = (String) principles.getPrimaryPrincipal();
        System.out.println("身份信息:" + primaryPrincipal);
        // 根据身份信息(用户名)获取当前用户的角色信息以及权限信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 将数据库中查询的角色信息赋值给权限对象
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        // 将数据库中查询的权限信息赋值给权限对象
        simpleAuthorizationInfo.addStringPermission("user:*:01");
        simpleAuthorizationInfo.addStringPermission("product:create");
        return simpleAuthorizationInfo;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取用户名
        String principal = (String) token.getPrincipal();

        String username="prannt";
        String password="e4f9bf3e0c58f045e62c23c533fcf633"; // 加密后
        // 根据用户名查询数据库(模拟)
        if (username.equals(principal)){
            // 参数1:数据库用户名
            // 参数2:数据库md5+salt之后的密码
            // 参数3:注册时的随机盐
            // 参数4:realm的名字
            return new SimpleAuthenticationInfo(principal,
                    password,
                    ByteSource.Util.bytes("X0*7ps"),
                    this.getName());
        }
        return null;
    }
}

TestCustomerMD5RealmAuthenticator

package com.prannt;

/**
 * @Description 测试
 * @Date 2022/1/21 17:34
 * @Created Prannt
 */
public class TestCustomerMD5RealmAuthenticator {
    public static void main(String[] args) {
        // 1.创建安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        // 2.注入realm
        CustomerMD5Realm realm = new CustomerMD5Realm();
        // 3.设置realm使用Hash凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 声明:使用的算法
        credentialsMatcher.setHashAlgorithmName("md5");
        // 声明:散列次数
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);
        securityManager.setRealm(realm);
        // 4.将安全管理器注入安全工具类
        SecurityUtils.setSecurityManager(securityManager);
        // 5.通过安全工具类获取subject
        Subject subject = SecurityUtils.getSubject();
        // 6.认证
        UsernamePasswordToken token = new UsernamePasswordToken("prannt", "123");
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("认证失败,用户名错误!");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("认证失败,密码错误!");
        }

        // 对认证用户进行授权
        if (subject.isAuthenticated()) {
            // 1.基于单角色的权限控制
            boolean flag = subject.hasRole("admin");
            System.out.println(flag);
            // 2.基于多角色的权限控制(同时具有多个角色)
            boolean flags = subject.hasAllRoles(Arrays.asList("admin", "user"));
            System.out.println(flags);
            // 3.是否具有其中一个角色
            boolean[] roles = subject.hasRoles(Arrays.asList("admin", "super", "user"));
            for (boolean role : roles) {
                System.out.println(role);
            }
            System.out.println("===========分割线============");
            // 4.基于权限字符串的访问控制    资源标志符:操作:资源类型
            System.out.println("权限:" + subject.isPermitted("user:update:01"));
            System.out.println("权限:" + subject.isPermitted("product:create:02"));
            System.out.println("==========分割线2==========");
            // 5.分别具有哪些权限
            boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
            for (boolean b : permitted) {
                System.out.println(b);
            }
            // 6.同时具有哪些权限
            boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
            System.out.println(permittedAll);
        }
    }
}

6. 整合SpringBoot项目实战

6.1 整合思路

6.2 环境配置

6.2.1 创建项目

就是个普通的SpringBoot工程。在main目录下创建目录webapp,在其中新建index.jsp文件,输入!然后tab可以生成jsp模板,在body中写hello world

6.2.2 引入依赖

<!--引入JSP解析依赖-->
<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
	</dependency>
<dependency>
	<groupId>jstl</groupId>
	<artifactId>jstl</artifactId>
	<version>1.2</version>
</dependency>
<!--引入shiro整合Springboot依赖-->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring-boot-starter</artifactId>
	<version>1.5.3</version>
</dependency>

6.2.3 修改视图

application.properties 文件

server.port=8888
server.servlet.context-path=/shiro
spring.application.name=shiro

spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

6.2.4 修改配置

JSP 与IDEA 与SpringBoot存在一定的不兼容,修改此配置即可解决

本工程使用SpringBoot 2.4.3,不需要此步骤,可以直接访问到页面。

6.2.5 首页和登录页

index.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>系统主页v1.0(受限资源)</h1>
    <ul>
        <li><a href="">用户管理</a></li>
        <li><a href="">商品管理</a></li>
        <li><a href="">订单管理</a></li>
        <li><a href="">物流管理</a></li>
    </ul>
</body>
</html>

login.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>用户登录(公共资源)</h1>
</body>
</html>

6.2.6 访问页面

http://localhost:8888/shiro/index.jsp

6.3 简单使用

6.3.1 创建配置类

package com.prannt.springboot_jsp_shiro.config;

/**
 * @Deception 整合shiro框架相关的配置类
 * shiroFilterFactoryBean依赖于defaultWebSecurityManager
 * @Date 2022/1/23 12:37
 * @Created Prannt
 */
@Configuration
public class ShiroConfig {
    // 1.创建shiroFilter,负责拦截所有请求
    @Bean   // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统的受限资源
        Map<String,String> map = new HashMap<>();
        map.put("/index.jsp","authc");  // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        // 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源

        // 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
        // shiroFilterFactoryBean.setLoginUrl("/login.jsp");

        return shiroFilterFactoryBean;
    }
    // 2.创建shiroFilter需要的安全管理器
    @Bean   //是工厂中的一个对象
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    // 3.创建自定义realm
    @Bean   //是工厂中的一个对象
    public Realm getRealm(){
        return new CustomerRealm();
    }
}

6.3.2 自定义realm

package com.prannt.springboot_jsp_shiro.shiro.realms;

/**
 * @Deception 自定义realm
 * @Date 2022/1/23 13:00
 * @Created Prannt
 */
public class CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

6.4 常见过滤器

注意: shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:

配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定url可以匿名访问(访问时不需要认证授权)
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录
logout LogoutFilter 登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreation NoSessionCreationFilter 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问
port PortFilter 需要指定端口才能访问
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
roles RolesAuthorizationFilter 需要指定角色才能访问
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问

6.5 认证和退出实现

login.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>用户登录(公共资源)</h1>
    <form action="${pageContext.request.contextPath}/user/login" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        提交:<input type="submit" value="登录">
    </form>
</body>
</html>

index.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>系统主页v1.0(受限资源)</h1>
    <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
    <ul>
        <li><a href="">用户管理</a></li>
        <li><a href="">商品管理</a></li>
        <li><a href="">订单管理</a></li>
        <li><a href="">物流管理</a></li>
    </ul>
</body>
</html>

ShiroConfig

package com.prannt.springboot_jsp_shiro.config;

/**
 * @Deception 整合shiro框架相关的配置类
 * shiroFilterFactoryBean依赖于defaultWebSecurityManager
 * @Date 2022/1/23 12:37
 * @Created Prannt
 */
@Configuration
public class ShiroConfig {
    // 1.创建shiroFilter,负责拦截所有请求
    @Bean   // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统的受限资源
        Map<String,String> map = new HashMap<>();
        // map.put("/index.jsp","authc");  // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
        map.put("/user/login","anon");   // 把登录页设为公共资源
        map.put("/**","authc");     // 除了login.jsp以外的资源全部需要用户验证
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        // 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源

        // 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
        // shiroFilterFactoryBean.setLoginUrl("/login.jsp");

        return shiroFilterFactoryBean;
    }
    // 2.创建shiroFilter需要的安全管理器
    @Bean   //是工厂中的一个对象
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    // 3.创建自定义realm
    @Bean   //是工厂中的一个对象
    public Realm getRealm(){
        return new CustomerRealm();
    }
}

UserController

package com.prannt.springboot_jsp_shiro.controller;

@RequestMapping("user")
@Controller
public class UserController {
    /**
     * 用于处理身份认证
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @RequestMapping("login")
    public String login(String username, String password){
        // 1.获取主体对象
        // 注意:在web环境中,只要创建了ShiroConfig,就会自动注入安全管理器
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username,password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "redirect:/login.jsp";
    }

    /**
     * 退出登录
     * @return
     */
    @RequestMapping("logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();   // 退出用户
        return "redirect:/login.jsp";
    }
}

CustomerRealm

package com.prannt.springboot_jsp_shiro.shiro.realms;

/**
 * @Deception 自定义realm
 * @Date 2022/1/23 13:00
 * @Created Prannt
 */
public class CustomerRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("================================");
        String principal = (String) authenticationToken.getPrincipal();
        if ("prannt".equals(principal)) {
            return new SimpleAuthenticationInfo(principal,"123",this.getName());
        }
        return null;
    }
}

测试:

输入:http://localhost:8888/shiro/login.jsp进行登录,点击退出重新登录。

6.6 MD5、Salt的认证实现

6.6.1 用户注册 + 随机盐处理

1、导入依赖

<!--mp-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--mp代码生成器-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.2.0</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

2、application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=630586

mybatis-plus.mapper-locations=classpath*:/mapper/**Mapper.xml
mybatis-plus.type-aliases-package=com.prannt.springboot_jsp_shiro.entity

3、创建数据库

CREATE DATABASE shiro;
USE shiro;

CREATE TABLE t_user(
 `id` INT(6) PRIMARY KEY auto_increment,
 `username` VARCHAR(40) NOT NULL,
 `password` VARCHAR(40) NOT NULL,
 `salt` VARCHAR(255) NOT NULL
);

4、代码生成器

public class CodeGenerator {
    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
//        gc.setOutputDir("D:\\test");
        gc.setAuthor("prannt");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("630586");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(null);
        pc.setParent("com.prannt");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/"
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

5、entity层

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TUser implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String salt;
}

6、controller层

@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class TUserController {
    private final TUserService tUserService;
    /**
     * 用于处理身份认证
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @RequestMapping("login")
    public String login(String username, String password){
        // 1.获取主体对象
        // 注意:在web环境中,只要创建了ShiroConfig,就会自动注入安全管理器
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username,password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "redirect:/login.jsp";
    }

    /**
     * 退出登录
     * @return
     */
    @RequestMapping("logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();   // 退出用户
        return "redirect:/login.jsp";
    }

    /**
     * 用户注册
     * @param tUser
     * @return
     */
    @RequestMapping("/register")
    public String register(TUser tUser){
        try {
            tUserService.register(tUser);
            return "redirect:/login.jsp";
        }catch (Exception e){
            e.printStackTrace();
            return "redirect:/register.jsp";
        }
    }
}

7、service层

接口

public interface TUserService extends IService<TUser> {
    // 注册用户
    void register(TUser tUser);
}

实现类

@Service
@Transactional
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {
    @Autowired
    private TUserMapper tUserMapper;

    // 注册用户
    @Override
    public void register(TUser tUser) {
        // 调用mapper层处理业务
        // 明文密码进行MD5加密 + 随机盐 + Hash散列
        // 1.生成随机盐
        String salt = SaltUtils.getSalt(8);
        // 2.将随机盐保存到数据库中
        tUser.setSalt(salt);
        // 3.根据明文密码MD5 + salt + 散列
        Md5Hash md5Hash = new Md5Hash(tUser.getPassword(),salt,1024);
        tUser.setPassword(md5Hash.toHex());
        // 通过TUserMapper保存对象
        tUserMapper.save(tUser);
    }
}

8、mapper层

接口

@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
    void save(TUser tUser);
}

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.prannt.springboot_jsp_shiro.mapper.TUserMapper">
    <insert id="save" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
        insert into t_user values (#{id},#{username},#{password},#{salt})
    </insert>
</mapper>

9、配置类

/**
 * @Deception 整合shiro框架相关的配置类
 * shiroFilterFactoryBean依赖于defaultWebSecurityManager
 * @Date 2022/1/23 12:37
 * @Created Prannt
 */
@Configuration
public class ShiroConfig {
    // 1.创建shiroFilter,负责拦截所有请求
    @Bean   // 是工厂中的一个filter对象,通过参数注入defaultWebSecurityManager
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统的受限资源
        Map<String,String> map = new HashMap<>();
        // map.put("/index.jsp","authc");     // 两个参数都是受限资源,authc表示请求这个资源需要认证和授权
        map.put("/user/login","anon");        // 把登录页设为公共资源
        map.put("/user/register","anon");     // 放行注册控制层
        map.put("/register.jsp","anon");     // 放行注册页面
        map.put("/**","authc");               // 除了login.jsp以外的资源全部需要用户验证

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        // 配置系统的公共资源,不需要配置,因为除了受限资源就是公共资源

        // 默认认证界面路径,不写路径默认就是login.jsp,也可以指定其他页面为默认路径
        // shiroFilterFactoryBean.setLoginUrl("/login.jsp");

        return shiroFilterFactoryBean;
    }
    // 2.创建shiroFilter需要的安全管理器
    @Bean   //是工厂中的一个对象
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    // 3.创建自定义realm
    @Bean   //是工厂中的一个对象
    public Realm getRealm(){
        return new CustomerRealm();
    }
}

10、创建salt工具类

public class SaltUtils {
    /**
     * 生成salt的静态方法
     * TODO 学完 vueblog项目后,看看能不能用 hutool工具类做这个工作,下面的开发效率太低了
     * @param n 随机盐的位数
     * @return
     */
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String salt = getSalt(4);
        System.out.println(salt);
    }
}

11、测试

访问:http://localhost:8888/shiro/register.jsp

6.6.2 开发数据库认证

1、开发mapper层

@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
    void save(TUser tUser);     // 保存用户
    TUser findByUsername(String username);  // 根据身份信息认证的方法
}
<?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.prannt.springboot_jsp_shiro.mapper.TUserMapper">
    <insert id="save" parameterType="TUser" useGeneratedKeys="true" keyProperty="id">
        insert into t_user values (#{id},#{username},#{password},#{salt})
    </insert>

    <select id="findByUsername" parameterType="String" resultType="TUser">
        select * from t_user where username = #{username}
    </select>
</mapper>

2、service层

public interface TUserService extends IService<TUser> {
    // 注册用户
    void register(TUser tUser);
    // 根据用户名查询用户信息
    TUser findByUsername(String username);
}
@Service("userService")
@Transactional
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {
    @Autowired
    private TUserMapper tUserMapper;

    // 根据用户名查询用户信息
    @Override
    public TUser findByUsername(String username) {
        return tUserMapper.findByUsername(username);
    }
}

3、修改自定义realm

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    String principal = (String) authenticationToken.getPrincipal();
    TUser user = tUserService.findByUsername(principal);    // 返回的是数据库中的user对象
    if (user != null) {
        return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
    }
    return null;
}

4、修改ShiroConfig中realm

// 3.创建自定义realm
@Bean   //是工厂中的一个对象
public Realm getRealm(){
    CustomerRealm customerRealm = new CustomerRealm();
    // 修改凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    // 设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("md5");
    // 设置散列次数
    credentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(credentialsMatcher);
    return customerRealm;
}

5、测试

输入:http://localhost:8888/shiro/login.jsp

参数为:xiaochen 123

6.7 授权实现

6.7.1 没有数据库

1、页面资源授权

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>系统主页v1.0(受限资源)</h1>
    <a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
    <ul>
        <!--具有admin权限才能看到-->
        <shiro:hasRole name="admin">
            <li><a href="">商品管理</a></li>
            <li><a href="">订单管理</a></li>
            <li><a href="">物流管理</a></li>
        </shiro:hasRole>
        <!--普通用户也能看到-->
        <shiro:hasRole name="user">
            <li><a href="">用户管理</a></li>
        </shiro:hasRole>

        <!--用户是user或者admin权限都能看到用户管理-->
        <shiro:hasAnyRoles name="user,admin">
            <li><a href="">用户管理</a>
                <ul>
                    <shiro:hasPermission name="user:add:*">
                        <li><a href="">添加</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:delete:*">
                        <li><a href="">删除</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:update:*">
                        <li><a href="">修改</a></li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:find:*">
                        <li><a href="">查询</a></li>
                    </shiro:hasPermission>
                </ul>
            </li>
        </shiro:hasAnyRoles>
    </ul>
</body>
</html>
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
    // 获取身份信息
    String primaryPrincipal = (String) principles.getPrimaryPrincipal();
    // 根据主身份信息获取角色信息和权限信息
    if ("xiaochen".equals(primaryPrincipal)){
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole("user");
        simpleAuthorizationInfo.addStringPermission("user:find:*");
        simpleAuthorizationInfo.addStringPermission("user:update:*");
        return simpleAuthorizationInfo;
    }
    return null;
}

2、代码方式授权

@Controller
@RequestMapping("order")
public class OrderController {
    // 订单保存
    @RequestMapping("save")
    public String save(){
        // 代码授权(基于角色)
        // 获取主体对象
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole("admin")) {
            System.out.println("保存订单");
        } else {
            System.out.println("无权访问");
        }
        return "redirect:/index.jsp";

        // 代码授权(基于权限字符串)
    }
}

测试:

输入:http://localhost:8888/shiro/login.jsp进行认证

然后访问:http://localhost:8888/shiro/order/save,将在后台打印无权访问

3、注解方式授权

  • CustomerRealm中修改simpleAuthorizationInfo.addRole("admin");
  • OrderControllersave()方法上添加注解@RequiresRoles("admin")
  • 访问http://localhost:8888/shiro/login.jsp先认证,然后访问http://localhost:8888/shiro/order/save,将在后台打印`保存订单``
  • ``@RequiresRoles注解的参数也可以传入一个数组,例如:@RequiresRoles(value={“admin”,”user”),表示同时具有admin权限和user`权限才可以访问
  • @RequiresPermissions()注解用于判断权限字符串,例如:@RequiresPermissions("user:update:01")

6.7.2 连接数据库

6.7.2.1 数据库设计思路

用户表和角色表之间是多对多的关系(一个用户可能对应多个角色,一个角色可能同时被多个用户绑定),角色表和权限表之间也是多对多的关系(一个角色可能有多个权限,一个权限可能被多个角色使用)。数据库在处理多对多关系的时候,需要转换成两个一对多去处理。所以真正在做处理的时候,需要引入中间表(用户角色表和角色权限表),中间表是多的一方。具体流程如下图所示:

6.7.2.2 sql脚本

根据以上分析,设计表的sql语句如下:

-- ----------------------------
-- Table structure for t_user 用户表
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `username` varchar(40) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role  角色表
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_perms 权限表
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_pers` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(80) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user_role 用户角色表
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
  `id` int(6) NOT NULL,
  `userid` int(6) DEFAULT NULL,
  `roleid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_role_perms 角色权限表
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
  `id` int(6) NOT NULL,
  `roleid` int(6) DEFAULT NULL,
  `permsid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
6.7.2.3 获取角色信息

①在数据库中插入数据

②实体类

@Data
public class TRole {
    private Integer id;
    private String name;
}
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TUser implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String salt;

    private List<TRole> roles;  // 角色集合
}

③service层

// 查询一个用户的多个角色
TUser findRolesByUsername(String username);
@Override
public TUser findRolesByUsername(String username) {
	return tUserMapper.findRolesByUsername(username);
}

④Mapper层

TUser findRolesByUsername(String username);   // 查询一个用户的多个角色
<resultMap id="userMap" type="TUser">
       <id column="uid" property="id"></id>
       <result column="username" property="username"></result>
       <!--角色信息-->
       <collection property="roles" javaType="list" ofType="TRole">
           <id column="id" property="id"></id>
           <result column="rname" property="name"></result>
       </collection>
   </resultMap>

   <select id="findRolesByUsername" parameterType="String" resultMap="userMap">
       select
           u.id uid,u.username,r.id,r.name rname
       from
           t_user u
               left join t_user_role ur
                         on
                             u.id = ur.userid
               left join t_role r
                         on
                             ur.roleid = r.id
       where
           u.username = #{username}
   </select>

⑤自定义realm

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
    // 获取身份信息
    String primaryPrincipal = (String) principles.getPrimaryPrincipal();
    TUser user = tUserService.findRolesByUsername(primaryPrincipal);
    // 授权角色信息
    if (!CollectionUtils.isEmpty(user.getRoles())) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        user.getRoles().forEach(role -> {
            simpleAuthorizationInfo.addRole(role.getName());    // 添加角色信息
        });
        return simpleAuthorizationInfo;
    }
    return null;
}

⑥测试

输入:http://localhost:8888/shiro/login.jsp,参数为xiaochen 123

输入:http://localhost:8888/shiro/login.jsp,参数为lucy 456

6.7.2.4 获取权限信息

①在数据库中插入数据

②实体类

@Data
public class TRole {
    private Integer id;
    private String name;
    private List<TPers> pers;   // 权限的集合
}
@Data
public class TPers {
    private Integer id;
    private String name;
    private String url;
}

③service层

// 根据角色id查询权限集合
List<TPers> findPersByRoleId(Integer id);
@Override
public List<TPers> findPersByRoleId(Integer id) {
    return tUserMapper.findPersByRoleId(id);
}

④Mapper层

List<TPers> findPersByRoleId(Integer id);    // 根据角色id查询权限集合
<select id="findPersByRoleId" parameterType="Integer" resultType="TPers">
    select
           p.id,p.name,p.url,r.name
    from
         t_role r
             left join
             t_role_perms rp
                 on
                     r.id = rp.roleid
             left join
             t_pers p
                 on
                     rp.permsid = p.id
    where
          r.id = #{id}
</select>

⑤自定义realm

// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principles) {
	// 获取身份信息
	String primaryPrincipal = (String) principles.getPrimaryPrincipal();
	TUser user = tUserService.findRolesByUsername(primaryPrincipal);
	// 授权角色信息
	if (!CollectionUtils.isEmpty(user.getRoles())) {
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		user.getRoles().forEach(role -> {
			simpleAuthorizationInfo.addRole(role.getName());    // 添加角色信息
			// 权限信息
			List<TPers> pers = tUserService.findPersByRoleId(role.getId());
			if (!CollectionUtils.isEmpty(pers)){
				pers.forEach(perm -> {
					simpleAuthorizationInfo.addStringPermission(perm.getName());
 				});
			}
		});
		return simpleAuthorizationInfo;
	}
	return null;
}

⑥测试


文章作者: Prannt
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Prannt !
评论
  目录