shiro项目爆肝

shiro项目爆肝

作者:不染心

时间:2021/8/2

项目地址: https://download.csdn.net/download/qq_38234785/20679811

一、概念介绍

(一)权限管理

1、权限管理

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

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

2、身份认证

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

3、授权

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

(二)Shiro

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

2、Shiro核心组件

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

3、Shiro认证

image-20200521204452288

4、Shiro授权

image-20200521230705964


二、基础项目构建

💥 实现功能:

  • MySQL数据库用户信息、权限存储
  • 注册、认证、授权
    • 注册:哈希(密码+salt)存储;
    • 认证:用户登陆,赋予角色;
    • 授权:用户登陆,赋予权限列表;
  • 设定权限访问API
    • 依据授权列表,判断该用户是否有访问API的权限;

(一)数据库

也可直接下载项目,里面有sql文件

🍃 ​ user

user_iduser_nameuser_roleuser_passsaltuser_telupdate_userupdate_time
18admintestadmin61ca717a947acb9ae594d41188a77176NF4&*7DT19987654376zhangsna7/22/2021 15:39:26
19usertestuserd246c655fbb39fde25f39b9282c2cda0Olr@fnwY19987654376zhangsna7/22/2021 16:37:03

🍃 role

role_idrole_namerole_coderole_description
1系统管理员admin系统管理员
10普通用户user普通用户

🍃 permission

permission_idpermission_namepermission_codepermission_url
1用户管理user/user
2用户新增useradd/user/add
3用户修改userupdate/user/update
4用户删除userdel/user/del
5权限管理permis/permis
6权限查看permislist/permis/list
7用户权限testusertest/user/test

🍃 role_permission

role_idpermission_id
11
12
13
14
101
107

(二)项目结构


(三)代码编写

1、配置文件

🍃pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dyl</groupId>
    <artifactId>shiroCSDN</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiroCSDNpro</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <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.6.0</version>
        </dependency>
        <!--mysql connector 根据自己mysql版本号选择驱动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>
        <!--mysql connector 根据自己mysql版本号选择驱动-->
        <!--参数校验依赖-->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.1.7.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!--参数校验依赖-->
        <!--ehcache缓存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.5.3</version>
        </dependency>
        <!--ehcache缓存-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

🍃resources > application.yml

spring:
  datasource:
    name: shirocsdn
    url: jdbc:mysql://localhost:3306/shirocsdn #url
    username: root  #用户名
    password: 123456  #密码
    driver-class-name: com.mysql.jdbc.Driver  #数据库链接驱动


mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.domainsysadmin.mapper

2、公共模块

🍃com.dyl.shirocsdn.utils.ApplicationContextUtils.java :获取容器中的bean

package com.dyl.shirocsdn.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextUtils 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);
    }
}

🍃com.dyl.shirocsdn.utils.PatternContents.java :参数校验正则匹配和异常信息

package com.dyl.shirocsdn.utils;

/**
 * 公共参数校验
 */
public class PatternContents {

    // 手机号
    public static final String TEL_TYPE = "^1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}$";
    public static final String TEL_DES = "电话号码不合法";

    // email
    public static final String EMAIL_TYPE = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$";
    public static final String EMAIL_DES = "Email不合法";

    // 身份证号码
    public static final String IDCARD_TYPE = "^\\d{15}|\\d{18}$";
    public static final String IDCARD_DES = "身份证不合法";

    // 中文名字
    public static final String CHINESE_NAME_TPYE = "^[\\u4e00-\\u9fa5]{2,}$";
    public static final String CHINESE_NAME_DES = "长度大于2的中文名字";

    /**
     * ========= user ===============
     */
    // 账户名称
    public static final String SYSUSER_NAME_TYPE = "^.{8,}$";
    public static final String SYSUSER_NAME_DES = "用户名称长度应大于8";

    // 账户密码
    public static final String USER_PASS_TYPE = "^(\\w){6,20}$";
    public static final String USER_PASS_DES = "用户密码长度应大于6";


    /**
     * ========= role ===============
     */
    public static final String ROLE_CODE = "^[A-Za-z]{2,}$";
    public static final String ROLE_DESCRIPTION = "^.{2,}$";

}

🍃com.dyl.shirocsdn.utils.SaltUtils.java :生成用户哈希用户密码的随即盐

package com.dyl.shirocsdn.utils;

import java.util.Random;

public class SaltUtils {
    public static String getSalt(int n){
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".toCharArray();
        StringBuilder stringBuilder = new StringBuilder();
        for(int i = 0; i < n; i++){
          char aChar = chars[new Random().nextInt(chars.length)];
          stringBuilder.append(aChar);
        }
        return stringBuilder.toString();
    }

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

3、实体类

🍃com.dyl.shirocsdn.entity.User.java

package com.dyl.shirocsdn.entity;

import com.dyl.shirocsdn.utils.PatternContents;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.validation.constraints.Pattern;


@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int user_id;
    @Pattern(regexp = PatternContents.SYSUSER_NAME_TYPE, message = PatternContents.SYSUSER_NAME_DES)
    private String user_name;

    private String user_role;

    @Pattern(regexp = PatternContents.USER_PASS_TYPE, message = PatternContents.USER_PASS_DES)
    private String user_pass;
    private String salt;

    @Pattern(regexp = PatternContents.TEL_TYPE, message = PatternContents.TEL_DES)
    private String user_tel;

    private String update_user;
    private String update_time;


    public User(String user_name, String user_role, String user_pass, String user_tel){
        this.user_name = user_name;
        this.user_role = user_role;
        this.user_pass = user_pass;
        this.user_tel = user_tel;
        this.update_user = "system";
    }
}

🍃com.dyl.shirocsdn.entity.ReturnEntity.java :返回实体

package com.dyl.shirocsdn.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ReturnEntity {
    private int code;
    private String msg;
    private String data;
}

4、Shiro层

🍃com.dyl.shirocsdn.shiro.realms.CustomerRealm.java :自定义realm,其中功能包括:认证、授权

package com.dyl.shirocsdn.shiro.realms;



import com.dyl.shirocsdn.entity.User;
import com.dyl.shirocsdn.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author dyl
 */
