Shiro

14 篇文章 0 订阅
1 篇文章 0 订阅

Shiro

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。

第一部分 Hello Shiro

1. 导入依赖
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.5.3</version>
</dependency>
2. 配置shiro.ini

shiro.ini用于存放临时数据,降低学习者成本(也就是模拟数据库数据)

[users]   // 注意格式 users表示用户数据列表
zhangsan=123
lisi=123456
3. 编写shiro代码认证
// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();

// 2设置凭证,也就是加载 shiro.ini配置文件
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

// 3.给安全工具设置安全管理器   安全工具为框架提供的工具帮助我们快速实现认证授权
SecurityUtils.setSecurityManager(securityManager);

// 4.通过安全工具获取主体(可以理解为登录用户的实体)
Subject subject = SecurityUtils.getSubject();

// 5.创建登录令牌信息
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123456");

try{
// 6.登录验证
    subject.login(token);
// 7.获取验证状态
    System.out.println(subject.isAuthenticated());
}catch (Exception e){
    e.printStackTrace();
}

第二部分 shiro核心类

1. SecurityManager

安全管理器,可以说是Shiro的心脏,所有的认证授权都会交给他,然后他再做处理

① 实现
DefaultSecurityManager securityManager = new DefaultSecurityManager();

一般情况下我们用这个实现类进行代码实现

② 设置安全数据

也就是我们数据库中存放的数据

数据写死,所以这里直接设置一个IniRealm

securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

数据库数据,不管怎样我们最后都要替换为数据库数据

securityManager.setRealm(new custom());

custom 为自己实现的对象,会在后续接触到,在这里面我们设置数据库的数据

2. SecurityUtils

框架提供的工具类,帮助我们快速实现登录认证

方法

① setSecurityManager

将我们定义好的SecurityManager交给他,毕竟核心是SecurityManager。

② 获取Subject

帮助我们获取一个基本是啥都没有的Subject

3. UsernamePasswordToken

创建一个登录令牌既token

UsernamePasswordToken token = new UsernamePasswordToken("lisi","123456"); 
4. Subject

认证主体,可以理解为登陆的用户,因为后续的所有于该用户相关的数据都会放在这里面

① 实现

通过框架提供的SecurityUtils直接获取

Subject subject = SecurityUtils.getSubject();
② 登录

算是核心方法了,完成用户认证

try{
    // 登录验证
    subject.login(token);
    // 获取验证状态
    System.out.println(subject.isAuthenticated());
}catch (UnknownAccountException e){
    e.printStackTrace();
    System.out.println("用户账号错误");
}catch (IncorrectCredentialsException e){
    System.out.println("用户认证凭证错误");
    e.printStackTrace();
}

认证之后的信息会存储在Subject中

如果认证失败会抛出错误异常,成功则不会抛出异常

第三部分 自定义身份认证

为什么要自定义?

从上述案例我们发现,用户数据是写死的,密码判断我们也没有参与

  1. 账号判断,我们默认使用的是IniRealm,是Shiro已经定义好的匹配方法,其中的数据也有我们写死,但实际上我们要是用数据数据进行判断,所以我们要自定义。
  2. 凭证判断(通常是指密码),默认Shiro通过断言方式比较,但是我们通常需要对密码需要加密,所以要自定义比较方法
1. 密码加密

加密方式通常有三种MD5、随机盐、hash三列,通常我们混合使用

① MD5
  1. 加密不可逆,加密回去就推不回来了
  2. MD5加密是将我们的明文加工成一串32位的十六进制数
② 随机盐

​ 就是在用户密码MD5之前随机添加几位数,防止用户密码太简单,被穷举出来

③ hash散列

​ 在随机盐以及MD5基础上在对密码进行散列,进一步加密密码

使用

shiro给我们提供了Md5Hash

// 直接使用方法
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex());

Md5Hash md5Hash2 = new Md5Hash("123","kjaspof");
System.out.println(md5Hash2.toHex());

Md5Hash md5Hash3 = new Md5Hash("123","kjaspof",1024);
System.out.println(md5Hash3.toHex());

// 参数一是 用户明文密码 
// 参数二是 随机盐字符串
// 参数三是 hash散列次数 一般是1024/2048

注:必须要使用Md5Hash的构造方法传值

2. 自定义身份认证

查看源码得知,账号匹配在AuthorizingRealm(类)一脉中的 doGetAuthenticationInfo 中进行

