Shiro学习笔记

Shiro

1. 什么是权限管理

一般来说,一个系统对于不同的用户,提供的不同的功能,也就是权限不同。所以对于多角色的用户,一般需要进行权限管理,实现对用户访问系统的控制,按照安全规则或者安全策略控制用户只能访问自己被授权的资源。

​ 权限管理包括对用户身份的 认证授权 两部分。对于需要访问控制的资源,用户首先要经过身份认证,认证通过后,根据授予的权限,访问相应的资源。

  • 认证

    ​ 判断一个用户是否为合法用户。(用户登录校验session验证、token验证都是常见的方式

  • 授权

    ​ 也就是访问控制,为用户授予相应的权限,必须拥有一定的权限才能访问系统资源。(比如普通管理员,无权访问比较隐私的系统资源,只有高级管理员才可以等等)

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下的Simple Java Security。它将软件系统的安全认证相关的功能抽取出来,实现身份验证、授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

3. Shiro的核心架构

Shiro核心架构

3.1 Subject

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

​ Subject在Shiro中是一个接口,定义了很多认证授权相关的方法,外部程序通过Subject,在Security Manager中进行认证授权。

3.2 Security Manager

安全管理器,对全部的Subject进行管理,是Shiro的 核心。其中包含了多个组件,分别对应不同的功能:

  • 通过Authenticator进行认证

  • 通过Authorizer进行授权

  • 通过Session Manager 进行会话管理

    Security Manager是一个接口,继承了Authenticator、Authorizer、SessionManager这三个接口

3.3 Authenticator

认证器,是一个接口,提供对用户身份进行认证的功能。Shiro提供 ModularRealmAuthenticator 这个实现类,基本上可以满足大多数的认证需求,也可以自定义认证器。

3.4 Authorizer

授权器,用户通过认证器认证后,访问功能时,需要通过授权器判断用户是否拥有权限。

3.5 Realm

领域,相当于Data Source数据源,Security Manager进行安全认证和权限判断时,需要通过Realm连接到数据源(数据库或者配置文件),获取用户相应的身份信息和权限数据

不要把Realm理解成只是从数据源取数据,在Realm中还有认证授权校验的相关代码

3.6 Session Manager

会话管理器,不依赖于web容器的Session,所以Shiro可以在非web应用上使用,也可以将分布式应用的会话集中在一点管理(实现单点登录)。

3.7 Session DAO

会话Dao,对session操作的一套接口,比如将session存入到数据库。

3.8 Cache Manager

缓存管理,将用户权限数据存储在缓存中,用户的权限只需要在第一次的时候通过Realm从数据源中获取,获取后会将其缓存在Cache Manager中,之后直接在这里获取即可,提高性能

3.9 Cryptography

密码管理,提供了一套加密/解密的组件,方便开发。

4. Shiro中的认证

4.1 Shiro认证的关键对象

  • Subject:主体

    访问系统的用户、程序,都是主体,需要进行认证

  • Principal:身份信息

    主体进行身份认证的标识,必须具有唯一性。一个主体可以有多个身份,但是必须要有一个主身份,用于认证。(相当于用户名)

  • Credential:凭证信息

    只有主体自己知道的安全信息,比如密码、证书等。(相当于口令)

4.2 认证流程

​ 首先将主体传入的身份信息+凭证信息转换成相应的 token → 安全管理器判断此token是否合法,只有合法,才可以进入系统。

而且只要认证通过,访问其他受限资源(需要先认证的资源),就不需要再进行认证了。(也就是说认证通过后,就不会再进行认证了。Shiro的任务就只剩下授权了)退出了登录,才需要重新认证。(subject.logout()

4.3 实操

  • 引入依赖

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.5.3</version>
    </dependency>
    
  • 加入配置文件

    • Shiro的配置文件是.ini,位置的话放在resources下即可,命名没有特别要求
    • 而且这个配置文件,只是为了在初学阶段,为了减少连接数据库的麻烦配置,可以直接在这个配置文件中写死指定,方便熟悉Shiro的特性和功能。随着学习的深入,信息则都是从数据库中获取的(一般的认证、权限信息需要从数据库中获取),这个配置文件就不必要了。
    • 或者在单机应用,不访问数据库的情况下使用配置文件来配置信息。
[users]  # 固定写法,表明下面数据为认证信息,可配置多条
# 配置认证信息:用户名=密码
klane=123456 
白鱼=lhy 
  • 认证的代码
    • 注意不要导错包,都是Shiro依赖内的
    • 认证成功时,不会有任何消息。认证失败时:
      • 如果是用户名不存在:抛出UnknownAccountException异常
      • 如果是密码错误:抛出IncorrectCredentialsException异常
public static void main(String[] args) {
    //1.创建安全管理器对象(SecurityManager是接口,DefaultSecurityManager是实现类)
    DefaultSecurityManager securityManager = new DefaultSecurityManager();

    //2.给安全管理器对象设置Realm(读取放在resources的配置文件)
    securityManager.setRealm(new IniRealm("classpath:shiro.ini")); 

    //3.使用全局安全工具类SecurityUtils,进行相应的认证和授权操作
    //给SecurityUtils设置安全管理器
    SecurityUtils.setSecurityManager(securityManager);

    //4.拿到Subject对象
    Subject subject = SecurityUtils.getSubject();

    //5.创建令牌(Subject认证时需要通过 “身份信息” 和 “凭证信息” 生成令牌)
    UsernamePasswordToken token = new UsernamePasswordToken("白鱼","lhy");

    //6.通过Subject对象进行用户认证(认证通过,没有任何返回消息;认证失败,抛出异常)
    try {
        System.out.println("认证状态:" + subject.isAuthenticated());  //false
        subject.login(token);  //认证
        System.out.println("认证状态:" + subject.isAuthenticated());  //true
    } catch (UnknownAccountException e) {
        System.out.println("用户不存在");
        e.printStackTrace();
    } catch (IncorrectCredentialsException e){
        System.out.println("密码不正确");
        e.printStackTrace();
    }
}

4.4 认证的源码浅析

  1. 最终执行身份信息校验的是在SimpleAccountRealm中的doGetAuthenticationInfo方法中进行校验

  2. 最终执行凭证信息校验的是在AuthenticatingRealm中的assertCredentialsMath方法中自动校验

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
            CredentialsMatcher cm = this.getCredentialsMatcher();
            if (cm != null) {
                if (!cm.doCredentialsMatch(token, info)) {    			 // !!!
                    String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
                    throw new IncorrectCredentialsException(msg);
                }
            } else {
                throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication.  If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
            }
        }
    
    • 这个方法内调用了CredentialsMatcher接口doCredentialsMatch,不同的实现类对此方法有不同的实现,但都是获取到token的凭证,再获取到数据源中的凭证,两者进行equals判断而已

      //SimpleCredentialsMatcher类中重写的方式
      public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
              Object tokenCredentials = this.getCredentials(token);
              Object accountCredentials = this.getCredentials(info);
              return this.equals(tokenCredentials, accountCredentials);
          }
      

    常见的Realm关系