@Slf4j
public class CustomerRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能进入这里说明用户已经通过验证了
        log.info("====授权开始===");
        String userName = (String)principalCollection.getPrimaryPrincipal();
        User sysUser = userService.selectUserByName(userName);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String user_role = sysUser.getUser_role();
        simpleAuthorizationInfo.addRole(user_role);
        List<String> permisList = userService.selectPermisByRole(user_role);
        log.info( userName+ "的权限有:" + permisList);

        for(String permission: permisList){
            simpleAuthorizationInfo.addStringPermission(permission);
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户输入的账户
        String user_name = (String) authenticationToken.getPrincipal();
        // 通过username从数据库中查找 UserInfo 对象
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userService.selectUserByName(user_name);
        if(user != null){
            return new SimpleAuthenticationInfo(user.getUser_name(), user.getUser_pass(), ByteSource.Util.bytes(user.getSalt()), this.getName());
        }
        return null;
    }
}

🍃com.dyl.shirocsdn.shiro.config.ShiroConfig.java :shiro配置文件

package com.dyl.shirocsdn.config;



import com.dyl.shirocsdn.shiro.realms.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.HashMap;
import java.util.Map;

/**
 * 用来整合shiro框架相关的配置类
 * @Ling https://zhuanlan.zhihu.com/p/354205960
 * @author dyl
 */
@Configuration
public class ShiroConfig {
    /**
     * 创建shiroFilter,负责拦截所有请求
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //配置系统受限资源
        //配置系统的公共资源
        Map<String, String> map = new HashMap<>();
        //请求这个资源需要认证和授权
        map.put("/api/user/login", "anon");
        map.put("/api/user/register", "anon");
        map.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        //默认认证界面路径(可不写)
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        return shiroFilterFactoryBean;
    }

    /**
     * 创建安全管理器
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //给安全管理器设置
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 创建自定义Realm
     * @return
     */
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //修改凭证校验匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置加密算法为md5
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        //设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        //开启cache缓存管理, 将自定义的RedisCacheManager注入进来
//        customerRealm.setCacheManager(new RedisCacheManager());
        // 使用ehcache做本地缓存
         customerRealm.setCacheManager(new EhCacheManager());
        //开启全局缓存
        customerRealm.setCachingEnabled(true);
        //开启认证缓存
        customerRealm.setAuthenticationCachingEnabled(true);
        //开启授权缓存
        customerRealm.setAuthorizationCachingEnabled(true);
        // 认证name
        customerRealm.setAuthenticationCacheName("authenticationCache");
        // 授权name
        customerRealm.setAuthorizationCacheName("authorizationCache");
        return customerRealm;
    }

    //注入权限管理
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }
}


5、Mapper层

🍃resources.mapper.UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.dyl.shirocsdn.mapper.UserMapper">
    <!--查询-->
    <select id="selectAllUser" resultType="com.dyl.shirocsdn.entity.User" parameterType="com.dyl.shirocsdn.entity.User">
        select * from user
    </select>

    <!--增-->
    <insert id="addUser" parameterType="com.dyl.shirocsdn.entity.User">
        insert into user(user_name, user_role, user_pass, salt, user_tel, update_user, update_time) VALUE(#{user_name}, #{user_role}, #{user_pass}, #{salt},  #{user_tel}, #{update_user}, #{update_time})
    </insert>

    <!--查询-->
    <select id="selectUserByName" resultType="com.dyl.shirocsdn.entity.User" parameterType="String">
        select * from user where user_name = #{user_name}
    </select>

    <select id="selectPermisByRole" resultType="String" parameterType="String">
        SELECT permission_code FROM `permission` as t1
        INNER JOIN role_permission as t2 on t1.permission_id = t2.permission_id
        INNER JOIN role as t3 on t3.role_id = t2.role_id
        where role_code = #{role};
    </select>
</mapper>

🍃com.dyl.shirocsdn.shiro.mapper.UserMapper.java

package com.dyl.shirocsdn.mapper;

import com.dyl.shirocsdn.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * @author Administrator
 */
@Mapper
public interface UserMapper {

    List<User> selectAllUser();

    int addUser(User user);

    User selectUserByName(String user_name);

    List<String> selectPermisByRole(String role);
}


6、Service层

🍃com.dyl.shirocsdn.shiro.service.UserService.java

package com.dyl.shirocsdn.service;

import com.dyl.shirocsdn.entity.ReturnEntity;
import com.dyl.shirocsdn.entity.User;
import org.apache.shiro.authz.annotation.RequiresPermissions;

import java.util.List;

/**
 * @author dyl
 */
public interface UserService {


    List<User> selectAllUser();

    ReturnEntity addUser(User user);

    ReturnEntity loginUser(String user_name, String user_pass);

    ReturnEntity userLoginOut();

    User selectUserByName(String userName);

    List<String> selectPermisByRole(String role);
}

🍃com.dyl.shirocsdn.shiro.service.impl.UserServiceImpl.java

package com.dyl.shirocsdn.service.impl;

import com.dyl.shirocsdn.entity.ReturnEntity;
import com.dyl.shirocsdn.entity.User;
import com.dyl.shirocsdn.mapper.UserMapper;
import com.dyl.shirocsdn.service.UserService;
import com.dyl.shirocsdn.utils.SaltUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author dyl
 * @description: 用户服务
 * @date 2021/7/30 0030 下午 16:31
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Override
    public List<User> selectAllUser() {
        return userMapper.selectAllUser();
    }

    /**
     * 添加用户
     * @param user
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ReturnEntity addUser(User user) {
        int addCount;
        log.info("添加用户为:" + user);
        String salt = SaltUtils.getSalt(8);
        user.setSalt(salt);

        Md5Hash md5Hash = new Md5Hash(user.getUser_pass(), salt, 1024);
        user.setUser_pass(md5Hash.toHex());
        addCount = userMapper.addUser(user);
        return addCount > 0
                ? new ReturnEntity(200, "创建用户成功", "")
                : new ReturnEntity(500, "创建用户失败", "");
    }

    /**
     * 登陆
     * @param user_name
     * @param user_pass
     * @return
     */

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ReturnEntity loginUser(String user_name, String user_pass) {
        Subject subject = SecurityUtils.getSubject();
        subject.login(new UsernamePasswordToken(user_name, user_pass));
        if(subject.isAuthenticated()) {
            log.info("{} 认证成功", user_name);
//            subject.checkPermission("userfjkdal");
//            subject.checkRole("admin");
        }
        return subject.isAuthenticated()
                ? new ReturnEntity(200, "OK", "登陆授权成功")
                : new ReturnEntity(500, "ERROR", "登陆授权成功");
    }

    @Override
    public ReturnEntity userLoginOut() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return new ReturnEntity(200, "OK", "登出成功");
    }

    @Override
    public User selectUserByName(String user_name) {
        return userMapper.selectUserByName(user_name);
    }

    @Override
    public List<String> selectPermisByRole(String role) {
        return userMapper.selectPermisByRole(role);
    }
}