如果我们在编码时Realm设定为 IniRealm,那么账号匹配就是在IniRealm的doGetAuthenticationInfo方法中进行

所以要自定义账号匹配也就是认证,那就需要自定义个Realm继承AuthorizingRealm实现其中的 doGetAuthenticationInfo 方法

① 密码未加密
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {
    // 授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 参数就是我们subject.login(token)时传递的UsernamePasswordToken

        // 获取用户登录身份,一般是账号
        Object principal = authenticationToken.getPrincipal();
        // 获取用户登录凭证,一般是密码
        Object credentials = authenticationToken.getCredentials();

        // TODO:获取数据库数据 进行一系列操作

        // 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
        SimpleAuthenticationInfo authenticationInfo = null;
        authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "数据库密码", this.getName());
        return authenticationInfo;
    }
}

① 密码加密

密码进行加密,那第一部我们要告诉Shiro我们用的什么加密

查看源码得知,密码断言是在Realm的 CredentialsMatcher 属性中进行,那我们就要自定义一个CredentialsMatcher

// 自定义一个CredentialsMatcher 设置加密方法为md5
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
// 如果还使用了散列,那就要设置散列次数
credentialsMatcher.setHashIterations(1024);

// 将其设置到我们的Realm中
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(credentialsMatcher);

这样Shrio在进行密码断言的时候就会先对密码进行MD5加密或散列,再进行断言。并且兼容随机盐(需要其他操作)

如果我们还用了随机盐,就需要对我们自定义的Realm方法进行改变

package org.example;

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

public class CustomerRealm extends AuthorizingRealm {
    // 授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 参数就是我们subject.login(token)时传递的UsernamePasswordToken

        // 获取用户登录身份,一般是账号
        Object principal = authenticationToken.getPrincipal();
        // 获取用户登录凭证,一般是密码
        Object credentials = authenticationToken.getCredentials();

        // TODO:获取数据库数据 进行一系列操作

        // 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
        SimpleAuthenticationInfo authenticationInfo = null;
        authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "数据库密码", ByteSource.Util.bytes("随机盐字符串"), this.getName());
        return authenticationInfo;
    }
}

第四部分 自定义授权

授权在认证之后

也就是说subject通过认证之后开始对其拥有的权限进行操作

1. 授权(授予subject权限)

在自定义的Realm中的doGetAuthorizationInfo方法中进行

// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 用户凭证
    String userName = (String) principalCollection.getPrimaryPrincipal();
    // 授权对象
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    // 1.身份授权
    authorizationInfo.addRole("admin");
    authorizationInfo.addRole("user");

    // 2.资源权限授权
    authorizationInfo.addStringPermission("user:*:01");
    authorizationInfo.addStringPermission("product:create:*");
    return authorizationInfo;
}

授权方式:

  1. 角色授权:admin,user等
  2. 资源访问授权 product:create:* 分为三级,从左到右一次类推,“*”代表全部

不同的授权方式,使用的方法也不尽相同

2. 权限认证

认证权限,判断该用户是否拥有某一个角色或者资源访问权限

方法类似身份认证,返回值为boolean

// 1.身份授权
// 单一身份
System.out.println(subject.hasRole("admin"));
// 多个身份
System.out.println(subject.hasAllRoles(Arrays.asList("admin","user")));
// 任一身份
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user"));
for (boolean boo : booleans) {
    System.out.println(boo);
}
// 2.资源授权
// 单一资源权限
System.out.println(subject.isPermitted("user:save:01"));
// 多个资源权限
System.out.println(subject.isPermittedAll("user:save:01","product:update:*"));
// 任一资源权限
boolean[] booleans = subject.isPermitted("user:save:01", "product:update:*");
for (boolean b : booleans) {
    System.out.println(b);
}

第五部分 认证流程源码解析

1. 执行流程

流程前提:

// 默认的安全管理器  
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 自定义的CustomeRealm
securityManager.setRealm(new CustomerRealm());
// 入口
subject.login(token);

认证流程追踪subject.login(token)方法