常见的Realm关系图

  • AuthenticatingRealm 认证Realm(抽象类),提供了doGetAuthenticationInfo抽象方法,用于认证
  • AuthorizingRealm 授权Realm(抽象类),提供了doGetAuthorizationInfo抽象方法, 用于授权

而AuthorizingRealm 又继承了AuthenticatingRealm ,所以SimpleAccountRealm只要继承了AuthorizingRealm 抽象类,就同时拥有上面授权和认证所需要的接口,可以进行重写。

也就是说,如果我们要自定义Realm,就需要去继承AuthorizingRealm这个抽象类,然后重写doGetAuthorizationInfo(用于授权)和doGetAuthenticationInfo(用于认证)方法即可。

4.5 自定义Realm

​ 上面说到的,自定义Realm需要继承AuthorizingRealm,然后重写两个方法

//自定义Realm,将认证、授权的数据源转为从数据库中获取
public class MyRealm extends AuthorizingRealm {

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

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取到token中拿到的身份凭证
        String principal = (String) token.getPrincipal();
        System.out.println(principal);

        //之后可以根据身份信息使用jdbc mybatis查询相关数据库即可
        //参数1:身份信息(用户名)    参数2:数据库中正确的凭证信息(密码)     参数3:提供当前Realm的名字
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("klane","123",this.getName());
        return simpleAuthenticationInfo;
    }
}