7、Controller层

🍃com.dyl.shirocsdn.shiro.controller.SourceController.java

package com.dyl.shirocsdn.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author dyl
 * @description:
 * @date 2021/7/30 0030 下午 21:59
 */
@Slf4j
@RestController
@RequestMapping("/api/res")
public class SourceController {

    @RequiresPermissions("user")
    @GetMapping("/sourceA")
    public String sourceA(){
        return "sourceA";
    }

    @RequiresPermissions("useradd")
    @GetMapping("/sourceB")
    public String sourceB(){
        return "sourceB";
    }
}

🍃com.dyl.shirocsdn.shiro.controller.UserController.java

package com.dyl.shirocsdn.controller;

import com.dyl.shirocsdn.entity.ReturnEntity;
import com.dyl.shirocsdn.entity.User;
import com.dyl.shirocsdn.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.sql.Timestamp;
import java.util.List;

/**
 * @author dyl
 * @description:
 * @date 2021/7/30 0030 下午 21:59
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    UserService userService;


    @GetMapping("/userlist")
    public List<User> userList(){
        return userService.selectAllUser();
    }


    @PostMapping("/register")
    public ReturnEntity addUser(@Validated User user) throws Exception {
        user.setUpdate_time(new Timestamp(System.currentTimeMillis()) + "");
        return userService.addUser(user);
    }


    @PostMapping("/login")
    public ReturnEntity loginUser(String user_name, String user_pass) throws Exception {
        return userService.loginUser(user_name, user_pass);
    }


    @GetMapping("/loginout")
    public ReturnEntity loginOut(){
        return userService.userLoginOut();
    }
}

(四)测试

1、注册测试

💥 该测试目的:测试参数校验、注册接口

测试一:

POST: http://localhost:8080/api/user/register?user_name=usertest2&user_role=user&user_pass=123456&user_tel=1217059

user_name=usertest2

user_role=user

user_pass=123456

user_tel=1217059

result:说明参数校验模块是起作用的

Field error in object 'user' on field 'user_tel': rejected value [1217059]; codes [Pattern.user.user_tel,Pattern.user_tel,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.user_tel,user_tel]; arguments []; default message [user_tel],[Ljavax.validation.constraints.Pattern$Flag;@27a34635,^1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}$]; default message [电话号码不合法]]

测试二:

POST: http://localhost:8080/api/user/register?user_name=usertest2&user_role=user&user_pass=123456&user_tel=1217059

user_name=usertest2

user_role=user

user_pass=123456

user_tel=17754017055

result

{
    "code": 200,
    "msg": "创建用户成功",
    "data": ""
}

2、登陆认证测试

💥 该测试目的:测试认证接口正确与否

测试一:

POST: http://localhost:8080/api/user/login?user_name=usertest&user_pass=123456

user_name=usertest

user_pass=123456

result

{
    "code": 200,
    "msg": "OK",
    "data": "登陆授权成功"
}

3、授权测试

💥 该测试目的:测试授权模块能够从数据库中拿到用户权限,并查看根据权限访问接口功能正确与否

两个资源测试接口:

@RequestMapping("/api/res")
public class SourceController {

    @RequiresPermissions("user")
    @GetMapping("/sourceA")
    public String sourceA(){
        return "sourceA";
    }

    @RequiresPermissions("useradd")
    @GetMapping("/sourceB")
    public String sourceB(){
        return "sourceB";
    }
}

**测试一:**在2步骤登陆之后,再访问sourceA接口测试授权

GET: http://localhost:8080/api/res/sourceA

result

sourceA

**测试二:**访问sourceB接口测试授权,

GET: http://localhost:8080/api/res/sourceB

result

{
    "timestamp": "2021-08-01T02:21:07.040+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/res/sourceB"
}

分析:

可以看出/sourceB没有权限useradd,控制台也会给出无权限错误。

console

2021-08-01 10:21:03.649  INFO 7600 --- [nio-8080-exec-4] c.d.s.shiro.realms.CustomerRealm         : usertest的权限有:[user, usertest]

org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.lang.String com.dyl.shirocsdn.controller.SourceController.sourceB()

4、缓存测试

💥 该测试目的:查看缓存是否生效

测试一:连续点击三次登陆,查看控制台输出情况

POST: http://localhost:8080/api/user/login?user_name=usertest&user_pass=123456

user_name=usertest

user_pass=123456

console

2021-08-01 10:29:46.468  INFO 2364 --- [nio-8080-exec-2] o.a.shiro.cache.ehcache.EhCacheManager   : Cache with name 'authenticationCache' does not yet exist.  Creating now.
2021-08-01 10:29:46.469  INFO 2364 --- [nio-8080-exec-2] o.a.shiro.cache.ehcache.EhCacheManager   : Added EhCache named [authenticationCache]
2021-08-01 10:29:46.501  INFO 2364 --- [nio-8080-exec-2] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Starting...
2021-08-01 10:29:46.748  INFO 2364 --- [nio-8080-exec-2] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Start completed.
2021-08-01 10:29:46.761 DEBUG 2364 --- [nio-8080-exec-2] c.d.s.m.UserMapper.selectUserByName      : ==>  Preparing: select * from user where user_name = ?
2021-08-01 10:29:46.783 DEBUG 2364 --- [nio-8080-exec-2] c.d.s.m.UserMapper.selectUserByName      : ==> Parameters: usertest(String)
2021-08-01 10:29:46.802 DEBUG 2364 --- [nio-8080-exec-2] c.d.s.m.UserMapper.selectUserByName      : <==      Total: 1
2021-08-01 10:29:46.834  INFO 2364 --- [nio-8080-exec-2] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功
2021-08-01 10:29:47.908  INFO 2364 --- [nio-8080-exec-3] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功
2021-08-01 10:29:50.663  INFO 2364 --- [nio-8080-exec-4] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功
  • nio-8080-exec-2是第一次认证授权
    • 缓存中查询结果:Cache with name ‘authenticationCache’ does not yet exist. Creating now.
    • 缓存中没有,查询数据库并向缓存中添加:Added EhCache named [authenticationCache]
  • nio-8080-exec-3是第二次认证
  • nio-8080-exec-4是第三次认证

可以发现没有从数据库中取数据,直接缓存中有,认证成功


5、登出测试

💥 该测试目的:测试

测试一:

GET: http://localhost:8080/api/user/loginout

result

{
    "code": 200,
    "msg": "OK",
    "data": "登出成功"
}

三、项目优化

💥 1、集成swagger端口调试;

💥 2、集成全局统一异常处理;

💥 3、使用Reis作为Cahcer、Session缓存实现

(一)Swagger端口调试

🍃pom.xml:添加依赖

<!-- 优化:swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

<!-- 优化:swagger2-->

🍃com.dyl.shirocsdn.config.Swagger2.java:配置swagger

package com.dyl.shirocsdn.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @Author: dyl
 * @Data: 2021/7/19
 * @Description: 配置swagger2
 */

