Shiro

Shiro

_

简介

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

核心架构

  • SubjectSubject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
  • SecurityManager:SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
  • AuthenticatorAuthenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
  • Authorizer:Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
  • Realm: Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
  • SessionManager: sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
  • SessionDAO:SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
  • CacheManager:CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
  • Cryptography: Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

shiro中的认证

认证

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

shiro中认证的关键对象

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

Principal:身份信息

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

credential:凭证信息

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

认证流程

入门案例

创建普通maven项目

项目结构

依赖

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

在resources下创建shiro.ini文件

[users]
zhangsan=123

实现

package com.qc.demo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

/**
 * @author c
 * @date 2022-06-09 10:29
 */
public class TestShiro {
    public static void main(String[] args) {

        //默认安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //读取配置文件信息
        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //设置安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //获取主题
        Subject subject = SecurityUtils.getSubject();
        //凭证校验
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","1233");

        try {
            //登陆
            subject.login(token);
            System.out.println("登陆成功!");
        }catch (UnknownAccountException e){
            //UnknownAccountException 帐号错误异常
            e.printStackTrace();
            System.err.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            //密码错误异常
            e.printStackTrace();
            System.err.println("密码错误");
        }

    }
}

常见异常

  • UnknownAccountException 帐号错误
  • IncorrectCredentialsException 密码错误
  • DisabledAccountException 帐号被禁用
  • LockedAccountException 帐号被锁定
  • ExcessiveAttemptsException 登录失败次数过多
  • ExpiredCredentialsException 凭证过期

自定义realm实现角色和权限

自定义realm

package com.qc.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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.util.ArrayList;
import java.util.List;

/**
 * @author c
 * @date 2022-06-09 17:32
 */
public class CustomerRealm extends AuthorizingRealm {


    /**
     * 帐号权限认证
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String PrimaryPrincipal = principalCollection.getPrimaryPrincipal().toString();
        System.out.println(PrimaryPrincipal);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        //简单的添加角色信息
        simpleAuthorizationInfo.addRole("admin");

        //添加单个权限
        simpleAuthorizationInfo.addStringPermission("user:update:*");

        List<String> list =  new ArrayList<>();
        list.add("product:*:*");
        list.add("test:*:*");
        //添加多个权限
        simpleAuthorizationInfo.addStringPermissions(list);


        return simpleAuthorizationInfo;
    }

    /**
     * 帐号认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        //获取登陆的帐号
        String principal = authenticationToken.getPrincipal().toString();
        //判断是否相等
        if ("zhangsan".equals(principal)){

            //md5加 salt 生成
            String password = "3c88b338102c1a343bcb88cd3878758e";
            String salt = "Q4F%";

            //返回 SimpleAuthenticationInfo对象
            return new SimpleAuthenticationInfo(principal,password,ByteSource.Util.bytes(salt),this.getName());
        }

        return null;
    }
}

测试

package com.qc.demo;

import com.qc.realm.CustomerRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;

/**
 * @author c
 * @date 2022-06-09 10:29
 */
public class TestShiro {
    public static void main(String[] args) {

        //默认安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //读取配置文件信息
//        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //使用自定义流
//        defaultSecurityManager.setRealm(new CustomerRealm());

        //md5 和salt的使用
        CustomerRealm customerRealm = new CustomerRealm();


        //md5加密对象
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置加密方式
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        defaultSecurityManager.setRealm(customerRealm);

        //设置安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //获取主题
        Subject subject = SecurityUtils.getSubject();
        //凭证校验
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123");

        try {
            //登陆
            subject.login(token);
            System.out.println("登陆成功!");
        }catch (UnknownAccountException e){
            //UnknownAccountException 帐号错误异常
            e.printStackTrace();
            System.err.println("用户名错误");
        }catch (IncorrectCredentialsException e){
            //密码错误异常
            e.printStackTrace();
            System.err.println("密码错误");
        }

        //认证通过   权限测试
        if (subject.isAuthenticated()){
            //用户是否拥有角色
            boolean admin = subject.hasRole("admin");
            System.out.println(admin);

            //用户是否拥有该权限
            boolean permitted = subject.isPermitted("product:create:001");
            System.out.println(permitted);

        }

    }
}

常见过滤器

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

SpringBoot项目实战

数据库

# 用户表
create table if not exists t_user(
    id int(11) not null primary key auto_increment comment 'id',
    username varchar(64) not null comment '账户',
    password varchar(100) not null comment '密码',
    salt varchar(255) default null comment '盐'
)engine = InnoDB DEFAULT CHAR SET=utf8mb4 comment '用户表';


# 角色表
create table  if not exists t_role(
    id int(11) not null  primary key auto_increment comment '角色id',
    role varchar(64) default null comment '角色'
)engine = InnoDB DEFAULT CHAR SET=utf8mb4 comment '角色表';



# 用户角色关系表
create table  if not exists t_user_role(
    id int(11) not null  primary key  auto_increment comment '用户角色关系表id',
    uid int(11) default null comment '用户id',
    rid int(11) default null comment '角色id'
)engine  = InnoDB default charset = utf8mb4 comment '用户角色关系表';


# 权限表
create table if not exists t_permissions(
    id int(11) not null primary key auto_increment comment '权限id',
    name varchar(64) default null comment '权限名称',
    url varchar(128) default null comment '路径'
)engine = InnoDB default charset = utf8mb4 comment '权限表';

# 角色权限关系表
create table if not exists t_role_permissions(
    id int(11) not null primary key auto_increment comment '角色权限表id',
    rid varchar(64) default null comment '角色id',
    pid varchar(128) default null comment '权限id'
)engine = InnoDB default charset = utf8mb4 comment '角色权限表';


# 创建角色
insert into t_role values (default,'admin'),(default,'user');

# 用户角色关系
insert into t_user_role values (default,1,1),(default,1,2);
insert into t_user_role values (default,2,2);

# 权限数据
insert into t_permissions values
                                 (default,'hello页面','hello'),
                                 (default,'系统设置','sys/sys_set'),
                                 (default,'修改用户','user/update');


# 角色权限关系   注册了角色信息手动注入的权限信息
# 添加admin的权限 admin拥有全部权限
insert into t_role_permissions values (default,1,1),(default,1,2),(default,1,3);
#添加user权限  user只有修改用户权限
insert into t_role_permissions values (default,2,1),(default,2,3);

创建springboo项目

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.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qc</groupId>
    <artifactId>shiro_demo02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro_demo02</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.9.0</version>
        </dependency>

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

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
      