​ 至于使用,只是在安全管理器中设置Realm的时候,new我们自定义的Realm即可。

//创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置自定义的Realm
securityManager.setRealm(new MyRealm());   	  //!!
//将安全管理器设置到安全工具类中
SecurityUtils.setSecurityManager(securityManager);
//通过安全工具类获取到Subject
Subject subject = SecurityUtils.getSubject();
//创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("klane", "123456");
//身份验证
try {
    subject.login(token);
} catch (UnknownAccountException e) {
    System.out.println("用户不存在");
    e.printStackTrace();
} catch (IncorrectCredentialsException e){
    System.out.println("密码不正确");
    e.printStackTrace();
}

4.6 凭证加密处理

​ 这里使用MD5+Salt的方式进行实现。将盐和散列后的值分别存入在数据库中,Realm从数据库中取出盐和加密后的值,与token中获取到的密码以及前面的盐去拼接并散列后,进行比较

MD5

作用:一般用来加密 或 签名(校验和)

特点:不可逆,一致性

结果:16进制 32位的字符串

应用:

​ 加密使用

​ 校验(比如判断两个文件是否内容相同,就可以通过比较两者的MD5值)

MD5要破解,只能通过穷举进行破解,所以如果数据库信息被盗窃了,虽然有在线网站之类的可以进行穷举破解出原文,但是只要密码过于复杂,穷举的难度也就加大了。

所以,为了避免有的用户真的使用了简单的密码,我们可以通过加盐的方式来复杂化密码(你不体面,我们帮你体面)。这个盐可以是随机的,不同用户有不同的盐,保存在数据库中。

即使这个盐和MD5后的密文都被盗窃了,但是由于我们通过盐复杂化了,要破解复杂化的MD5密文,也还是很难的,而且还需要清楚盐拼接在哪里(前面?中间?后面?),又加上了一层保障。

当然了,若是真的被穷举到了,知道了明文,又知道了盐,人脑判断一下拼接规则,那么这个用户也就木大啦,黑客就可以使用了这个拼接规则去写自动处理的程序了)

  • 加密的代码实现(用在注册的时候)

    public static void main(String[] args) {
        //创建普通的一个md5算法。
        Md5Hash md5Hash = new Md5Hash("123");
        //注意:不能通过setBytes的方式传入要加密的数据,这样子并不会进行MD5加密
        //md5Hash.setBytes("12345".getBytes());
    
        //创建加盐的md5。 参数1:要加密的数据    参数2:盐
        //默认是将盐拼接到数据的面,再去进行MD5散列
        Md5Hash md5Hash = new Md5Hash("123", "1*sp23v");
    
        //md5 + salt + 散列次数(默认是一次)
        Md5Hash md5Hash = new Md5Hash("123", "1*sp23v", 1024);
    
    
        System.out.println(md5Hash.toHex());  //转成16进制(32个字符)
    }
    
  • 认证的代码实现(用在登录的时候)

    • 注意:前面我们使用的时候是没有用到MD5的,所以对于凭证信息的校验,使用的是SimpleCredentialsMatcherdoCredentialsMatch方法,这个方法是无法对凭证信息进行相应的MD5运算,再去和数据库中保存的加密后的凭证去比较,所以我们需要为Realm设置一个合适的凭证匹配器(HashedCredentialsMatcher)。

      //创建安全管理器
      DefaultSecurityManager securityManager = new DefaultSecurityManager();
      //创建自定义的Realm
      MyRealm myRealm = new MyRealm();
      
      //创建可以处理MD5的凭证匹配器			(在这一部分处理,其他的一样不变)
      HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
      credentialsMatcher.setHashAlgorithmName("md5");  //指定散列函数名
      credentialsMatcher.setHashIterations(1024);//指定散列次数
      myRealm.setCredentialsMatcher(credentialsMatcher);  //这是到自定义Realm中
      
      securityManager.setRealm(myRealm);
      
      //将安全管理器设置到安全工具类中
      SecurityUtils.setSecurityManager(securityManager);
      //通过安全工具类获取到Subject
      Subject subject = SecurityUtils.getSubject();
      //创建令牌
      UsernamePasswordToken token = new UsernamePasswordToken("klane", "123456");
      //身份验证
      try {
          subject.login(token);
          System.out.println(subject.isAuthenticated());
      } catch (UnknownAccountException e) {
          System.out.println("用户不存在");
          e.printStackTrace();
      } catch (IncorrectCredentialsException e){
          System.out.println("密码不正确");
          e.printStackTrace();
      }
      
    • 同时对于自定义的Realm,也有需要进行改变的部分

      public class MyRealm extends AuthorizingRealm {
          //授权
      	……
      
          //认证
          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
              //获取到token中拿到的身份凭证
              String principal = (String) token.getPrincipal();
              System.out.println(principal);
      
              //之后可以根据身份信息使用jdbc mybatis查询相关数据库即可
              
         //参数1:身份信息(用户名)               参数2:数据库中正确的凭证信息(密码 + md5 + salt)
         //参数3:(注册时生成的)随机盐            参数4:提供当前Realm的名字
              SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("klane",
                      "aa901020133e0926060b7de4c1d0ccf0",
                      ByteSource.Util.bytes("1*sp23v"),
                      this.getName());
              return simpleAuthenticationInfo;
          }
      }
      