在这里插入图片描述

  • Subject调用自己的 login(AuthenticationToken token) 方法进行认证登录

  • login方法调用DefaultSecurityManager的login(Subject subject, AuthenticationToken token) 方法进行认证

  • login方法调用父类AuthenticatingSecurityManagerd的authenticate(AuthenticationToken token)方法进行认证

  • authenticate方法调用自身的realm认证管理器ModularRealmAuthenticator的authenticate(token)方法进行认证

  • authenticate:

    • 断言是否存在Realm
    • 获取所有的realm
    • 根据realm个数使用不同的逻辑进行认证
      • 如果realm为一个则调用自身单realm方法进行认证
      • 如果realm为多个则调用,多realm逻辑进行认证,多realm下会使用到realm的认证策略。shiro提供三种认证策略
        • AllSuccessfulStrategy,这个表示所有的 Realm 都认证成功才算认证成功
        • AtLeastOneSuccessfulStrategy,这个表示只要有一个 Realm 认证成功就算认证成功,默认策略
        • FirstSuccessfulStrategy,这个表示只要第一个 Realm 认证成功,就算认证成功
  • 调用realm的doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法进行认证以及后续处理

2. 多Realm配置

比如说:现在我们需要一个adminRealm来处理admin登录认证;userRealm来处理user登录认证,那么我们就需要进行多Realm配置

通过分析认证流程可得,Realm的操作是在ModularRealmAuthenticator的authenticate方法中 所以我们若是需要对realm进行操作,那么就需要继承ModularRealmAuthenticator重写authenticate方法。

实现思路

  • 编写adminRealm、userRealm
  • 扩展UsernamePasswordToken
  • 重写authenticate方法
  • 串联自动义类
  • 测试
① 编写adminRealm、userRealm

adminRealm

package com.llz;

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

public class AdminRealm extends AuthorizingRealm {

    public String getName(){
        return "admin";
    }
    // 授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 参数就是我们subject.login(token)时传递的UsernamePasswordToken

		...

        // 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
        SimpleAuthenticationInfo authenticationInfo = null;
        authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "123456", this.getName());
        return authenticationInfo;
    }
}

userRealm

package com.llz;

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

public class UserRealm extends AuthorizingRealm {

    public String getName(){
        return "user";
    }
    // 授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    // 认证方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 参数就是我们subject.login(token)时传递的UsernamePasswordToken

		...

        // 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
        SimpleAuthenticationInfo authenticationInfo = null;
        authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "123", this.getName());
        return authenticationInfo;
    }
}

② 扩展UsernamePasswordToken

为什么要扩展UsernamePasswordToken

在认证管理器的authenticate方法中,需要通过标记将realm分开,所以我们需要token携带登录类型。

只扩充一个标记

package com.llz;

import org.apache.shiro.authc.UsernamePasswordToken;

public class CustomerUsernamePasswordToken extends UsernamePasswordToken {

    private final String TYPE_NAME;

    public CustomerUsernamePasswordToken(String username,String password,String type_name) {
        super(username,password);
        TYPE_NAME = type_name;
    }

    public String getTYPE_NAME() {
        return TYPE_NAME;
    }
}

③ 重写authenticate方法
package com.llz;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import java.util.ArrayList;
import java.util.Collection;

public class CustomerModularRealmAuthenticator extends ModularRealmAuthenticator {

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        /**
         * 开始修改
         * 1. 获取token中的类型
         * 2. 创建realm集合
         * 3. 遍历所有的realm,找到类型批量的realm
         * 4. 根据realm的个数编写返回值
         */

        // 1.获取token中的类型
        CustomerUsernamePasswordToken token = (CustomerUsernamePasswordToken) authenticationToken;
        String typeName = token.getTYPE_NAME();

        // 2. 创建realm集合
        Collection<Realm> realmList = new ArrayList<>();

        // 3. 创建符合请求类型的realm
        for (Realm realm : realms) {
            if (realm.getName().equals(typeName)){
                realmList.add(realm);
            }
        }

        // 4. 根据realm的个数编写返回值
        return realmList.size() == 1 ? this.doSingleRealmAuthentication((Realm)realmList.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realmList, authenticationToken);
    }

}

④ 串联自动义类
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.Subject;

import java.util.ArrayList;

public class TestDome01 {