              <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

server.port=8080

# 关闭thymeleaf缓存
spring.thymeleaf.cache=false

mybatis-plus.mapper-locations=classpath:/mapper/*Mapper.xml
#自动扫描这个包下的实体类,并配置别名
mybatis-plus.type-aliases-package=com.qc.entity
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl


spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/c

config

MybatisConfig

package com.qc.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author c
 * @date 2022-06-09 22:05
 * mybatis 配置
 */
@Configuration
@MapperScan("com.qc.mapper")
public class MybatisConfig {


}

ShiroConfig

package com.qc.config;

import com.qc.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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.LinkedHashMap;
import java.util.Map;

/**
 * @author c
 * @date 2022-06-09 20:00
 */
@Configuration
public class ShiroConfig {

    /**
     * 创建shiroFilter
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){

        //创建shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        Map<String, String> map = new LinkedHashMap<>();
        // /**代表拦截项目中的一切资源,authc代表shiro中的一个filter的别名
        map.put("/registers","anon");
        map.put("/register","anon");
        map.put("/login","anon");
        map.put("/**","authc");

        shiroFilterFactoryBean.setLoginUrl("/login");
        //配置认证和授权规则
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;

    }

    /**
     * 创建web管理器
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){

        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;

    }

    //注入自定义realm
    @Bean
    public Realm realm(){
        CustomerRealm customerRealm = new CustomerRealm();

        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置md5加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);

        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        return customerRealm;
    }




}

entity

User

package com.qc.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
 * @author c
 * @date 2022-06-09 22:02
 */
@TableName("t_user")
@Data
public class User implements Serializable {


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

    /**
     * 角色列表
     */
    @TableField(exist = false)
    private List<Role> roles;



}

Role

package com.qc.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
 * @author c
 * @date 2022-06-13 18:41
 *
 * 角色表
 */
@TableName("t_role")
@Data
public class Role implements Serializable {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String role;

    /**
     * 权限列表
     */
    @TableField(exist = false)
    private List<Permissions> permissions;


}

Permissinos

package com.qc.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

/**
 * @author c
 * @date 2022-06-13 18:39
 *
 * 权限表
 */
@TableName("t_permissions")
@Data
public class Permissions implements Serializable {

    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String name;
    private String url;


}

realm

CustomerRealm

package com.qc.realm;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.qc.entity.Permissions;
import com.qc.entity.Role;
import com.qc.entity.User;
import com.qc.service.UserService;
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 org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author c
 * @date 2022-06-09 20:06
 */
public class CustomerRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取身份信息
        String username = principalCollection.getPrimaryPrincipal().toString();

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        //如果为null  返回异常
        if (!StringUtils.hasText(username)){
            throw new IllegalArgumentException("用户为null");
        }
        //查询用户角色权限信息
        User user = userService.getUserRole(username);
        List<Role> roles = user.getRoles();

