前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >这可能是最全的Shiro入门(整合SSM)

这可能是最全的Shiro入门(整合SSM)

作者头像
上分如喝水
发布2022-04-13 21:09:21
1.4K0
发布2022-04-13 21:09:21
举报
文章被收录于专栏:XiaoLin笔记XiaoLin笔记

文章目录

一、Shiro概述

1.1、权限管理

1.1.1、权限管理

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

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

1.1.2、身份认证

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

1.1.3、授权

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

2.1、什么是shiro

image-20200520220106539
image-20200520220106539

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.

Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。

2.2、为什么要学shiro

  1. 既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
  2. shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。

java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。

2.3、基本功能

image-20200927182532297
image-20200927182532297
  1. Authentication

身份认证/登录,验证用户是不是拥有相应的身份;

  1. Authorization

授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用 户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用 户对某个资源是否具有某个权限;

SessionManager

会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信 息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography

加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

WebSupport

Web 支持,可以非常容易的集成到Web 环境;

Caching

缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency

shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能 把权限自动传播过去;

Testing

提供测试支持;

Run As

允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me

记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录 了。

注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。关于设计,后面的ssm集成shiro里面去说哦

2.4、架构说明

image-20200927210420029
image-20200927210420029

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提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

2.5、下载Shiro和maven的依赖

http://shiro.apache.org/download.html

image-20200928092649234
image-20200928092649234
image-20200928092719767
image-20200928092719767
  • shiro-all 是shiro的所有功能jar包
  • shiro-core 是shiro的基本功能包
  • shiro-web 和web集成的包
  • shiro-spring shrio和spring集成的包

二、Shiro基础入门

2.1、Shiro.ini文件的说明

  1. ini (InitializationFile) 初始文件.Window系统文件扩展名.
  2. Shiro 使用时可以连接数据库,也可以不连接数据库. 如果不连接数据库,可以在shiro.ini中配置静态数据

2.2、Shrio.ini文件的组成部分

[main] :定义全局变量

  1. 内置securityManager对象.
  2. 操作内置对象时,在[main]里面写东西
代码语言:javascript
复制
[main]
securityManager.属性=值
xiaolin=123456
securityManager.对象属性=$xiaolin

[users] :定义用户名和密码

代码语言:javascript
复制
[users]
# 定义用户名为zhangsan 密码为zs
zhangsan=zs
# 定义用户名lisi密码为lisi同时具有role1和role2两个角色
lisi=lisi,role1,role2

[roles]:定义角色

代码语言:javascript
复制
[roles]
role1=权限名1,权限名2 
role2=权限3,权限4

[urls] :定义哪些内置urls生效.在web应用时使用.