5. Shiro的授权

5.1 Shiro授权的关键对象

  • Subject(主体)

    就是谁要来访问。(认证通过是前提)

  • Resource(资源)

    如系统菜单、按钮、页面请求路径、方法、商品信息等等。资源包括资源类型资源实例

    商品信息为资源类型,具体的某一类商品、某一个商品是资源实例。

  • Permission(权限)

    规定了主体对资源的操作许可,权限离开资源没有意义(所以权限需要和资源进行绑定,当然了,权限也是需要和主体进行绑定的)。

    权限 = 资源 + 操作(CURD)

授权就是:定义 “主体” 对哪些 “资源” 具有某些 “操作/权限”。

5.2 授权方式

  • 基于 角色 的访问控制(Role-Based Access Control)

    if(subject.hasRole("admin")){
        //具有admin角色的所有操作权限
    }
    
  • 基于 资源 的访问控制(Resource-Based Access Control)

    if(subject.isPermitted("goods:find:*")){
        //对所有的商品具有查询权限
    }
    
    if(subject.isPermitted("goods:*:01")){
        //对编号为01的商品,具有任意权限(CURD)
    }
    

5.3 权限字符串

​ 上面基于资源的访问控制,使用的就是权限字符串进行判断。

​ 权限字符串的规则是:资源标识符 : 操作 : 资源实例表示符。例子:

  • goods : find : *(可以省略成goods : find) 对所有的商品具有查询权限
  • goods : * : 01 对编号为01的商品,具有任意权限(CURD)
  • * : * : * 对所有资源,所有实例,拥有所有的操作(超级管理员)

5.4 代码中实现授权的方式

5.4.1 编程式

if(subject.hasRole("admin")){
    //有权限
} else{
    //无权限
}

5.4.2 注解式

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

5.4.3 标签式(少)

<!--在JSP/GSP页面中使用标签-->
<shiro:hasRole name="admin">  有权限的操作  </shiro:hasRole>

<!--thymeleaf使用shiro需要额外集成,JSP可以直接使用-->

5.5 实操

5.5.1 基于角色的访问控制

  • 在自定义Realm中,重写doGetAuthorizationInfo方法
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //获取主身份信息(其实就是用户名)
    String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
    System.out.println("主身份:" + primaryPrincipal);

    //根据这个用户名,可以去查询数据库,获取到当前用户的角色信息以及权限信息
	……

    //返回目标对象
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    //设置此用户拥有的角色(从数据库中查询出来的)
    simpleAuthorizationInfo.addRole("admin");
    simpleAuthorizationInfo.addRole("user");
    return simpleAuthorizationInfo;
}
  • 在认证通过的前提下,判断是否拥有角色
//基于单角色访问控制
if(subject.hasRole("admin")){
    //操作
}

//如果需要同时拥有多个角色
if(subject.hasAllRoles(Arrays.asList("admin", "user"))){
    //操作
}

//判断是否拥有指定角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for(boolean b: booleans){
    System.out.println(b);
}