        List<String> strings = new ArrayList<>();

        Set<String> per = new HashSet<>();

        roles.forEach(role -> {
            //将角色添加到集合
            strings.add(role.getRole());
            //根据角色id查询权限信息
            Role rolePermissions = userService.getRolePermissions(role.getId());

            //角色权限集合
            List<Permissions> permissions = rolePermissions.getPermissions();


            if (!CollectionUtils.isEmpty(permissions)){
                permissions.forEach(permissions1 -> per.add(permissions1.getUrl()));
            }
            //加入权限
            simpleAuthorizationInfo.setStringPermissions(per);

        });

        //加入角色
        simpleAuthorizationInfo.addRoles(strings);


        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        String username = authenticationToken.getPrincipal().toString();

        //查询帐号为username的具体信息
        User user = userService.getOne(new LambdaQueryWrapper<User>().eq(StringUtils.hasText(username), User::getUsername, username));

        return new SimpleAuthenticationInfo(username,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());

    }
}

mapper

UserMapper

package com.qc.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qc.entity.Role;
import com.qc.entity.User;

/**
 * @author c
 * @date 2022-06-09 22:14
 */
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根据用户名获取角色信息
     * @return user
     */
    User getUserRole(String username);

    /**
     * 根据角色id查询权限
     */
    Role getRolePermissions(Integer rid);

}

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.xml -->
<mapper namespace="com.qc.mapper.UserMapper">

    <select id="getUserRole" parameterType="string" resultMap="userMap">
        select * from t_user u
            left join t_user_role  ur on u.id =ur.uid
            left join t_role r on ur.rid = r.id
        where username = #{username}
    </select>

    <resultMap id="userMap" type="user">
        <result property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="salt" column="salt"/>
        <collection property="roles" ofType="role" javaType="list">
            <result property="id" column="id"/>
            <result property="role" column="role"/>
        </collection>
    </resultMap>


    <select id="getRolePermissions" parameterType="integer" resultMap="rolePermissionsMap">
        select * from t_role tr
            left join t_role_permissions trp on tr.id = trp.rid
            left join t_permissions tp on  trp.pid = tp.id
        where tr.id = #{id}
    </select>

    <resultMap id="rolePermissionsMap" type="role">
        <result property="id" column="id"/>
        <result property="role" column="role"/>
        <collection property="permissions" ofType="permissions" javaType="list">
            <result property="id" column="id"/>
            <result property="name" column="name"/>
            <result property="url" column="url"/>
        </collection>
    </resultMap>
</mapper>

Service

UserService

package com.qc.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qc.entity.Role;
import com.qc.entity.User;


/**
 * @author c
 * @date 2022-06-09 22:16
 */
public interface UserService extends IService<User> {

    void register(User user);

    User getUserRole(String username);

    Role getRolePermissions(Integer rid);

}


Impl

package com.qc.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qc.entity.Role;
import com.qc.entity.User;
import com.qc.mapper.UserMapper;
import com.qc.service.UserService;
import com.qc.utils.SaltUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;


/**
 * @author c
 * @date 2022-06-09 22:18
 */
@Service
@Transactional(rollbackFor = {Exception.class,RuntimeException.class})
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 注册功能
     */
    @Override
    public void register(User user) {

        //获取salt
        String salt = SaltUtils.getSalt(8);

        //将明文密码加盐进行md5+hash散列
        Md5Hash md5 = new Md5Hash(user.getPassword(),salt,1024);
        //将密码和盐放入实体类
        user.setPassword(md5.toHex());
        user.setSalt(salt);

        System.out.println("正在注册中--------------------》");
        System.out.println(user);
        //存入数据库
        save(user);
    }

    @Override
    public User getUserRole(String username) {
        return userMapper.getUserRole(username);
    }

    @Override
    public Role getRolePermissions(Integer rid) {
        return userMapper.getRolePermissions(rid);
    }
}

controller

package com.qc.controller;

import com.qc.entity.User;
import com.qc.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;

/**
 * @author c
 * @date 2022-06-09 20:15
 */
@Controller
public class ShiroController {

    @Resource
    private UserService userService;

    @RequestMapping("/{url}")
    public String redirect(@PathVariable String url){
        System.out.println("usr:"+url);
        if ("favicon.ico".equals(url)){
            return "redirect:classpath:/templates/favicon.ico";
        }
        return url;
    }