@Configuration
@EnableSwagger2
public class Swagger2 extends WebMvcConfigurationSupport {
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.dyl.shirocsdn.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    public ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("用户权限系统API调试")
                .description("用restful风格写接口")
                .termsOfServiceUrl("")
                .version("1.0")
                .build();
    }
}

访问:http://localhost:8080/swagger-ui.html

💥问题:swagger ui访问可能是404,这是因为资源访问被Shiro拦截器拦截,尽心如下设置。

🍃com.dyl.shirocsdn.interceptor.SourceAccessInterceptor.java:资源访问拦截器

package com.dyl.shirocsdn.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * @Des: url资源拦截器
 * @author Administrator
 */
@Slf4j
public class SourceAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("{} 进入拦截器了", request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("{} 走出拦截器", request.getRequestURI());
    }
}

🍃com.dyl.shirocsdn.config.InterceptorTrainConfigurer.java:swagger资源访问放行

package com.dyl.shirocsdn.config;

import com.dyl.shirocsdn.interceptor.SourceAccessInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @author Administrator
 */
@Configuration
public class InterceptorTrainConfigurer extends WebMvcConfigurationSupport {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SourceAccessInterceptor())
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/api/test/hello")
                .addPathPatterns("/**");
    }
    /**
     * 解决swagger-ui.html 404无法访问的问题
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 解决静态资源无法访问
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/");
        // 解决swagger无法访问
        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        // 解决swagger的js文件无法访问
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

访问:http://localhost:8080/swagger-ui.html

image-20210802152414349


(二)全局统一异常处理

💥 目的:在使用Spring MVC进行开发时,总是要对系统异常和用户的异常行为进行处理,以提供给用户友好的提示,也可以提高系统的安全性。

🍃com.dyl.shirocsdn.entity.CodeMsgException.java

package com.dyl.shirocsdn.entity;

import java.io.Serializable;

/**
 * @Author: dyl
 * @Data: 2021/7/17
 * @Description: 异常返回信息
 *
 */

public class CodeMsgException  implements Serializable {
    private int code;
    private String msg;

    //通用异常
    public static CodeMsgException SUCCESS = new CodeMsgException(0, "success");
    public static CodeMsgException SERVER_ERROR = new CodeMsgException(500100, "服务端异常");
    //注意  %s ,格式化字符串
    public static CodeMsgException SERVER_BIND_ERROR = new CodeMsgException(500101, "服务端绑定异常:%s");
    //登录模块 5002XX
    public static CodeMsgException MSG_NAME_IS_EMPTY = new CodeMsgException(500200, "用户名不能为空!");
    public static CodeMsgException MSG_PASSWORD_IS_EMPTY = new CodeMsgException(500201, "密码不能为空!");
    public static CodeMsgException MSG_MOBILE_ERROR = new CodeMsgException(500202, "手机号格式不正确!");
    public static CodeMsgException MSG_MOBILE_IS_EMPTY = new CodeMsgException(500203, "手机号不能为空!");
    public static CodeMsgException MSG_MOBILE_NOT_EXIST = new CodeMsgException(500204, "手机号不存在!");
    public static CodeMsgException MSG_PASSWORD_ERROR = new CodeMsgException(500205, "密码错误!");

    //权限模块 5003XX
    public static CodeMsgException MSG_PERMISSION_NEED_ADMIN = new CodeMsgException(500301, "需要管理员权限!");


    private CodeMsgException(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public CodeMsgException fillArgs(Object ... args){
        int code=this.code;
        String message=String.format(msg,args);
        return new CodeMsgException(code,message);
    }
    public int getCode() {
        return code;
    }
    public String getMsg() {
        return msg;
    }
}

🍃com.dyl.shirocsdn.entity.GlobalException.java

package com.dyl.shirocsdn.entity;

import java.io.Serializable;

/**
 * @Author: dyl
 * @Data: 2021/7/17
 * @Description: 全局异常类
 *
 */
public class GlobalException extends RuntimeException implements Serializable {
    private CodeMsgException codeMsgException;
    public GlobalException(CodeMsgException codeMsgException){
        super(codeMsgException.toString());
        this.codeMsgException = codeMsgException;

    }

    public CodeMsgException getCodeMsg() {
        return codeMsgException;
    }
}

🍃com.dyl.shirocsdn.interceptor.GlobalExceptionHandler.java:根据异常的种类返回给用户对应的错误编码

package com.dyl.shirocsdn.interceptor;

import com.dyl.shirocsdn.entity.CodeMsgException;
import com.dyl.shirocsdn.entity.GlobalException;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * @Author: dyl
 * @Data: 2021/7/17
 * @Description: 异常拦截器,处理系统中出现的异常,或者是自定义异常
 *
 */

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    public CodeMsgException exceptionHandler(HttpServletRequest request, Exception e) {
        // 本地自定义异常处理
        if(e instanceof GlobalException){
            return ((GlobalException) e).getCodeMsg();
        }
        //绑定异常是需要明确提示给用户的
        else if (e instanceof BindException) {
            BindException exception = (BindException) e;
            List<ObjectError> errors = exception.getAllErrors();
            //获取自错误信息
            String msg = errors.get(0).getDefaultMessage();
            //将具体错误信息设置到CodeMsg中返回
            return CodeMsgException.SERVER_BIND_ERROR.fillArgs(msg);
        }
        // 系统异常处理
        return CodeMsgException.SERVER_BIND_ERROR.fillArgs(e.getCause() + e.getMessage());
    }
}

(三)Reis作为Cahcer、Session缓存实现


🍃com.dyl.shirocsdn.shiro.session.ShiroSessionDAO.java:配置session

package com.dyl.shirocsdn.shiro.session;

import com.dyl.shirocsdn.utils.ApplicationContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Repository;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author dyl
 * @data: 2021/7/29
 */
@Slf4j
@Repository
public class ShiroSessionDAO extends AbstractSessionDAO {