5.5.2 基于权限字符串的访问控制

  • 在自定义Realm中,重写doGetAuthorizationInfo方法
//核心代码,设置权限信息
simpleAuthorizationInfo.addStringPermission("user:*:*");
  • 在认证通过的前提下,判断是否拥有权限
/**核心代码**/

//判断是否具有权限(true)
boolean b = subject.isPermitted("user:update:01")
  
//同时具有哪些权限
boolean b = subject.isPermittedAll("", "", "", ……);
    
//多个权限判断
boolean[] booleans = subject.isPermitted("", "", "", ……);

6. Shiro整合SpringBoot项目实战

6.1 引入依赖

<!--Shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>

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

<!--数据库相关-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.39</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>

6.2 配置Shiro

  • 创建 配置类
@Configuration
public class ShiroConfig {

    //创建ShiroFilter(用于拦截所有请求,对受限资源进行Shiro的认证和授权判断)
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系统的受限资源
        Map<String, String> map = new HashMap<>();
//      map.put("/index","authc"); //key:资源路径   value:标识为受限资源(需要认证和授权)
        //如果是/**,表示所有资源都是受限资源
        map.put("/**", "authc");
        map.put("/login", "anon"); //登录路径、注册页面、注册路径都需要放行(公共资源,登录页面已经是了)
        map.put("/registerPage", "anon");
        map.put("/register", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //指定认证不通过时的跳转路径(默认是/login.jsp,从这里可以看出shiro和jsp的好基友关系)
        shiroFilterFactoryBean.setLoginUrl("/loginPage");

        return shiroFilterFactoryBean;
    }

    //创建安全管理器(对于web应用,不能使用DefaultSecurityManager,不具备web功能)
    //而且会自动设置到SecurityUtils中设置这个安全管理器,不需要手动设置
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        //给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    //创建自定义Realm
    @Bean
    public Realm getRealm(){
        CustomerRealm realm = new CustomerRealm();
        //修改凭证校验匹配器(处理加密)
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(1024);

        realm.setCredentialsMatcher(hashedCredentialsMatcher);
        return realm;
    }
}

  • Shiro提供的过滤器

​ 上面配置类中,配置了ShiroFilter,通过map保存了受限资源。value为:authc,其实是Shiro提供了一种过滤器。Shiro还提供了其他的过滤器。

配置缩写对应的过滤器功能
anonAnnoymousFilter指定的URL无需经过认证,可以直接访问(即公共资源
authcFormAuthenticationFilter指定的URL需要form表单登录。如果登录不成功毁跳转到loginUrl配置的路径。(即受限资源
authcBasicBasicHttpAuthenticationFilter需要Basic登录
logoutLogoutFilter登出过滤器,配置指定的URL就可以实现退出功能
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转成相应的动词来构造一个权限字符串
rolesFolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或者“记住我”的用户才能访问
  • 定义 自定义Realm
这个在下面登录认证的时候粘代码

6.3 数据库

​ 数据库设计,对于权限部分,可以有多种设计方式:

1、用户表 + 角色表 + 权限表 →(用户拥有多个角色,角色拥有多个权限)

2、用户表 + 角色表

3、用户表 + 权限表

上面表之间的关系,都是多对多的关系,所以还需要另外建立相应的**关联表**

​ 这里采取第一种方式,也是比较常见的方式。

表关系

  • 用户表
CREATE TABLE `t_user` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
  • 角色表
CREATE TABLE `t_role` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(60) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 权限表
CREATE TABLE `t_pers` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `persname` varchar(60) NOT NULL,
  `url` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 用户-角色关联表
CREATE TABLE `t_user_role` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `userid` int(6) NOT NULL,
  `roleid` int(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 角色-权限关联表
CREATE TABLE `t_role_pers` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `roleid` int(6) NOT NULL,
  `persid` int(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

6.4 注册时加密

  • controller、mapper层的比较简单就不放代码了,核心就是service中对密码加密的处理
//service中的注册方法
public void register(User user) {
    //对密码进行:MD5 + salt + 散列
    String salt = SaltUtil.getSalt(8);
    user.setSalt(salt);  //salt也要保存
    Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
    user.setPassword(md5Hash.toHex());

    //保存到数据库中
    userMapper.save(user);
}
public class SaltUtil {
    //随机生成n位的盐
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqestuvwxyz0123456789!@#$%^&*".toCharArray();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            sb.append(c);
        }
        return sb.toString();
    }
}

6.5 登录时 认证

  • 自定义的Realm要重写
public class CustomerRealm extends AuthorizingRealm {
//    @Autowired
//    private UserService userService;

	//授权
    ……

    //认证    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //在工厂中获取userMapper对象(或者使用自动注入),说是使用工厂的这种方式比较好??
        UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");

        String principal = (String) authenticationToken.getPrincipal();
        //根据用户名,获取到相应的User对象
        User user = userService.findByUserName(principal);

        if(user != null){
            return new SimpleAuthenticationInfo(user.getUsername(),
                    user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()),
                    this.getName());
        }

        return null;
    }
}
//xxxAware接口,可以在提供需要重写的方法中,通过参数的方式获取到相应的xxx对象
@Component
public class ApplicationContextUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    //根据bean的名字获取工厂中的bean对象
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}