    /**
     * 主页
     * @return index
     */
    @RequestMapping("/index")
    public String index(){
        return "index";
    }



    @PostMapping("/login")
    public String login(String username,String password){

        System.out.println("登陆中------------------");
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username,password));
            return "index";
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("帐号错误");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "login";

    }

    @ResponseBody
    @RequiresPermissions("hello")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }

    //注册功能
    @PostMapping("/register")
    public String register(User user){
        userService.register(user);

        return "login";
    }



    //退出登陆
    @RequestMapping("/logout")
    public String logout(){

        //退出登陆
        SecurityUtils.getSubject().logout();

        return "login";
    }

    /**
     * 系统设置 权限实验用
     * @return null
     */
    @RequestMapping("/sys/sys_set")
    @RequiresAuthentication
    @RequiresRoles({"admin","user"})
    @RequiresPermissions("sys:sys_set")
    @ResponseBody
    public String sysSet(){
        return "系统设置";
    }


    @RequestMapping("/user/update")
    @ResponseBody
    @RequiresPermissions("user:update")
    public String userUpdate(){
        return "用户修改";
    }

}

启动类

package com.qc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShiroDemo02Application {

    public static void main(String[] args) {
        SpringApplication.run(ShiroDemo02Application.class, args);
    }

}

html

login

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    用户名:<label>
            <input type="text" name="username" >
          </label><br/>
    密码  :<label>
            <input type="text" name="password" >
           </label><br>
    <input type="submit" value="登陆" >
    <a th:href="@{/registers}">注册</a>
</form>
</body>
</html>

index

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
<a th:href="@{/hello}">hello</a>
<a th:href="@{/logout}">退出登陆</a>


</body>
</html>

registers

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
</head>
<body>
<form th:action="@{/register}" method="post">
    用户名:<label>
    <input type="text" name="username" >
</label><br/>
    密码  :<label>
    <input type="text" name="password" >
</label><br>
    <input type="submit" value="注册" >
</form>

</body>
</html>

使用Cache

依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.9.0</version>
        </dependency>

开启缓存

    //注入自定义realm
    @Bean
    public Realm realm(){
        CustomerRealm customerRealm = new CustomerRealm();

        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置md5加密
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);

        //开启缓存管理
        customerRealm.setCachingEnabled(true);
        //权限缓存
        customerRealm.setAuthorizationCachingEnabled(true);
        //身份验证缓存
        customerRealm.setAuthenticationCachingEnabled(true);

        customerRealm.setCacheManager(new EhCacheManager());


        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        return customerRealm;
    }


使用redis缓存

启动redis

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>

properties

# redis 端口
spring.redis.port=6379
# ip地址
spring.redis.host=192.168.1.133
# redis 默认库
spring.redis.database=0
# 密码 没有默认为空不写
#spring.redis.password=

RedisCacheManager

package com.qc.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Configuration;


/**
 * @author c
 * @date 2022-06-14 16:39
 *
 * 自定义CacheManager
 */
@Configuration
public class RedisCacheManager implements CacheManager {


    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
        System.out.println("缓存名称: "+cacheName);
        //返回重写的redisCache
        return new RedisCache<>(cacheName);
    }
}

RedisCache

package com.qc.cache;

import com.qc.utils.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;

/**
 * @author c
 * @date 2022-06-14 16:43
 */
public class RedisCache<K,V> implements Cache<K,V> {



    private String cacheName;

    public RedisCache() {

    }

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

    @Override
    public V get(K k) throws CacheException {
        //获取缓存
        System.out.println("获取缓存:"+ k);
        return (V) getRedisTemplate().opsForHash().get(this.cacheName,k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
        System.out.println("设置缓存key: "+k+" value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

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

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

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

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

    @Override
    public Collection<V> values() {
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }


    //封装获取redisTemplate
    private RedisTemplate getRedisTemplate(){
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

}

ApplicationContextUtils

package com.qc.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);
    }
}

MyByteSource

由于shiro中提供的simpleByteSource实现没有实现序列化,所有在认证时出现错误信息,需要自动salt实现序列化

//自定义salt实现  实现序列化接口
public class MyByteSource extends SimpleByteSource implements Serializable {
    public MyByteSource(String string) {
        super(string);
    }
}

在realm中使用自定义salt


    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        String username = authenticationToken.getPrincipal().toString();

        //查询帐号为username的具体信息
        User user = userService.getOne(new LambdaQueryWrapper<User>().eq(StringUtils.hasText(username), User::getUsername, username));

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

    }

redis数据

Gateway+Sentinel 2022-06-08
统一返回类R 2022-07-20

评论区