    public static void main(String[] args) {
        // 1.创建默认安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        // 2. 设置自定义认证器
        CustomerModularRealmAuthenticator realmAuthenticator = new CustomerModularRealmAuthenticator();
        // 设置多realm认证策略 当前环境为单realm,没必要设置
        // realmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
        securityManager.setAuthenticator(realmAuthenticator);

        // 3. 设置Realm
        ArrayList<Realm> realms = new ArrayList<>();
        // admin类型
        realms.add(new AdminRealm());
        // user类型
        realms.add(new UserRealm());
        securityManager.setRealms(realms);

        // 4. 给安全工具设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);

        // 5. 通过安全工具获取主体(可以理解为登录用户的实体)
        Subject subject = SecurityUtils.getSubject();

        // 6.创建自定义登录令牌信息,传入登录类型
        CustomerUsernamePasswordToken admin = new CustomerUsernamePasswordToken("zhangsan", "123456", "admin");
        CustomerUsernamePasswordToken user = new CustomerUsernamePasswordToken("zhangsan", "1234", "user");

        try {
            // 7.登录验证
            subject.login(admin);
            // 8.获取验证状态
            System.out.println(subject.isAuthenticated());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
⑤ 测试
  • 单Realm测试
    • 也就是每个类型的Realm只有一个
  • 多Realm测试
    • 传入多个admin类型的Realm测试(验证密码不同)
    • 修改认证策略进行测试

第六部分 Spring Boot整合

SpringBoot+Shiro整合案例

0. 实现步骤
  1. 导入依赖
  2. 配置MyBatis-Plus配置
  3. Shiro配置(自定以Realm、设置安全管理器、设置资源过滤器)
  4. 编写实体类User以及登陆Controller进行搭建环境测试
  5. Utils准备(随即盐获取,以及ApplicationContext的Bean获取工具类)
  6. 完善注册操作
  7. 完善认证操作
  8. 完善授权操作
  9. 开启shiro缓存-EhCache
  10. 整合Redis缓存
1. 导入依赖
② 实现

导入依赖

<!--spring Web启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf模板依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--lombok依赖-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!--测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--shiro依赖-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
</dependency>

<!--持久层相关依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.8</version>
</dependency>
2. MyBatis-Plus配置
spring:
  thymeleaf:
    prefix: classpath:/templates/
    check-template-location: true
    cache: false
    suffix: .html
    mode: HTML5
  datasource:
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test01?serverTimezone=GMT%2B8
    type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.xspringboot.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. Shiro配置
① 自定义Realm
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权");
  		//TODO:授权(使用数据库数据)
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //TODO:认证(使用数据库数据)
        System.out.println("简单认证");
     
        return null;
    }
}
② Shiro配置
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class ShiroConfig {

    // 把自定义的Realm加入容器
    @Bean
    public Realm realm(){
        return new CustomerRealm();
    }

    // 创建适用于Web的 SecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    // 创建过滤器 这是web必备的 在这里设置那些资源需要拦截(还可以设置拦截类型)
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        HashMap<String, String> hashMap = new HashMap<>();
        //hashMap.put("/index","anon");
        hashMap.put("/**","authc");
        factoryBean.setFilterChainDefinitionMap(hashMap);
        factoryBean.setLoginUrl("/login");
        return factoryBean;
    }

}
4. 初步调试

在这里调试系统是否搭建成功,能否链接到数据库什么的

① User

根据数据表进行实体类编写,省略User相关的service、mapper、mapper.xml代码(基本的增删改查)

@Data
public class User {
    private Long id;
    // name存放随机盐 命名错了
    private String name;
    private String account;
    private String password;
    private LocalDateTime createTime;
    private Integer stateFlag;
// 省略构造方法
}
② 测试controller
@RequestMapping("/index")
public ModelAndView index(ModelAndView modelAndView,String username,String password){
   
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(new UsernamePasswordToken(username,password));
    }catch (UnknownAccountException e){
        System.out.println("账号错误!!!");
    }catch (IncorrectCredentialsException e){
        System.out.println("密码错误!!!");
    }
    modelAndView.setViewName("index");
    return modelAndView;
}

// 因为Realm我们并没有配置所以一定报错,只要打印出认证就算成功  页面省略,访问任意路径都会跳转到login页面需要认证才能访问index

调通之后可进行下一步

5. 工具类准备
① Utils

获取随机盐

@Component
public class Utils {

     // 将散列次数定义为常量
    public static final int HASH_CODE = 1024;

    public static String getSalt(int i){
        char[] charArray = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+".toCharArray();
        StringBuilder stringBuffer = new StringBuilder();
        for (int j = 0; j < i; j++) {
            stringBuffer.append(charArray[new Random().nextInt(charArray.length)]);
        }
        return stringBuffer.toString();
    }

}
② ApplicationContextUtils
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
	// 用来获取指定的Bean
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }
}
6. 完善 注册操作
① 注册
@RequestMapping("/register")
public ModelAndView register(ModelAndView modelAndView,String account,String password){
    // 获取随机盐
    String salt = Utils.getSalt(4);
    // 加密密码
    Md5Hash md5Hash = new Md5Hash(password, salt,Utils.HASH_CODE);
    // 保存用户信息
    service.save(new User(account,md5Hash.toHex(),salt));
    modelAndView.setViewName("login");
    return modelAndView;
}
② Realm设置加密方法