代码语言:javascript
复制
[urls]
#url地址=内置filter或自定义filter
# 访问时出现/login的url必须去认证.支持authc对应的Filter 
/login=authc
# 任意的url都不需要进行认证等功能.
/** = anon
# 所有的内容都必须保证用户已经登录.
/**=user
# url abc 访问时必须保证用户具有role1和role2角色.
/abc=roles[“role1,role2”]

2.3、过滤器详解

过滤器名称

过滤器类

描述

anon

org.apache.shiro.web.filter.authc.AnonymousFilter

匿名过滤器

authc

org.apache.shiro.web.filter.authc.FormAuthenticationFilter

如果继续操作,需要做对应的表单验证否则不能通过

authcBasic

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

基本http验证过滤,如果不通过,跳转屋登录页面

logout

org.apache.shiro.web.filter.authc.LogoutFilter

登录退出过滤器

noSessionCreation

org.apache.shiro.web.filter.session.NoSessionCreationFilter

没有session创建过滤器

perms

org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

权限过滤器

port

org.apache.shiro.web.filter.authz.PortFilter

端口过滤器,可以设置是否是指定端口如果不是跳转到登录页面

rest

org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

http方法过滤器,可以指定如post不能进行访问等

roles

org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

角色过滤器,判断当前用户是否指定角色

ssl

org.apache.shiro.web.filter.authz.SslFilter

请求需要通过ssl,如果不是跳转回登录页

user

org.apache.shiro.web.filter.authc.UserFilter

如果访问一个已知用户,比如记住我功能,走这个过滤器

anon: 匿名处理过滤器,即不需要登录即可访问;一般用于静态资源过滤;

如: /static/**=anon

authc: 表示需要认证(登录)才能使用;

如:/**=authc

roles:角色授权过滤器,验证用户是否拥有资源角色;

如:/admin/*=roles[admin]

perms:权限授权过滤器,验证用户是否拥有资源权限;

如:/employee/input=perms[“user:update”]

logout:注销过滤器

如: /logout=logout

三、认证

3.1、认证

认证的过程即为用户的身份确认过程,所实现的功能就是我们所熟悉的登录验证,用户输入账号和密码提交到后台,后台通过访问数据库执行账号密码的正确性校验。

3.2、认证中的关键对象

3.2.1、Subject:主体

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

3.2.2、Principal:身份信息

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

3.2.3、credential:凭证信息

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

3.3、认证流程

image-20200521204452288
image-20200521204452288
image-20200707152052684
image-20200707152052684

最终执行用户比较是在 SimpleAccountRealm类中的doGetAuthenticationInfo方法完成了用户名的校验。

最终密码校验是在 AuthenticatingRealm类中的assertCredentialsMatch方法中完成密码的校验,且是自动的,不需要我们手动完成。

3.4、代码实现

3.4.1、引入依赖

代码语言:javascript
复制
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.2</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.22</version>
    <scope>provided</scope>
</dependency>

3.4.2、编写ini文件

代码语言:javascript
复制
[users]
xiaolin=666
zs=13

3.4.3、编写测试类

代码语言:javascript
复制
 /**
  * 用于测试Shiro基础
  */
  @Test
  public void testShiroBase(){
    // 创建Shiro安全管理器,是Shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    // 加载ini配置文件,得到配置文件中配置的用户信息
    IniRealm iniRealm = new IniRealm("classpath:shiro-authc.ini");
    // 将realm对象注入到securityManager中
    securityManager.setRealm(iniRealm);
    // 将安全管理器注入到当前环境中
    SecurityUtils.setSecurityManager(securityManager);
    // 获取subject主体对象,无论是否登录都可以获取
    Subject subject = SecurityUtils.getSubject();
    // 打印一下认证状态
    System.out.println("认证状态"+subject.isAuthenticated());
    // 创建一个携带了账号和密码的令牌
    UsernamePasswordToken token = new UsernamePasswordToken("xiaolin", "666");
    // 调用subject传入令牌进行登录
    subject.login(token);
    System.out.println("认证状态"+subject.isAuthenticated());
  }

3.4.4、注意

如果输入的身份和凭证和 ini 文件中配置的能够匹配,那么登录成功,登录状态为true,反之登录状态为false。

登录失败一般存在几种情况(抛出的异常):

  1. 账号错误:org.apache.shiro.authc.UnknownAccountException
  2. 密码错误:org.apache.shiro.authc.IncorrectCredentialsException

3.4.5、Shiro认证源码分析

1、调用subject.login方法进行登录,其会自动委托给securityManager.login方法进行登录; 2、securityManager通过Authenticator(认证器)进行认证; 3、Authenticator的实现类ModularRealmAuthenticator调用realm从ini配置文件取用户真实的账号和密码,这里使用的是IniRealm(shiro自带,相当于数据源); 4、IniRealm先根据token中的账号去ini中找该账号,如果找不到则给ModularRealmAuthenticator返回null,如果找到则匹配密码,匹配密码成功则认证通过。

image-20200707152208225
image-20200707152208225

3.5、自定义Realm

自定义 Realm 在实际开发中使用非常多,应该我们需要使用的账户信息通常来自程序或者数据库中, 而不是前面使用到的 ini 文件的配置。

在 AuthenticatingRealm 中调用doGetAuthenticationInfo方法来获取,如果返回的 info不等于空,说明账号存在,才会进行密码校验,如果不存在则直接抛出UnknownAccountException异常。

所以,如果我们要自定义 Realm,应该覆写 doGetAuthenticationInfo()方法,然后在该方法中实现账号的校验,并返回 AuthenticationInfo 对象给上层调用者 AuthenticatingRealm 做进一步的校验。

3.5.1、Shiro提供的Realm

image-20200521212728541
image-20200521212728541

3.5.2、SimpleAccountRealm

我们进行Debug的时候,可以看到认证的源码中使用的是SimpleAccountRealm。

image-20200521213451998
image-20200521213451998

3.5.3、SimpleAccountRealm的部分源码

代码语言:javascript
复制
// 认证
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();
        }
    }
}

3.6、SSM整合Shiro认证