6.6 进入后授权

  • 重写Realm中的doGetAuthorizationInfo授权方法
@Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取主身份信息(用户名)
        String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();

        //通过主身份,查询数据库,获取对应的角色、权限信息
        UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
        List<Role> roles = userService.findRolesByUserName(primaryPrincipal);
        if(!CollectionUtils.isEmpty(roles)){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role->simpleAuthorizationInfo.addRole(role.getRolename()));
            return simpleAuthorizationInfo;
        }
        return null;
    }
  • controller中进行判断(编程式、注解式)
    • 在前后端分离的项目中,标签式比较少用
@Controller
@RequestMapping("/order")
public class OrderController {

    @RequestMapping("/save")
//    @RequiresRoles("user")   //注解的方式(以角色为例),注意:没权限会抛异常
//    @RequiresPermissions("user:save:*")
    public String saveOrder(){

        //代码的方式(以角色为例)
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("admin")){
            System.out.println("保存订单信息!");
        }
        else{
            System.out.println("无权访问!");
        }

        return "redirect:/index";
    }
}

​ 这里是基于角色去简单判断授权的。至于权限表部分,其实差不多,也都是获取后添加到授权方法中返回的对象即可。

7. Shiro的缓存

​ 给用户的授权数据加上缓存,避免每次都需要调用Realm的doGetAuthorizationInfo方法判断权限(每次都需要去查询数据源),减轻DB的访问压力,同时提高系统查询效率。

​ Shiro提供了Cache Manager接口,来实现缓存功能。

7.1 EhCache

​ Shiro默认是实现EhCache,实现 本地缓存。(服务器重启,缓存则清空)

  • 引入EhCache依赖
<!--EhCache缓存-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>
  • 在自定义Realm中,开启缓存
 //开启缓存管理
realm.setCacheManager(new EhCacheManager());  //添加缓存管理器
realm.setCachingEnabled(true);                //开启全局缓存
realm.setAuthenticationCachingEnabled(true);  //开启认证缓存
realm.setAuthenticationCacheName("authenticationCache");    //设置认证缓存名
realm.setAuthorizationCachingEnabled(true);   //开启授权缓存
realm.setAuthorizationCacheName("authorizationCache");      //设置授权缓存名
  • 可以开启debug模式,看是否有SQL语句的日志输出。(只有第一次访问才有)

7.2 Redis

​ 结合Redis,实现 分布式缓存

  • 引入springboot整合redis的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 配置redis信息

    spring.redis.port=6379
    spring.redis.host=127.0.0.1
    spring.redis.database=0
    
  • 自定义Shiro缓存管理器(结合Redis)

public class RedisCacheManager implements CacheManager {

    //参数s:认证或者授权的名称
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new RedisCache<K, V>(s);
    }
}
  • 自定义RedisCache