也就是修改ShiroConfig中的Realm

@Bean
public Realm realm(){
    // 设置加密方法
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    hashedCredentialsMatcher.setHashIterations(Utils.HASH_CODE);
    
    CustomerRealm customerRealm = new CustomerRealm();
    customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
    return customerRealm;
}
7. 完善认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("认证开始");
    // 获取账号
    String account = (String) authenticationToken.getPrincipal();
    // 获取UserService
    UserService userServiceImpl = (UserService)ApplicationContextUtils.getBean("userServiceImpl");
    // 获取User
    User user = userServiceImpl.getUserByAccount(account);
   // 认证
        if (user != null){
            return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(), ByteSource.Util.bytes(user.getName()),this.getName());
        }
    return null;
}
8. 完善授权方法
① 设置用户
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("授权");
    // 获取用户账号
    String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
    // 获取UserService
    UserService userServiceImpl = (UserService)ApplicationContextUtils.getBean("userServiceImpl");
    // 获取用户权限信息
    List<Permission> permissionList = userServiceImpl.getPermissionByAccount(primaryPrincipal);
    // 设置权限
    if (!permissionList.isEmpty()){
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        permissionList.forEach(po ->{
            simpleAuthorizationInfo.addStringPermission(po.getValue());
        });
        return simpleAuthorizationInfo;
    }
    return null;
}
② 设置资源访问权限-subject

使用subject进行资源权限确认

@RequestMapping("/list")
public String list(){
    Subject subject = SecurityUtils.getSubject();
    boolean permitted = subject.isPermitted("llz:product:list");
    if (permitted){
        ...
    }
    return "商品列表";
}
② 设置资源访问权限-注解

使用注解方式设置资源访问权限

@RequestMapping("/save")
@RequiresPermissions("llz:product:save")  // 需要开启注解方式
public String save(){
    return "商品保存";
}

开启注解方式

在ShiroConfig中配置

/**
 * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
 */
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
    return authorizationAttributeSourceAdvisor;
}

/**
 * 配置异常处理   必须是HandlerExceptionResolver接口实现类
 * @return
 */
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
    SimpleMappingExceptionResolver  simpleMappingExceptionResolver =
        new SimpleMappingExceptionResolver();
    Properties properties = new Properties();
    //配置当出现未授权异常时,跳转地址,只依赖shiro的配置不会跳转
    properties.put("org.apache.shiro.authz.UnauthorizedException","/html/unauthorized.html");
    simpleMappingExceptionResolver.setExceptionMappings(properties);
    return simpleMappingExceptionResolver;
}
9. 开启Shiro - EhCache缓存

EhCache缓存特点:程序内缓存,程序终止缓存消失

① 引入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>
② 开启缓存

为Realm设置开启缓存

@Bean
public Realm realm(){
    // 设置加密方法
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    hashedCredentialsMatcher.setHashIterations(Utils.HASH_CODE);
    CustomerRealm customerRealm = new CustomerRealm();
    customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

    // 开启shiroEhCacheManager
    customerRealm.setCacheManager(new EhCacheManager());  // 配置缓存管理器
    customerRealm.setCachingEnabled(true);  // 启用全局缓存
    customerRealm.setAuthenticationCachingEnabled(true);  // 启用登录验证缓存
    customerRealm.setAuthorizationCachingEnabled(true);  // 启用授权认证缓存
    customerRealm.setAuthenticationCacheName("authentication_cache");  // 为登录验证缓存命名
    customerRealm.setAuthorizationCacheName("authorization_cache");  // 为授权认证缓存命名

    return customerRealm;
}
10. redis缓存

redis缓存不会因程序终结而消失

① redis配置

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 版本默认和spring一致 也可以自己指定版本

yml配置

spring: 
    redis:
      host: localhost
      port: 6379
      #    使用的数据库
      database: 0
      #    链接超时时间
      #    connect-timeout:
      lettuce:
        pool:
          #        最大连接数
          max-active: 20
          #        最大等待时间 负数为永久
          max-wait: -1
          #        最大空闲连接数
          max-idle: 5
          #        最小空闲连接数
          min-idle: 0
② 定义 RedisCacheManager