3.6.1、添加依赖

代码语言:javascript
复制
<properties>
	<shiro.version>1.5.2</shiro.version>
</properties>
<!--shiro 核心-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 的 Web 模块-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 和 Spring 集成-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 底层使用的 ehcache 缓存-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!--shiro 依赖的日志包-->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
<!--Freemarker 的 shiro 标签库-->
<dependency>
    <groupId>net.mingsoft</groupId>
    <artifactId>shiro-freemarker-tags</artifactId>
    <version>1.0.1</version>
</dependency>

3.6.2、配置代理过滤器

在访问的时候,需要做一系列的预处理操作,Shiro是选择使用filter过滤器来进行拦截的,因为Shiro不依赖Spring容器,所以当没有springmvc时意味着不能用拦截器,但过滤器则不同,只要是web项目都可以使用。我们需要在web.xml进行配置过滤器。

代码语言:javascript
复制
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这里使用了一个代理过滤器DelegatingFilterProxy,因为真正的shiroFilter需要注入很多复杂的对象,而web.xml中只能配置字符串或数字的参数,是不能满足的,因此我们会把shiroFilter交给 Spring 进行管理,通过spring的xml文件来配置。 使用DelegatingFilterProxy代理过滤器后,但浏览器发送请求过来,被代理过滤器拦截到后,代理过滤器会自动从 spring 容器中找filter-name所配置相同名称的bean,来实现真正的业务。