    private static final String SESSION_REDIS_KEY_PREFIX = "session:";

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        getRedisTemplate().boundValueOps(SESSION_REDIS_KEY_PREFIX + session.getId().toString()).set(session);
        return sessionId;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        getRedisTemplate().boundValueOps(SESSION_REDIS_KEY_PREFIX + session.getId().toString()).set(session);
    }

    @Override
    public void delete(Session session) {
        getRedisTemplate().delete(SESSION_REDIS_KEY_PREFIX + session.getId().toString());
        HttpServletRequest request = getRequest();
        // 一定要进行空值判断,因为SessionValidationScheduler的线程也会调用这个方法,而在那个线程中是不存在Request对象的
        if (request != null) {
            request.removeAttribute(session.getId().toString());
        }
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        HttpServletRequest request = getRequest();
        if (request != null) {
            Session sessionObj = (Session) request.getAttribute(sessionId.toString());
            if (sessionObj != null) {
                return sessionObj;
            }
        }

        Session session = (Session) getRedisTemplate().boundValueOps(SESSION_REDIS_KEY_PREFIX + sessionId).get();
        if (session != null && request != null) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = getRedisTemplate().keys(SESSION_REDIS_KEY_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            List<Object> sessions = getRedisTemplate().opsForValue().multiGet(keys);
            if (sessions != null) {
                return sessions.stream().map(o -> (Session) o).collect(Collectors.toList());
            }
        }
        return Collections.emptySet();
    }

    private HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes != null ? requestAttributes.getRequest() : null;
    }

    /**
     * 序列化
     * @return
     */
    public RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        return  redisTemplate;
    }
}

🍃com.dyl.shirocsdn.shiro.cache.RedisCache.java:配置cache

    package com.dyl.shirocsdn.shiro.cache;


    import com.dyl.shirocsdn.salt.NewSimpleAuthenticationInfo;
    import com.dyl.shirocsdn.utils.ApplicationContextUtils;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    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.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;

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

    /**
     * @author Administrator
     * @data: 2021/7/29
     */
    @Slf4j
    /**
     * 自定义redis缓存的实现
     */
    public class RedisCache<k, v> implements Cache<k, v>  {

        private String cacheName;

        public RedisCache(){}

        /**
         * 有参构造器,获取定义的cacheName
         * @param cacheName
         */
        public RedisCache(String cacheName){
            this.cacheName = cacheName;
        }

        @Override
        @JsonIgnoreProperties(ignoreUnknown = true)
        public v get(k k) throws CacheException {
            log.info("=========redis cache get=============");

            Object v1 =  getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
            log.info("get key : " + k);
            return (v) v1;
        }

        /**
         * 存储key-value,存储的方式是,用cacheName做为hash数据结构中的key
         * @param k
         * @param v
         * @return
         * @throws CacheException
         */
        @Override
        public v put(k k, v v) throws CacheException {
            log.info("put key : " + k);
            log.info("put value : " + v.getClass().getName());
            if(v.getClass().getName() == "org.apache.shiro.authc.SimpleAuthenticationInfo"){
                NewSimpleAuthenticationInfo newSimpleAuthenticationInfo = new NewSimpleAuthenticationInfo(((SimpleAuthenticationInfo) v).getCredentials(), ((SimpleAuthenticationInfo) v).getCredentialsSalt());
                getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), newSimpleAuthenticationInfo);
            }else{
                getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
            }

            return null;
        }

        @Override
        public v remove(k k) throws CacheException {
            log.info("=========redis cache remove=============");
            return (v) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
        }

        @Override
        public void clear() throws CacheException {
            getRedisTemplate().opsForHash().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);
        }

        /**
         * 从容器中获取RedisTemplate,序列化操作
         * @return
         */
        public RedisTemplate getRedisTemplate(){
            RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            // 这是关键点,对象序列化为json
            redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
            return  redisTemplate;
        }
    }

🍃com.dyl.shirocsdn.shiro.cache.RedisCacheManager.java:配置cache

package com.dyl.shirocsdn.shiro.cache;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheExjavaception;
import org.apache.shiro.cache.CacheManager;

/**
 * @author dyl
 */
@Slf4j
public class RedisCacheManager implements CacheManager {
    /**
     * 参数:认证或者是授权缓存的统一名称
     * cacheName 也是作为redis Hash存储结构的key
     */
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        log.info("缓存的名字是: " + cacheName);
        return (Cache<K, V>) new RedisCache<K, V>(cacheName);
    }
}

🍃com.dyl.shirocsdn.shiro.salt.NewSimpleAuthenticationInfo.java:解决序列化错误问题

package com.dyl.shirocsdn.salt;

import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.util.ByteSource;

/**
 * @author dyl
 * @description:
 * @date 2021/8/1 0001 下午 20:09
 */
public class NewSimpleAuthenticationInfo extends SimpleAuthenticationInfo {
    public NewSimpleAuthenticationInfo(Object hashedCredentials, ByteSource credentialsSalt){
        this.credentials = hashedCredentials;
        this.credentialsSalt = credentialsSalt;
    }
    public NewSimpleAuthenticationInfo(){
    }
}

🍃com.dyl.shirocsdn.shiro.config.ShiroConfig.java:配置shiroConfig

package com.dyl.shirocsdn.config;



import com.dyl.shirocsdn.shiro.cache.RedisCacheManager;
import com.dyl.shirocsdn.shiro.realms.CustomerRealm;
import com.dyl.shirocsdn.shiro.session.ShiroSessionDAO;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.HashMap;
import java.util.Map;

/**
 * 用来整合shiro框架相关的配置类
 * @Ling https://zhuanlan.zhihu.com/p/354205960
 * @author dyl
 */