通过代码发现,shiro开启缓存使用的是一个shiroEhCacheManager,所以要开启redis缓存就要创建自己的RedisCacheManager

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

// 实现shior指定的缓存管理器接口
public class ShiroCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
		// 如果是认证的缓存 s=authentication_cache 
        // 如果是授权的缓存 s=authorization_cache
        return new RedisCache<K,V>(s);
    }
}

通过返回值我们得知,我们需要返回一个实现Cache<K, V>接口的类

③ RedisCache<K, V>

我们需要shiro缓存规定的 Cache<K, V>接口

import com.example.xspringboot.common.ApplicationContextUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Collection;
import java.util.Set;

// 自定义redis缓存器 最主要的就是get put remove
public class RedisCache<K, V> implements Cache<K, V> {

    // 缓存名称,我们将认证和授权名称作为参数传递,方便后续指定缓存key
    private final String cacheName;

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

    // 当触发认证或授权 会自动调用get方法 参数k就是我们的principal(就是账号了)
    @Override
    public V get(K k) throws CacheException {
        RedisTemplate<String, V> template = getTemplate();
        template.setKeySerializer(new StringRedisSerializer());
        return (V) template.opsForHash().get(cacheName, k.toString());
    }

    // 触发缓存时 会使用put方法进行数据存储 所以我们要在这个方法内完成redis数据添加,这样就可以get到了
    @Override
    public V put(K k, V v) throws CacheException {
        // k就是我们的账号 v就是我们要缓存的数据(认证时是密码,授权时是权限列表)
        RedisTemplate<String, V> template = getTemplate();
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.opsForHash().put(cacheName, k.toString(), v);
        return null;
    }

 	// 当使用 subject.logout(); 方法时会调用该方法
    @Override
    public V remove(K k) throws CacheException {
        RedisTemplate<String, V> template = getTemplate();
        template.opsForHash().delete(cacheName,k.toString());
        return null;
    }

    // 清除指定key...
    @Override
    public void clear() throws CacheException {
        RedisTemplate<String, V> template = getTemplate();
        template.delete(cacheName);
    }

    @Override
    public int size() {
        RedisTemplate<String, V> template = getTemplate();
        return template.opsForHash().size(cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
        RedisTemplate<String, V> template = getTemplate();
        return (Set<K>) template.opsForHash().keys(cacheName);
    }

    @Override
    public Collection<V> values() {
        RedisTemplate<String, V> template = getTemplate();
        return (Collection<V>) template.opsForHash().values(cacheName);
    }

    // 多个地方使用RedisTemplate所以封装为一个方法 当然也可以象cacheName那样作为一个属性,这样就不用写这些了
    private RedisTemplate<String, V> getTemplate() {
        return (RedisTemplate<String, V>) ApplicationContextUtils.getBean("redisTemplate");
    }

}

为什么要使用Hash呢?

  1. 我们要进行 认证和授权 的缓存,但我们的k又是相同的,所以必须用一个更大的k来区分他是缓存还是认证
  2. 我们要缓存的用户不止一个,是一个列表
  3. 综上所述我们要使用hash,大k就是cacheName,这也是我们用他的原因
④ ByteSource序列化处理

修改原因

  1. 因为密码加密使用了 ByteSource进行随机盐转换
  2. redis保存需要序列化,官方ByteSource没有序列化
  3. 所以缓存时会出异常,所以需要我们自定义ByteSource

代码实现

就一模板也不用记

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

public class MyByteSource implements ByteSource, Serializable {

    private static final long serialVersionUID = 5175082362119580768L;

    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 MyDataSource.BytesHelper()).getBytes(file);
    }

    public MyByteSource(InputStream stream) {
        this.bytes = (new MyDataSource.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 void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }


    @Override
    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }
        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    @Override
    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的地方改为自己的ByteSource

修改处在认证方法的最后一步

...
     return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword()
                                         ,new MyByteSource(user.getName()).bytes()   // 修改
                                         ,this.getName());    
    
...

null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}

    return this.cachedBase64;
}

@Override
public boolean isEmpty() {
    return this.bytes == null || this.bytes.length == 0;
}

@Override
public String toString() {
    return this.toBase64();
}

@Override
public int hashCode() {
    return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}

@Override
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的地方改为自己的ByteSource**

修改处在认证方法的最后一步

```java
...
     return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword()
                                         ,new MyByteSource(user.getName()).bytes()   // 修改
                                         ,this.getName());    
    
...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值