3.6.3、创建shiro.xml

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--自定义Realm-->
  <bean id="myRealm" class="cn.linstudy.shiro.CarBusinessRealm"/>
    <!--创建安全管理器-->
  <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="myRealm"/>
  </bean>
  <bean id="shiroFilter"
    class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!--引用指定的安全管理器-->
    <property name="securityManager" ref="securityManager"/>
    <!--shiro默认的登录地址是/login.jsp 现在要指定我们自己的登录页面地址-->
    <property name="loginUrl" value="/login.html"/>
    <!--路径对应的规则-->
    <property name="filterChainDefinitions">
      <!--配置请求拦截的方式,是可以匿名还是-->
      <value>
        /empLogin=anon
        /static/**=anon
        /upload/**=anon
        /getImage=anon
        /getEmailCode=anon
        /checkEmail=anon
        /checkUsername=anon
        /error=anon
        /fronted/**=anon
        /**=authc
      </value>
    </property>
  </bean>
</beans>

3.6.4、引入shrio.xml

在mvc.xml 中引入shiro.xml

代码语言:javascript
复制
<import resource="classpath:shiro.xml"/>

3.6.5、配置安全管理器

我们需要在shiro.xml中配置安全管理器。

代码语言:javascript
复制
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"></bean>

3.6.6、配置自定义Realm

代码语言:javascript
复制
public class CarBusinessRealm extends AuthorizingRealm {  
@Autowired
  private EmployeeService employeeService;

  // 认证
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
      throws AuthenticationException {
    // 通过token获取用户名(用户登录的时候)
    String username = (String) token.getPrincipal();
    // 判断是否存在在数据库
    Employee employee = employeeService.selectByUsername(username);
    if (employee != null) {
      // 说明此时用户名是对的,返回一个身份对象
      return new SimpleAuthenticationInfo(employee, employee.getPassword(), this.getName());
    }
    return null;
  }
}

3.6.7、配置自定义Realm

代码语言:javascript
复制
<bean id="myRealm" class="cn.linstudy.shiro.CarBusinessRealm"/>
  <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="myRealm"/>
  </bean>	

3.6.8、修改登录方法

代码语言:javascript
复制
      try {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        SecurityUtils.getSubject().login(token);
        Employee employee = employeeMapper.selectByUsername(username);
        return employee;
      } catch (UnknownAccountException e) {
        throw new CarBussinessException("用户名错误");
      } catch (IncorrectCredentialsException e) {
        throw new CarBussinessException("密码错误");
      }

3.7、登出

shiro中内置了登出方法,我们只需在shiro.xml中的路径规则加入 /logout=logout 即可交给shiro来处理。

四、授权

4.1、授权概述

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。 系统中的授权功能就是为用户分配相关的权限,只有当用户拥有相应的权限后,才能访问对应的资源。 如果系统中无法管理用户的权限,那么将会出现客户信息泄露,数据被恶意篡改等问题,所以在绝大多数的应用中,我们都会有权限管理模块。一般基于角色的权限控制管理有以下三个子模块:

  1. 用户管理
  2. 角色管理
  3. 权限管理
授权流程
授权流程

4.2、 关键对象

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

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

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

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

4.3、授权流程

image-20200521230705964
image-20200521230705964

4.4、授权方式

4.4.1、基于角色的访问控制

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

代码语言:javascript
复制
if(subject.hasRole("admin")){//主体具有admin角色
   //操作什么资源
}

4.4.2、基于资源的访问控制

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

代码语言:javascript
复制
if(subject.isPermission("user:find:*")){ //对用户模块的所有用户有查询权限
  //对01用户进行修改
}
if(subject.isPermission("user:*:01")){  //user模块下的所有用户有所有权限
  //对01用户进行修改
}

4.5、权限字符串

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

例子:

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

4.6、Shiro授权的实现方式

4.6.1、编程式

代码语言:javascript
复制
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
	//有权限
} else {
	//无权限
}

4.6.2、注解式

代码语言:javascript
复制
@RequiresRoles("admin")
public void hello() {
	//有权限
}

4.6.3、标签式

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

4.7、基于ini的授权

4.7.1、编写ini文件

代码语言:javascript
复制
#用户的身份、凭据、角色
[users]
zhangsan=555,hr,seller
xiaolin=123,hr

#角色与权限信息
[roles]
hr=employee:list,employee:delete
seller=customer:list,customer:save

4.7.2、编写测试类

代码语言:javascript
复制
@Test
public void testAuthor(){
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    IniRealm iniRealm = new IniRealm("classpath:shiro-author.ini");
    securityManager.setRealm(iniRealm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("dafei","666");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
    
    //判断用户是否有某个角色
    System.out.println("role1:"+subject.hasRole("role1"));
    System.out.println("role2:"+subject.hasRole("role2"));

    //是否同时拥有多个角色
    System.out.println("是否同时拥有role1和role2:"+subject.hasAllRoles(Arrays.asList("role1", "role2")));

    //check开头的是没有返回值的,当没有权限时就会抛出异常
    subject.checkRole("hr");

    //判断用户是否有某个权限
    System.out.println("user:delete:"+subject.isPermitted("user:delete"));
    subject.checkPermission("user:delete");
}

4.7.3、自定义Realm

代码语言:javascript
复制
public class EmployeeRealm extends AuthorizingRealm {
    //提供认证信息
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
        throws AuthenticationException {
        //暂且使用假数据来模拟数据库中真实的账号和密码
        Employee employee = new Employee();
        employee.setName("admin");
        employee.setPassword("1");
        //获取token中需要登录的账号名
        String username = (String)token.getPrincipal();
        //如果账号存在,则返回一个 AuthenticationInfo 对象
        if(username.equals(employee.getName())){
            return new SimpleAuthenticationInfo(
                employee,//身份对象,与主体subject绑定在一起的对象,暂时无用但后续要用 
                employee.getPassword(),//该账号真正的密码,传给shiro做密码校验的
                this.getName()//当前 Realm 的名称,暂时无用,不需纠结
            );
        }
        return null;
    }
	//提供授权信息
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //登录成功用户对象
        Employee employee = (Employee)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 根据登录用户的id查询到其拥有的所有角色的编码,这里进行模拟数据库
        List<String> roleSns = Arrays.asList("hr","manager","seller");
        // 将用户拥有的角色添加到授权信息对象中,供 Shiro 权限校验时使用
        info.addRoles(roleSns);
        // 根据登录用户的 id 查询到其拥有的所有权限表达式,这里进行模拟数据库
        List<String> expressions = Arrays.asList("employee:list","employee:save");
        // 将用户拥有的权限添加到授权信息对象中,供 Shiro 权限校验时使用
        info.addStringPermissions(expressions);
        return info;
    }
}

4.8、SSM整合Shiro认证

在开发中,我们一般使用注解来完成授权操作, 我们需要使用 Shiro 自身提供的一套注解来完成。

4.8.1、贴注解

在 Controller 的方法上贴上 Shiro 提供的权限注解(@RequiresPermissions,@RequiresRoles)

代码语言:javascript
复制
  // 说明需要有这个权限才可以访问这个方法 
  @RequiresPermissions(value = "employee:list")
  @RequestMapping("list")
  public String listAll(@ModelAttribute("qo") EmployeeQueryObject employeeQueryObject, Model model,
      DepartmentQueryObject departmentQueryObject) {
    PageInfo<Employee> pageInfo = employeeService.selectForPage(employeeQueryObject);
    List<Department> departments = departmentService.selectAll();
    List<Role> roles = roleService.selectAll();
    model.addAttribute("departments", departments);
    model.addAttribute("roles", roles);
    model.addAttribute("pageInfo", pageInfo);
    return "employee/list";
  }

4.8.2、配置注解扫描

当扫描到 Controller 中有使用 @RequiresPermissions 注解时,会使用cglib动态代理为当前 Controller 生成代理对象,增强对应方法,进行权限校验

代码语言:javascript
复制
<!-- <aop:config/> 会扫描配置文件中的所有advisor,并为其创建代理 -->
<aop:config />
<!-- Pointcut advisor通知器, 会匹配所有加了shiro权限注解的方法 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>

4.8.3、修改自定义Realm

代码语言:javascript
复制
// 授权
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 创建info对象
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    // 获取当前登录主体对象
    //Subject subject = SecurityUtils.getSubject();
    // 获取主体的身份对象(这里获取的对象与认证方法doGetAuthenticationInfo返回的SimpleAuthenticationInfo对象的第一个参数是同一个对象)
    Subject subject = SecurityUtils.getSubject();
    Employee employee = (Employee) subject.getPrincipal();
    // 判断是否是超级管理员
    if (employee.getAdmin()) {
      info.addRole("admin");
      // 给他所有的权限
      info.addStringPermission(" *:* ");
    } else {
      // 通过员工id拿到所有的角色
      List<Role> roles = employeeService.selectRolesById(employee.getId());
      for (Role role : roles) {
        info.addRole(role.getSn());
      }
      //通过员工id查询出所有的权限
      List<String> permissionByEmployeeId = employeeService
          .getPermissionByEmployeeId(employee.getId());
      info.addStringPermissions(permissionByEmployeeId);
    }
    return info;
  }

4.8.4、配置自定义异常

代码语言:javascript
复制
 @ExceptionHandler(AuthorizationException.class)
  public String BussinessException(AuthorizationException exception, HttpServletResponse response,
      HandlerMethod method) throws IOException {
    exception.printStackTrace(); //方便开发的时候找bug
    //如果原本控制器的方法是返回jsonresult数据,现在出异常也应该返回jsonresult
    //获取当前出现异常的方法,判断是否有ResponseBody注解,有就代表需要返回jsonresult
    if (method.hasMethodAnnotation(ResponseBody.class)) {
      try {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().print(JSON.toJSONString(new ResponseResult(false, "没有权限操作")));
      } catch (IOException e1) {
        e1.printStackTrace();
      }
      return null;
    }
    //如果原本控制器的方法是返回视图页面,现在也应该返回视图页面
    return "common/nopermission";
  }

4.8.5、Shiro标签集成FreeMarker

在前端页面上,我们通常可以根据用户拥有的权限来显示具体的页面,如:用户拥有删除员工的权限,页面上就把删除按钮显示出来,否则就不显示删除按钮,通过这种方式来细化权限控制。我们需要使用Shiro标签来进行控制。要能够实现上面的控制,需要使用 Shiro 中提供的相关标签。

4.8.5.1、拓展FreeMarker标签

前端页面我们选择的是freemarker,而默认 freemarker 是不支持 shiro 标签的,所以需要对其功能做拓展,可以理解为注册 shiro 的标签,达到在freemarker 页面中使用的目的。

代码语言:javascript
复制
public class ShiroFreeMarkerConfig extends FreeMarkerConfigurer {
  
  @Override
  public void afterPropertiesSet() throws IOException, TemplateException {
    //继承之前的属性配置,这不能省
    super.afterPropertiesSet();
    Configuration cfg = this.getConfiguration();
    cfg.setSharedVariable("shiro", new ShiroTags());//注册shiro 标签,这里可以换成自己想要其他的标签,但是一般建议是用shiro
  }
}
4.8.5.2、修改mvc.xml中的配置

在mvc.xml 中把以前的FreeMarkerConfigurer修改成我们自定义的MyFreeMarkerConfig类

代码语言:javascript
复制
  <!-- 注册 FreeMarker 配置类 -->
  <bean class="cn.linstudy.shiro.ShiroFreeMarkerConfig">
    <!-- 配置 FreeMarker 的文件编码 -->
    <property name="defaultEncoding" value="UTF-8"/>
    <!-- 配置 FreeMarker 寻找模板的路径 -->
    <property name="templateLoaderPath" value="/WEB-INF/views/"/>
    <property name="freemarkerSettings">
      <props>
        <!-- 兼容模式 ,配了后不需要另外处理空值问题,时间格式除外 -->
        <prop key="classic_compatible">true</prop>
        <!-- 数字格式化 , 不会有,字符串的 -->
        <prop key="number_format">0.##</prop>
      </props>
    </property>
  </bean>
4.8.5.3、常用标签
4.8.5.3.1、authenticated标签

authenticated标签里面囊括的表示的是已经通过了认证了的用户才会显示的前端界面

代码语言:javascript
复制
<@shiro.authenticated> </@shiro.authenticated>
4.8.5.3.2、notAuthenticated标签

与authenticated标签相对立,表示为认证通过的用户。

代码语言:javascript
复制
<@shiro.notAuthenticated></@shiro.notAuthenticated>
4.8.5.3.3、principal 标签

principal 标签表示的是输出当前用户的信息。,通常可以用来输出登录用户的用户名。

代码语言:javascript
复制
<@shiro.principal property="name" />
4.8.5.3.4、hasRole 标签

hasRole表示验证当前用户是否拥有某些角色。

代码语言:javascript
复制
<@shiro.hasRole name="admin">Hello admin!</@shiro.hasRole>
4.8.5.3.5、hasAnyRoles 标签

hasAnyRoles标签表示验证当前用户是否拥有这些角色中的任何一个,角色之间逗号分隔。

代码语言:javascript
复制
<@shiro.hasAnyRoles name="admin,user">Hello admin</@shiro.hasAnyRoles>
4.8.5.3.6、hasPermission 标签

hasPermission 标签表示验证当前用户是否拥有该权限。

代码语言:javascript
复制
<@shiro.hasPermission name="department:delete">删除</@shiro.hasPermission>

4.8.6、重构权限加载方法

这里要说的是一种思想,我们在项目中可能会遇到需要加载项目中加了@RequiresPermissions注解的权限,就会有一个类似加载权限的按钮。

image-20210325114035329
image-20210325114035329

但是我们发现好像Shiro的@RequiresPermissions注解并没有提供name属性给我们,仅仅只有value属性,那么我们需要另辟蹊径来完成这个需求。 Shiro的@RequiresPermissions注解有两个属性:

value属性:这个属性是一个数组,也就是说一个请求映射方法可以运行配置多个权限。多个权限之间用逗号隔开

代码语言:javascript
复制
value={"employee:list","employee:delete", }

logical 属性:该属性根据配置属性值对当前用户是否有权限访问请求映射方法进行限制,他有两个值: Logical.AND: 必须同时拥有value配置所有权限才允许访问。 Logical.OR:只需要拥有value配置所有权限中一个即可允许访问。

我们可以约定@RequiresPermissions 注解中的value属性值(数组)中第一位为权限表达式, 第二位为权限名称。

代码语言:javascript
复制
  // shiro注解无法使用name属性,所以约定,value中第一个位置的值是权限表达式,第二个位置的值是权限名称.
  // 但是 logical 的值必须是Logical.OR
  @RequiresPermissions(value = {"employee:list", "员工列表"}, logical = Logical.OR)
  @RequestMapping("list")
  public String listAll(@ModelAttribute("qo") EmployeeQueryObject employeeQueryObject, Model model,
      DepartmentQueryObject departmentQueryObject) {
    PageInfo<Employee> pageInfo = employeeService.selectForPage(employeeQueryObject);
    List<Department> departments = departmentService.selectAll();
    List<Role> roles = roleService.selectAll();
    model.addAttribute("departments", departments);
    model.addAttribute("roles", roles);
    model.addAttribute("pageInfo", pageInfo);
    return "employee/list";
  }

同时修改reload方法

代码语言:javascript
复制
 public void reload() {
    //一次性的把所有的权限信息查出来.
    List<Permission> permissionsOnDB = permissionMapper.selectAll();
    // 拿到所有方法
    Map<RequestMappingInfo, HandlerMethod> handlerMethods = rmhm.getHandlerMethods();
    // 遍历所有方法
    for (HandlerMethod method : handlerMethods.values()) {
      // 判断方法上是否有RequiresPermissions注解
      RequiresPermissions annotation = method.getMethodAnnotation(RequiresPermissions.class);
      if (annotation != null) {
        // 如果不为空。说明是贴了注解的
        // 数组的第一个元素表示的是权限表达式
        String expression = annotation.value()[0];
        // 数组的第二个元素表示的是权限的名称
        String name = annotation.value()[1];
        // 在存入数据库之前先判断以下,如果数据库中没有才插入
        if (!permissionsOnDB.contains(expression)) {
          Permission permission = new Permission();
          permission.setName(name);
          permission.setExpression(expression);
          permissionMapper.insert(permission);
        }
      }
    }
  }

五、Shiro密码加密

加密的目的是从系统数据的安全考虑,如,用户的密码,如果我们不对其加密,那么所有用户的密码在数据库中都是明文,只要有权限查看数据库的都能够得知用户的密码,这是非常不安全的。所以,只要密码被写入磁盘,任何时候都不允许是明文, 以及对用户来说非常机密的数据,我们都应该想到使用加密技术,这里我们采用的是 MD5+盐+散列次数来进行加密。

如何实现项目中密码加密的功能:

  1. 添加用户的时候,对用户的密码进行加密
  2. 登录时,按照相同的算法对表单提交的密码进行加密然后再和数据库中的加密过的数据进行匹配

5.1、MD5+盐加密

MD5 加密的数据如果一样,那么无论在什么时候加密的结果都是一样的,所以,相对来说还是不够安全,但是我们可以对数据加“盐”。同样的数据加不同的“盐”之后就是千变万化的,因为我们不同的人加的“盐”都不一样。这样得到的结果相同率也就变低了。

盐一般要求是固定长度的字符串,且每个用户的盐不同。

可以选择用户的唯一的数据来作为盐(账号名,身份证等等),注意使用这些数据作为盐要求是不能改变的,假如登录账号名改变了,则再次加密时结果就对应不上了。

5.2、Md5Hash()

Md5Hash()这个方法有三个参数,第一个参数表示需要加密的密码的明文,第二个参数表示加密时所需要的盐,第三个参数表示散列次数(加密几次),这样可以保证加密后的密文很难恢复和破解。

5.3、注册用户(密码加密)

在添加用户的时候,需要对用户的密码进行加密。

代码语言:javascript
复制
@RequestMapping("checkUsername")
  @ResponseBody
  public ResponseResult checkUsername(String username, String password, HttpSession session) {
    Employee employee = employeeService.selectByUsername(username);
    if (employee == null) {
      // 进行MD5密码加密
      Md5Hash md5Hash = new Md5Hash(password, username, 1024);
      EmployeeInsertVO employeeVO = new EmployeeInsertVO(username, username, md5Hash.toString(),
          session.getAttribute("EMAIL_IN_SESSION").toString(), false, true);
      employeeService.regsiter(employeeVO);
      return new ResponseResult(true, "注册成功");
    } else {
      return new ResponseResult(false, "用户名已存在");
    }
  }

5.4、登录

在登录时, 先对前端传过来的密码进行与注册相同的相同算法的加密,再传给shiro进行认证处理即可。

代码语言:javascript
复制
try {
        // 对传进来的密码进行加密
        Md5Hash md5Hash = new Md5Hash(password, username, 1024);
        UsernamePasswordToken token = new UsernamePasswordToken(username, md5Hash.toString());
        SecurityUtils.getSubject().login(token);
        Employee employee = employeeMapper.selectByUsername(username);
        return employee;
      } catch (UnknownAccountException e) {
        throw new CarBussinessException("用户名错误");
      } catch (IncorrectCredentialsException e) {
        throw new CarBussinessException("密码错误");
      }
}

六、Shiro集成EhCache

6.1、Cache是什么

Cache是缓存,他是计算机内存中一段数据 ,他的作用是 用来减轻DB的访问压力,从而提高系统的查询效率。

image-20200530090656417
image-20200530090656417

6.2、使用缓存的原因

我们在进行Debug的时候,我们会发现,一旦请求到需要权限控制的方法的时候,每请求一次他都会去调用自定义Realm中的 doGetAuthorizationInfo 方法获取用户的权限信息,这个时候对数据库造成的访问压力是十分大的,而且用户登陆后,授权信息一般很少变动,所以我们可以在第一次授权后就把这些授权信息存到缓存中,下一次就直接从缓存中获取,避免频繁访问数据库。

6.3、集成EhCache

6.3.1、引入依赖

代码语言:javascript
复制
<!--引入shiro和ehcache-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-ehcache</artifactId>
  <version>1.5.3</version>
</dependency>

6.3.2、添加缓存配置文件

代码语言:javascript
复制
<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="600"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

配置文件属性详解: maxElementsInMemory: 缓存对象最大个数。

**eternal **:对象是否永久有效,一但设置了,timeout 将不起作用。

timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。

timeToLiveSeconds:对象存活时间,指对象从创建到失效所需要的时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,默认是 0,也就是对象存活时间无穷大。

memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。

缓存策略一般有3种:

  1. 默认LRU(最近最少使用,距离现在最久没有使用的元素将被清出缓存)。
  2. FIFO(先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉)。
  3. LFU(较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率),hit 值最小的将会被清出缓存)。

6.3.2、配置缓存管理器

代码语言:javascript
复制
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!--注册自定义数据源-->
    <property name="realm" ref="employeeRealm"/>
    <!--注册缓存管理器-->
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> 
    <!-- 设置配置文件 -->
    <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-04-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 一、Shiro概述
    • 1.1、权限管理
      • 1.1.1、权限管理
      • 1.1.2、身份认证
      • 1.1.3、授权
    • 2.1、什么是shiro
      • 2.2、为什么要学shiro
        • 2.3、基本功能
          • 2.4、架构说明
            • 2.5、下载Shiro和maven的依赖
            • 二、Shiro基础入门
              • 2.1、Shiro.ini文件的说明
                • 2.2、Shrio.ini文件的组成部分
                  • 2.3、过滤器详解
                  • 三、认证
                    • 3.1、认证
                      • 3.2、认证中的关键对象
                        • 3.2.1、Subject:主体
                        • 3.2.2、Principal:身份信息
                        • 3.2.3、credential:凭证信息
                      • 3.3、认证流程
                        • 3.4、代码实现
                          • 3.4.1、引入依赖
                          • 3.4.2、编写ini文件
                          • 3.4.3、编写测试类
                          • 3.4.4、注意
                          • 3.4.5、Shiro认证源码分析
                        • 3.5、自定义Realm
                          • 3.5.1、Shiro提供的Realm
                          • 3.5.2、SimpleAccountRealm
                          • 3.5.3、SimpleAccountRealm的部分源码
                        • 3.6、SSM整合Shiro认证
                          • 3.6.1、添加依赖
                          • 3.6.2、配置代理过滤器
                          • 3.6.3、创建shiro.xml
                          • 3.6.4、引入shrio.xml
                          • 3.6.5、配置安全管理器
                          • 3.6.6、配置自定义Realm
                          • 3.6.7、配置自定义Realm
                          • 3.6.8、修改登录方法
                        • 3.7、登出
                        • 四、授权
                          • 4.1、授权概述
                            • 4.2、 关键对象
                              • 4.3、授权流程
                                • 4.4、授权方式
                                  • 4.4.1、基于角色的访问控制
                                  • 4.4.2、基于资源的访问控制
                                • 4.5、权限字符串
                                  • 4.6、Shiro授权的实现方式
                                    • 4.6.1、编程式
                                    • 4.6.2、注解式
                                    • 4.6.3、标签式
                                  • 4.7、基于ini的授权
                                    • 4.7.1、编写ini文件
                                    • 4.7.2、编写测试类
                                    • 4.7.3、自定义Realm
                                  • 4.8、SSM整合Shiro认证
                                    • 4.8.1、贴注解
                                    • 4.8.2、配置注解扫描
                                    • 4.8.3、修改自定义Realm
                                    • 4.8.4、配置自定义异常
                                    • 4.8.5、Shiro标签集成FreeMarker
                                    • 4.8.6、重构权限加载方法
                                • 五、Shiro密码加密
                                  • 5.1、MD5+盐加密
                                    • 5.2、Md5Hash()
                                      • 5.3、注册用户(密码加密)
                                        • 5.4、登录
                                        • 六、Shiro集成EhCache
                                          • 6.1、Cache是什么
                                            • 6.2、使用缓存的原因
                                              • 6.3、集成EhCache
                                                • 6.3.1、引入依赖
                                                • 6.3.2、添加缓存配置文件
                                                • 6.3.2、配置缓存管理器
                                            相关产品与服务
                                            容器服务
                                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                            领券
                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档