@Configuration
public class ShiroConfig {
    /**
     * 创建shiroFilter,负责拦截所有请求
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //配置系统受限资源
        //配置系统的公共资源
        Map<String, String> map = new HashMap<>();
        //请求这个资源需要认证和授权
        map.put("/api/user/login", "anon");
        map.put("/api/user/register", "anon");
        map.put("/api/*", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        //默认认证界面路径(可不写)
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        return shiroFilterFactoryBean;
    }

    /**
     * 自定义sessionManager
     *
     * @return SessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager() {
            @Override // 重写touch()方法,降低Session更新的频率
            public void touch(SessionKey key) throws InvalidSessionException {
                Session session = doGetSession(key);
                if (session != null) {
                    long oldTime = session.getLastAccessTime().getTime();
                    session.touch(); // 更新访问时间
                    long newTime = session.getLastAccessTime().getTime();
                    if (newTime - oldTime > 300000) { // 如果两次访问的时间间隔大于5分钟,主动持久化Session
                        onChange(session);
                    }
                }
            }
        };
        // 绑定SessionDAO
        sessionManager.setSessionDAO(new ShiroSessionDAO());
        SimpleCookie sessionIdCookie = new SimpleCookie("sessionId");
        sessionIdCookie.setPath("/");
        // 设置 Cookie 的过期时间,秒为单位
        sessionIdCookie.setMaxAge(8 * 60 * 60);
        // 配置Cookie模版
        sessionManager.setSessionIdCookie(sessionIdCookie);

        sessionManager.setSessionIdUrlRewritingEnabled(false);
        // session的失效时长,单位毫秒; 60 * 60 * 1000
        sessionManager.setGlobalSessionTimeout( 60 * 60 * 1000);
        // 是否开启会话验证器,默认是开启的
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 设置调度时间间隔,单位毫秒,默认就是1小时
        sessionManager.setSessionValidationInterval(2 * 60 * 60 * 1000);
        // 删除失效的session
        sessionManager.setDeleteInvalidSessions(true);

        return sessionManager;
    }

    /**
     * 创建安全管理器
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

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

        // 加入session管理器
        defaultWebSecurityManager.setSessionManager(sessionManager());

        return defaultWebSecurityManager;
    }

    /**
     * 创建自定义Realm
     * @return
     */
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //修改凭证校验匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置加密算法为md5
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        //设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        //开启cache缓存管理, 将自定义的RedisCacheManager注入进来
        customerRealm.setCacheManager(new RedisCacheManager());
        // 使用ehcache做本地缓存
        //  customerRealm.setCacheManager(new EhCacheManager());
        //开启全局缓存
        customerRealm.setCachingEnabled(true);
        //开启认证缓存
        customerRealm.setAuthenticationCachingEnabled(true);
        //开启授权缓存
        customerRealm.setAuthorizationCachingEnabled(true);
        // 认证name
        customerRealm.setAuthenticationCacheName("authenticationCache");
        // 授权name
        customerRealm.setAuthorizationCacheName("authorizationCache");
        return customerRealm;
    }



    //注入权限管理
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }
}

(四)测试

第一次使用usertest账户去登陆,控制台输出如下:

  • 首先,去缓存中查找get key usertest不存在;
  • 然后,去数据库中拿数据
  • 最后,将数据存储到缓存中
2021-08-02 15:36:31.806  INFO 17968 --- [nio-8080-exec-7] c.d.s.i.SourceAccessInterceptor          : /api/user/login 进入拦截器了
2021-08-02 15:36:31.811  INFO 17968 --- [nio-8080-exec-7] c.d.s.shiro.cache.RedisCacheManager      : 缓存的名字是: authenticationCache
2021-08-02 15:36:31.811  INFO 17968 --- [nio-8080-exec-7] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-02 15:36:34.366  INFO 17968 --- [nio-8080-exec-7] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-02 15:36:34.405  INFO 17968 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Starting...
2021-08-02 15:36:34.756  INFO 17968 --- [nio-8080-exec-7] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Start completed.
2021-08-02 15:36:34.770 DEBUG 17968 --- [nio-8080-exec-7] c.d.s.m.UserMapper.selectUserByName      : ==>  Preparing: select * from user where user_name = ?
2021-08-02 15:36:34.793 DEBUG 17968 --- [nio-8080-exec-7] c.d.s.m.UserMapper.selectUserByName      : ==> Parameters: usertest(String)
2021-08-02 15:36:34.825 DEBUG 17968 --- [nio-8080-exec-7] c.d.s.m.UserMapper.selectUserByName      : <==      Total: 1
2021-08-02 15:36:34.837  INFO 17968 --- [nio-8080-exec-7] c.dyl.shirocsdn.shiro.cache.RedisCache   : put key : usertest
2021-08-02 15:36:34.837  INFO 17968 --- [nio-8080-exec-7] c.dyl.shirocsdn.shiro.cache.RedisCache   : put value : org.apache.shiro.authc.SimpleAuthenticationInfo
2021-08-02 15:36:35.015  INFO 17968 --- [nio-8080-exec-7] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功
2021-08-02 15:36:35.029  INFO 17968 --- [nio-8080-exec-7] c.d.s.i.SourceAccessInterceptor          : /api/user/login 走出拦截器

查看第一次登陆后,Redis缓存中存的认证和session序列化后的对象:

127.0.0.1:6379> keys *
1) "session:49bd5e7b-6e25-4b80-bd93-06a35441cc47"
2) "authenticationCache"

同一个账号第二次、第三次登陆

  • 第二次、第三次usertest账号登陆
  • 从Redis缓存中直接拿到了数据,就不去查询数据库了,缓解了数据库的压力
2021-08-02 15:37:59.320  INFO 17968 --- [io-8080-exec-10] c.d.s.i.SourceAccessInterceptor          : /api/user/login 进入拦截器了
2021-08-02 15:37:59.320  INFO 17968 --- [io-8080-exec-10] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-02 15:37:59.617  INFO 17968 --- [io-8080-exec-10] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-02 15:37:59.626  INFO 17968 --- [io-8080-exec-10] c.d.s.i.SourceAccessInterceptor          : /api/user/login 走出拦截器
2021-08-02 15:38:05.966  INFO 17968 --- [nio-8080-exec-1] c.d.s.i.SourceAccessInterceptor          : /api/user/login 进入拦截器了
2021-08-02 15:38:05.966  INFO 17968 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-02 15:38:05.972  INFO 17968 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-02 15:38:05.975  INFO 17968 --- [nio-8080-exec-1] c.d.s.i.SourceAccessInterceptor          : /api/user/login 走出拦截器

授权

  • 上述步骤只完成了认证与会话管理,接下来进行授权
  • 访问:/api/res/sourceA
  • 首先,会去缓存中找数据get key : usertest,缓存中不存在
  • 然后,去数据库中拿数据,并存储到缓存中,将权限赋给用户usertest的权限有:[user, usertest]