public class RedisCache<K, V> implements Cache<K, V> {

    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String cacheName) {
        this.cacheName = cacheName;
    }

    //通过redis的hash数据类型,利用到了cacheName,方便之后管理
    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());        // 设置key的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());    //hash中的小key
        return redisTemplate;
    }

    @Override
    public V get(K k) throws CacheException {
        return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
        return null;    //put操作返回值意义不大
    }

    //用户退出时调用(注意:如果是最后一个用户,进行退出后,连同map都会被清除)
    @Override
    public V remove(K k) throws CacheException {
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
    }

    @Override
    public void clear() throws CacheException {
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }
}
  • 在自定义Realm中,设置成我们自定义的这个RedisCacheManager即可。
//开启缓存管理
realm.setCacheManager(new RedisCacheManager());  //添加缓存管理器
realm.setCachingEnabled(true);                //开启全局缓存
realm.setAuthenticationCachingEnabled(true);  //开启认证缓存
realm.setAuthenticationCacheName("authenticationCache");    //设置认证缓存名
realm.setAuthorizationCachingEnabled(true);   //开启授权缓存
realm.setAuthorizationCacheName("authorizationCache");      //设置授权缓存名
  • 注意,由于认证和授权都需要将信息缓存到Redis中,而在认证的时候添加了盐, ByteSource.Util.bytes(user.getSalt()。但是ByteSource没有办法序列化,所以需要自定义一个ByteSource,既要拥有ByteSource的功能,又要实现序列化接口

    //使用的代码是SimpleByteSource的源码,
    //只不过在其基础上,添加了无参构造,并把bytes全局变量去掉final。这是为了可以成功序列化
    public class MyByteSource implements Serializable, ByteSource {
    
        private byte[] bytes;
        private String cachedHex;
        private String cachedBase64;
    
        //无参构造器
        public MyByteSource(){
        }
    
        public MyByteSource(byte[] bytes) {
            this.bytes = bytes;
        }
    
        public MyByteSource(char[] chars) {
            this.bytes = CodecSupport.toBytes(chars);
        }
    
        public MyByteSource(String string) {
            this.bytes = CodecSupport.toBytes(string);
        }
    
        public MyByteSource(ByteSource source) {
            this.bytes = source.getBytes();
        }
    
        public MyByteSource(File file) {
            this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
        }
    
        public MyByteSource(InputStream stream) {
            this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
        }
    
        public static boolean isCompatible(Object o) {
            return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
        }
    
        public byte[] getBytes() {
            return this.bytes;
        }
    
        public boolean isEmpty() {
            return this.bytes == null || this.bytes.length == 0;
        }
    
        public String toHex() {
            if (this.cachedHex == null) {
                this.cachedHex = Hex.encodeToString(this.getBytes());
            }
    
            return this.cachedHex;
        }
    
        public String toBase64() {
            if (this.cachedBase64 == null) {
                this.cachedBase64 = Base64.encodeToString(this.getBytes());
            }
    
            return this.cachedBase64;
        }
    
        public String toString() {
            return this.toBase64();
        }
    
        public int hashCode() {
            return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
        }
    
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            } else if (o instanceof ByteSource) {
                ByteSource bs = (ByteSource)o;
                return Arrays.equals(this.getBytes(), bs.getBytes());
            } else {
                return false;
            }
        }
    
        private static final class BytesHelper extends CodecSupport {
            private BytesHelper() {
            }
    
            public byte[] getBytes(File file) {
                return this.toBytes(file);
            }
    
            public byte[] getBytes(InputStream stream) {
                return this.toBytes(stream);
            }
        }
    
    }
    
    • 然后我们自定义的Realm的验证方法,盐的传入,换成我们自定义的ByteSource

      new SimpleAuthenticationInfo(user.getUsername(),
                                   user.getPassword(),
                                   new MyByteSource(user.getSalt()),
                                   this.getName());
      

8. Shiro与Thymeleaf的结合(标签式)

8.1 引入扩展依赖

<!--Thymeleaf&Shiro-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

8.2 在html页面中引入命名空间

<html xmlns="http://www.w3.org/1999/xhtml" 
      xmlns="http://www.thymeleaf.org"
      xmlns="http://www.pollix.at/thymeleaf/shiro">

8.3 加入Shiro的方言配置,使标签生效

@Configuration
public class ShiroConfig{
    @Bean(name="shiroDialect")
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}

​ 至于标签的使用,这里就不列举了。

​ -----------END(待续)-------------


版权声明:本文为weixin_41896463原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。