2021-08-02 15:40:33.614  INFO 6068 --- [nio-8080-exec-4] c.d.s.i.SourceAccessInterceptor          : /api/res/sourceA 进入拦截器了
2021-08-02 15:40:33.622  INFO 6068 --- [nio-8080-exec-4] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-02 15:40:33.624  INFO 6068 --- [nio-8080-exec-4] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-02 15:40:33.624  INFO 6068 --- [nio-8080-exec-4] c.d.s.shiro.realms.CustomerRealm         : ====授权开始===
2021-08-02 15:40:33.626 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectUserByName      : ==>  Preparing: select * from user where user_name = ?
2021-08-02 15:40:33.627 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectUserByName      : ==> Parameters: usertest(String)
2021-08-02 15:40:33.628 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectUserByName      : <==      Total: 1
2021-08-02 15:40:33.631 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectPermisByRole    : ==>  Preparing: SELECT permission_code FROM `permission` as t1 INNER JOIN role_permission as t2 on t1.permission_id = t2.permission_id INNER JOIN role as t3 on t3.role_id = t2.role_id where role_code = ?;
2021-08-02 15:40:33.632 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectPermisByRole    : ==> Parameters: user(String)
2021-08-02 15:40:33.677 DEBUG 6068 --- [nio-8080-exec-4] c.d.s.m.UserMapper.selectPermisByRole    : <==      Total: 2
2021-08-02 15:40:33.678  INFO 6068 --- [nio-8080-exec-4] c.d.s.shiro.realms.CustomerRealm         : usertest的权限有:[user, usertest]
2021-08-02 15:40:33.678  INFO 6068 --- [nio-8080-exec-4] c.dyl.shirocsdn.shiro.cache.RedisCache   : put key : usertest
2021-08-02 15:40:33.678  INFO 6068 --- [nio-8080-exec-4] c.dyl.shirocsdn.shiro.cache.RedisCache   : put value : org.apache.shiro.authz.SimpleAuthorizationInfo
2021-08-02 15:40:33.694  INFO 6068 --- [nio-8080-exec-4] c.d.s.i.SourceAccessInterceptor          : /api/res/sourceA 走出拦截器

  • 访问:/api/res/sourceB
  • 首先,会去缓存中找数据get key : usertest,缓存中直接命中
  • 将权限赋给用户usertest的权限有:[user, usertest]
  • sourceB要求权限是useradd,没有权限,返回如下,异常经过全局异常处理之后返回给前端
    • { "code": 500101, "msg": "服务端绑定异常:org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.lang.String com.dyl.shirocsdn.controller.SourceController.sourceB()Subject does not have permission [useradd]" }
2021-08-02 15:40:43.801  INFO 6068 --- [nio-8080-exec-5] c.d.s.i.SourceAccessInterceptor          : /api/res/sourceB 进入拦截器了
2021-08-02 15:40:43.801  INFO 6068 --- [nio-8080-exec-5] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-02 15:40:43.809  INFO 6068 --- [nio-8080-exec-5] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-02 15:40:43.816  INFO 6068 --- [nio-8080-exec-5] c.d.s.i.SourceAccessInterceptor          : /api/res/sourceB 走出拦截器

Redis缓存中存储内容

HASH : authenticationCache
键名:usertest
键值:
{
  "@class": "com.dyl.shirocsdn.salt.NewSimpleAuthenticationInfo",
  "principals": null,
  "credentials": "d246c655fbb39fde25f39b9282c2cda0",
  "credentialsSalt": {
    "@class": "com.dyl.shirocsdn.salt.MyByteSource",
    "bytes": "T2xyQGZud1k=",
    "empty": false
  }
}

HASH : com.dyl.shirocsdn.shiro.realms.CustomerRealm.authorizationCache
键名:usertest
键值:
{
  "@class": "org.apache.shiro.authz.SimpleAuthorizationInfo",
  "roles": [
    "java.util.HashSet",
    [
      "user"
    ]
  ],
  "stringPermissions": [
    "java.util.HashSet",
    [
      "usertest",
      "user"
    ]
  ],
  "objectPermissions": null
}

HASH : session:28188226-cc51-451a-9080-5ee92764c435
键名:usertest
键值:
\xAC\xED\x00\x05sr\x00*org.apache.shiro.session.mgt.SimpleSession\x9D\x1C\xA1\xB8\xD5\x8Cbn\x03\x00\x00xpw\x02\x00\xDBt\x00$28188226-cc51-451a-9080-5ee92764c435sr\x00\x0Ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\x08\x00\x00\x01{\x05\xCD\xF8\xq\x00~\x00\x04w\x19\x00\x00\x00\x00\x006\xEE\x80\x00\x0F0:0:0:0:0:0:0:1sr\x00\x11java.util.HashMap\x05\x07\xDA\xC1\xC3\x16`\xD1\x03\x00\x02F\x00\x0AloadFactorI\x00\x09thresholdxp?@\x00\x00\x00\x00\x00\x0Cw\x08\x00\x00\x00\x10\x00\x00\x00\x02t\x00Porg.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEYsr\x00\x11java.lang.Boolean\xCD r\x80\xD5\x9C\xFA\xEE\x02\x00\x01Z\x00\x05valuexp\x01t\x00Morg.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEYsr\x002org.apache.shiro.subject.SimplePrincipalCollection\xA8\x7FX%\xC6\xA3\x08J\x03\x00\x01L\x00\x0FrealmPrincipalst\x00\x0FLjava/util/Map;xpsr\x00\x17java.util.LinkedHashMap4\xC0N\\x10l\xC0\xFB\x02\x00\x01Z\x00\x0BaccessOrderxq\x00~\x00\x05?@\x00\x00\x00\x00\x00\x0Cw\x08\x00\x00\x00\x10\x00\x00\x00\x01t\x00.com.dyl.shirocsdn.shiro.realms.CustomerRealm_0sr\x00\x17java.util.LinkedHashSet\xD8l\xD7Z\x95\xDD*\x1E\x02\x00\x00xr\x00\x11java.util.HashSet\xBAD\x85\x95\x96\xB8\xB74\x03\x00\x00xpw\x0C\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01t\x00\x08usertestxx\x00w\x01\x01q\x00~\x00\x0Fxxx

四、Bug秃头调试

(一)Redis序列化错误

💥问题:认证redis缓存的时候,第一次登陆没问题,第二次登陆SimpleAuthenticationInfo反序列化过程中无法识别部分属性。

**错误提示:**啥意思呢?就是它不认识其中的属性empty

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized field "empty" (class org.apache.shiro.subject.SimplePrincipalCollection), not marked as ignorable (one known property: "realmNames"])
......
1、第一次点击登陆是没有问题的,并查看redis中的数据
# 因为我采用的序列化方式是:redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
# Redis中的数据存储格式为json格式

键名:admintest

键值:
{
  "@class": "org.apache.shiro.authc.SimpleAuthenticationInfo",
  "principals": {
    "@class": "org.apache.shiro.subject.SimplePrincipalCollection",
    "empty": false,
    "primaryPrincipal": "admintest",
    "realmNames": [
      "java.util.LinkedHashMap$LinkedKeySet",
      [
        "com.example.domainsysadmin.shiro.realms.CustomerRealm_0"
      ]
    ]
  },
  "credentials": "61ca717a947acb9ae594d41188a77176",
  "credentialsSalt": {
    "@class": "com.example.domainsysadmin.salt.MyByteSource",
    "bytes": "TkY0Jio3RFQ=",
    "empty": false
  }
}
2、删除没用的属性,测试
键名:admintest

键值:
{
  "@class": "org.apache.shiro.authc.SimpleAuthenticationInfo",
  "credentials": "61ca717a947acb9ae594d41188a77176",
  "credentialsSalt": {
    "@class": "com.example.domainsysadmin.salt.MyByteSource",
    "bytes": "TkY0Jio3RFQ="
  }
}

# 使用redis客户端,把数据改成这样,第二次点击登陆尽然就没问题了。MMP
3、问题定位:

基本上可以肯定问题是:SimplePrincipalCollection反序列化问题,直接看看源码构造函数如下:

	
	private Map<String, Set> realmPrincipals;

    private transient String cachedToString; 

	public SimplePrincipalCollection() {
    }

    public SimplePrincipalCollection(Object principal, String realmName) {
        if (principal instanceof Collection) {
            addAll((Collection) principal, realmName);
        } else {
            add(principal, realmName);
        }
    }

    public SimplePrincipalCollection(Collection principals, String realmName) {
        addAll(principals, realmName);
    }

    public SimplePrincipalCollection(PrincipalCollection principals) {
        addAll(principals);
    }

这问题就来了:

 "principals": {
    "@class": "org.apache.shiro.subject.SimplePrincipalCollection",
    "empty": false,
    "primaryPrincipal": "admintest",
    "realmNames": [
      "java.util.LinkedHashMap$LinkedKeySet",
      [
        "com.example.domainsysadmin.shiro.realms.CustomerRealm_0"
      ]
    ]
  }

# principals 这个 SimplePrincipalCollection 对象,怎么可能用上面的构造方法初始化,不可能正确?
#  "empty"   "primaryPrincipal"      "realmNames"   这三个全是黑头户口。
# 解决:黑头户都不要
4、问题解决:

新建一个NewSimpleAuthenticationInfo来继承SimpleAuthenticationInfo,加一个构造函数:

package com.dyl.shirocsdn.salt;

import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.util.ByteSource;

/**
 * @author dyl
 * @description:
 * @date 2021/8/1 0001 下午 20:09
 */
public class NewSimpleAuthenticationInfo extends SimpleAuthenticationInfo {
    // 其他的参数不要
    public NewSimpleAuthenticationInfo(Object hashedCredentials, ByteSource credentialsSalt){
        this.credentials = hashedCredentials;
        this.credentialsSalt = credentialsSalt;
    }
    public NewSimpleAuthenticationInfo(){
    }
}

新建一个盐类MyByteSource

package com.dyl.shirocsdn.salt;

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;

/**
 * @author dyl
 * @description:
 * @date 2021/7/31 0031 下午 19:59
 */
public class MyByteSource implements ByteSource,Serializable {

    private  byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    private String empty; // 加一个无用的属性
    //加入无参数构造方法实现序列化和反序列化
    public MyByteSource(){

    }

    public void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

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

    public MyByteSource(InputStream stream) {
        this.bytes = (new 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;
    }
    @Override
    public byte[] getBytes() {
        return this.bytes;
    }
    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }
    @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 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);
        }
    }
}
5、测试

连续登陆两次

2021-08-01 23:16:11.432  INFO 11796 --- [nio-8080-exec-1] c.d.s.shiro.cache.RedisCacheManager      : 缓存的名字是: authenticationCache
2021-08-01 23:16:11.432  INFO 11796 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-01 23:16:13.565  INFO 11796 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-01 23:16:13.593  INFO 11796 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Starting...
2021-08-01 23:16:13.818  INFO 11796 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : shirocsdn - Start completed.
2021-08-01 23:16:13.829 DEBUG 11796 --- [nio-8080-exec-1] c.d.s.m.UserMapper.selectUserByName      : ==>  Preparing: select * from user where user_name = ?
2021-08-01 23:16:13.851 DEBUG 11796 --- [nio-8080-exec-1] c.d.s.m.UserMapper.selectUserByName      : ==> Parameters: usertest(String)
2021-08-01 23:16:13.926 DEBUG 11796 --- [nio-8080-exec-1] c.d.s.m.UserMapper.selectUserByName      : <==      Total: 1
2021-08-01 23:16:13.942  INFO 11796 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : put key : usertest
2021-08-01 23:16:13.942  INFO 11796 --- [nio-8080-exec-1] c.dyl.shirocsdn.shiro.cache.RedisCache   : put value : org.apache.shiro.authc.SimpleAuthenticationInfo
2021-08-01 23:16:14.104  INFO 11796 --- [nio-8080-exec-1] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功
2021-08-01 23:16:16.878  INFO 11796 --- [nio-8080-exec-3] c.dyl.shirocsdn.shiro.cache.RedisCache   : =========redis cache get=============
2021-08-01 23:16:16.953  INFO 11796 --- [nio-8080-exec-3] c.dyl.shirocsdn.shiro.cache.RedisCache   : get key : usertest
2021-08-01 23:16:16.954  INFO 11796 --- [nio-8080-exec-3] c.d.s.service.impl.UserServiceImpl       : usertest 认证成功

nio-8080-exec-3:第二次登陆直接从Redis中直接拿到了认证的SimpleAuthenticationInfo对象

看一下Redis中的数据

HASH: authenticationCache

键名:admintest

键值:
{
  "@class": "com.dyl.shirocsdn.salt.NewSimpleAuthenticationInfo",
  "principals": null,
  "credentials": "d246c655fbb39fde25f39b9282c2cda0",
  "credentialsSalt": {
    "@class": "com.dyl.shirocsdn.salt.MyByteSource",
    "bytes": "T2xyQGZud1k=",
    "empty": false
  }
}
HASH: com.dyl.shirocsdn.shiro.realms.CustomerRealm.authorizationCache

键名:admintest

键值:
{
  "@class": "org.apache.shiro.authz.SimpleAuthorizationInfo",
  "roles": [
    "java.util.HashSet",
    [
      "user"
    ]
  ],
  "stringPermissions": [
    "java.util.HashSet",
    [
      "usertest",
      "user"
    ]
  ],
  "objectPermissions": null
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不染心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值