Shiro-使用笔记

cywdder / 2023-07-29 / 原文

目录
  • 零、资料
  • 一、使用
    • 1.1、环境搭建
      • 1.1.1、创建数据库
      • 1.1.2、导入依赖
      • 1.1.3、编写springboot配置文件
      • 1.1.4、创建 统一结果集类 + pojo + DTO
      • 1.1.5、mapper接口及xml
      • 1.1.6、service接口及实现类
      • 1.1.7、shiro操作数据库的类
      • 1.1.8、shiro的配置类
      • 1.1.9、统一异常处理
      • 1.1.10、controller
      • 1.1.11、前端

零、资料

  • 快速入门:https://zhuanlan.zhihu.com/p/54176956
  • 官网:https://shiro.apache.org/get-started.html
  • 笔记:https://blog.csdn.net/qq_43056248/article/details/120931020

一、使用

1.1、环境搭建

  • SpringBoot2
  • Mybatis3
  • MySQL8.0
  • Shiro

1.1.1、创建数据库

create database db_shiro_demo;
  
drop table if exists tb_user;
create table tb_user(
	id int primary key auto_increment,
	user_name varchar(50),
	pwd varchar(500)	
);
-- 3个用户的密码都是123,盐值为salt_123,shiro校验密码是hash次数设置为了3次
INSERT INTO `db_shiro_demo`.`tb_user` (`id`, `user_name`, `pwd`) VALUES (1, 'zs', '2b5c7c724baa14fd1da53f4cb45b0a0d');
INSERT INTO `db_shiro_demo`.`tb_user` (`id`, `user_name`, `pwd`) VALUES (2, 'ls', '2b5c7c724baa14fd1da53f4cb45b0a0d');
INSERT INTO `db_shiro_demo`.`tb_user` (`id`, `user_name`, `pwd`) VALUES (3, 'ww', '123');



drop table if exists tb_role;
create table tb_role(
	id int primary key auto_increment,
	role_name varchar(50)
);
INSERT INTO `db_shiro_demo`.`tb_role` (`id`, `role_name`) VALUES (1, 'admin');
INSERT INTO `db_shiro_demo`.`tb_role` (`id`, `role_name`) VALUES (2, 'normal');




drop table if exists tb_perm;
create table tb_perm(
	id int primary key auto_increment,
	perm_name varchar(50)
);
INSERT INTO `db_shiro_demo`.`tb_perm` (`id`, `perm_name`) VALUES (1, 'add');
INSERT INTO `db_shiro_demo`.`tb_perm` (`id`, `perm_name`) VALUES (2, 'del');
INSERT INTO `db_shiro_demo`.`tb_perm` (`id`, `perm_name`) VALUES (3, 'update');
INSERT INTO `db_shiro_demo`.`tb_perm` (`id`, `perm_name`) VALUES (4, 'find');



drop table if exists tb_mid_role_perm;
create table tb_mid_role_perm(
	id int primary key auto_increment,
	role_id int,
	perm_id int
);
INSERT INTO `db_shiro_demo`.`tb_mid_role_perm` (`id`, `role_id`, `perm_id`) VALUES (1, 1, 1);
INSERT INTO `db_shiro_demo`.`tb_mid_role_perm` (`id`, `role_id`, `perm_id`) VALUES (2, 1, 2);
INSERT INTO `db_shiro_demo`.`tb_mid_role_perm` (`id`, `role_id`, `perm_id`) VALUES (3, 1, 3);
INSERT INTO `db_shiro_demo`.`tb_mid_role_perm` (`id`, `role_id`, `perm_id`) VALUES (4, 1, 4);
INSERT INTO `db_shiro_demo`.`tb_mid_role_perm` (`id`, `role_id`, `perm_id`) VALUES (5, 2, 4);



drop table if exists tb_mid_user_role;
create table tb_mid_user_role(
	id int primary key auto_increment,
	user_id int,
	role_id int
);
INSERT INTO `db_shiro_demo`.`tb_mid_user_role` (`id`, `user_id`, `role_id`) VALUES (1, 1, 1);
INSERT INTO `db_shiro_demo`.`tb_mid_user_role` (`id`, `user_id`, `role_id`) VALUES (2, 2, 2);


-- 【约定】:用户zs: admin角色,增删改查权限;用户ls:normal角色,查权限;用户ww:没有任何角色和权限


1.1.2、导入依赖

maven依赖:

<?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.14</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <groupId>com.cyw</groupId>
    <artifactId>shiro-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>shiro-demo</name>
    <description>shiro-demo</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.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </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>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.12.0</version>
        </dependency>


    </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>

1.1.3、编写springboot配置文件

server:
  port: 8000
mybatis:
  mapper-locations: classpath:/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_shiro_demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: root

logging:
  level:
    com.cyw.shirodemo.mapper: info

1.1.4、创建 统一结果集类 + pojo + DTO

统一结果集类 Rst

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Rst<T> {
    private Integer code;
    private Boolean flag;
    private String msg;
    private T data;

    public static <T> Rst<T> ok() {
        return new Rst<>(2000, true, "请求成功", null);
    }

    public static <T> Rst<T> ok(String msg) {
        return new Rst<>(2000, true, msg, null);
    }

    public static <T> Rst<T> ok(String msg, T data) {
        return new Rst<>(2000, true, msg, data);
    }

    public static <T> Rst<T> ok(Integer code, String msg, T data) {
        return new Rst<>(code, true, msg, data);
    }

    public static <T> Rst<T> err() {
        return new Rst<>(4000, false, "请求失败", null);
    }

    public static <T> Rst<T> err(String msg) {
        return new Rst<>(4000, false, msg, null);
    }

    public static <T> Rst<T> err(Integer code, String msg) {
        return new Rst<>(code, false, msg, null);
    }
    public static <T> Rst<T> err(Integer code, String msg,T data) {
        return new Rst<>(code, false, msg, data);
    }

}

用户 User

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String userName;
    private String pwd;
    private List<Role> roleList;
}

角色 Role

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private Integer id;
    private String roleName;
    private List<Perm> permList;
} 

权限 Perm

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Perm {
    private Integer id;
    private String permName;
}

UserDto

@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserDto{
    private Integer id;
    private String userName;
    private String pwd;
    private Set<String> roleStrSet;
    private Set<String> permStrSet;
}

1.1.5、mapper接口及xml

UserMapper.java

@Mapper
public interface UserMapper {
    User findUserByName(String userName);
}

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.cyw.shirodemo.mapper.UserMapper">

    <resultMap id="BaseResultMap" type="com.cyw.shirodemo.pojo.User">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="userName" column="user_name" jdbcType="VARCHAR"/>
            <result property="pwd" column="pwd" jdbcType="VARCHAR"/>
            <collection property="roleList" ofType="com.cyw.shirodemo.pojo.Role">
                <id property="id" column="role_id"/>
                <result property="roleName" column="role_name"/>
            </collection>
    </resultMap>

    <sql id="Base_Column_List">
        tb_user.id,
        tb_user.user_name,
        tb_user.pwd
    </sql>

    <select id="findUserByName" resultMap="BaseResultMap">
        select <include refid="Base_Column_List"></include>,tb_mid_user_role.role_id,tb_role.role_name
            from tb_user,tb_role,tb_mid_user_role
            where
                tb_user.id = tb_mid_user_role.user_id
                and tb_mid_user_role.role_id = tb_role.id
                and tb_user.user_name=#{userName}
    </select>

</mapper>

PermMapper.java

@Mapper
public interface PermMapper {
    List<Perm> findPermListByRoleName(String roleName);
}

PermMapper.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.cyw.shirodemo.mapper.PermMapper">

    <resultMap id="BaseResultMap" type="com.cyw.shirodemo.pojo.Perm">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="permName" column="perm_name" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        tb_perm.id,tb_perm.perm_name
    </sql>

    <select id="findPermListByRoleName" resultType="com.cyw.shirodemo.pojo.Perm">
        select tb_perm.*
        from tb_role,tb_mid_role_perm,tb_perm
        where
            tb_role.id = tb_mid_role_perm.role_id
            and tb_mid_role_perm.perm_id = tb_perm.id
            and tb_role.role_name = #{roleName}
    </select> 

</mapper>

1.1.6、service接口及实现类

UserService:

public interface UserService {
    User findUserByName(String userName);
    UserDto findUserWithRoleAndPerm(String userName);
}

UserServiceImpl:


@AllArgsConstructor
@Transactional
@Service("userService")
public class UserServiceImpl implements UserService {
    private UserMapper userMapper;
    private PermMapper permMapper;

    /**
     * 按用户名查询用户信息
     * @param userName
     * @return
     */
    @Override
    public User findUserByName(String userName) {
        return userMapper.findUserByName(userName);
    }
    
    /**
     * 按用户名查询带有权限的用户信息
     * @param principal
     * @return
     */
    @Override
    public UserDto findUserWithRoleAndPerm(String principal) {

        User user = userMapper.findUserByName(principal);
        UserDto userDto = new UserDto();
        BeanUtils.copyProperties(user,userDto);

        Set<String> roleSet = new HashSet<>();
        Set<String> permSet = new HashSet<>();
        user.getRoleList()
                .forEach(role -> {
                    roleSet.add(role.getRoleName());

                    List<Perm> permListByRoleName = permMapper.findPermListByRoleName(role.getRoleName());
                    Set<String> permSetTmp = permListByRoleName.stream()
                            .map(Perm::getPermName).collect(Collectors.toSet());
                    permSet.addAll(permSetTmp);
                });
        userDto.setRoleStrSet(roleSet);
        userDto.setPermStrSet(permSet);
        return userDto;
    }

}

1.1.7、shiro操作数据库的类

MyRealm.java

import org.apache.shiro.authc.*;
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;

@AllArgsConstructor
public class MyRealm extends AuthorizingRealm {

    private UserService userService;


    /**
     * 认证(登录)的处理
     *
     * @param token the authentication token containing the user's principal and credentials.
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        
        // 来自浏览器的用户名和密码
        String principal = (String) token.getPrincipal();
        
        // 按用户名从数据中查询用户信息
        User user = userService.findUserByName(principal);

        if (user == null) {
            throw new UnknownAccountException("用户不存在");
        }

        // 返回数据库中能正确登录的用户信息给shiro
        //// return new SimpleAuthenticationInfo(principal, user.getPwd(), "myRealm");
        // 参数:用户名,加密后的密码,盐值,realm名
        return new SimpleAuthenticationInfo(principal, user.getPwd(), ByteSource.Util.bytes("salt_123"),"myRealm");
    }


    /**
     * 授权的处理
     *
     * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        
        // 浏览器传过来的用户名
        String principal = (String) principals.getPrimaryPrincipal();

        // 从数据库中根据浏览器传过来的用户名查询带有角色和权限的用户信息
        UserDto userDto = userService.findUserWithRoleAndPerm(principal);
        
        // 将带有权限的用户信息封装为shiro能够识别的用户信息对象AuthorizationInfo
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(userDto.getRoleStrSet());
        simpleAuthorizationInfo.setStringPermissions(userDto.getPermStrSet());

        return simpleAuthorizationInfo;
    }

}

1.1.8、shiro的配置类

ShiroConfig.java

@Configuration
public class ShiroConfig {

    @Bean
    public MyRealm myRealm(UserService userService){
        MyRealm myRealm = new MyRealm(userService);
        // 使用HashedCredentialsMatcher带加密的匹配器来替换原先明文密码匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 指定加密算法
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        // 指定加密次数
        hashedCredentialsMatcher.setHashIterations(3);
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return myRealm;
    }

    @Bean
    public DefaultWebSecurityManager defaultSecurityManager(MyRealm myRealm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(myRealm);
        SecurityUtils.setSecurityManager(defaultWebSecurityManager);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        
        // 在本次案例的url说明:
        //  - "/":登录页
        //  - "/html/home.html":登录后访问的页面
        //  - "/js": js资源
        //  - "/user/login": 登录表单提交的接口
        //  - "/user/logout":退出登录表单的接口
        //  - "/user/test1": 测试是否授权成功的接口,只有用户有admin角色才能访问该接口
        //  - "/user/403": 没有权限时自动请求的接口

        // 设置未认证(登录)时,访问需要认证的资源时跳转的页面
        shiroFilterFactoryBean.setLoginUrl("/user/login");

        // 设置访问无权限的资源时跳转的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/403");
        

        // 指定路径和过滤器的对应关系
        Map<String, String> filterMap = new HashMap<>();

        // 设置不需要登录就能访问的url
        filterMap.put("/", "anon");
        filterMap.put("/js/**", "anon");
        filterMap.put("/user/login", "anon");


        // 设置权限与url的映射关系,此处为登录用户拥有名为admin的角色时才能访问
        // - roles: 需要xx角色,才能访问
        // - perms: 需要xx权限,才能访问
        // - anon: 匿名过滤器,未登陆也可以访问
        // - authc: 认证过滤器, 登陆后访问
        // - user: 需要xx用户,才能访问
        // - port:指定端口才能访问
        // - ssl: 必须使用https协议才能访问
        // - logout: 登出功能
        // - rest: 根据指定HTTP请求访问才能访问 ,get方式提交 或者 post方式提交才能访问
        filterMap.put("/user/test1", "roles[admin]");

        // 其他路径则需要登录才能访问
        filterMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }


    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
        return authorizationAttributeSourceAdvisor;
    }

}

常见Shiro中的过滤器:

配置过滤器(权限)与url的映射关系时,除了像上面那样在配置类中写外,还可以在 Controller的接口上加如下5个注解:

  • @RequiresRoles({"user","admin"}, logical=Logical.OR):需要有xxx角色才能访问,logical默认是and
  • @RequiresPermissions({"find","del"}, logical=Logical.OR):需要有xxx权限才能访问
  • @RequiresAuthentication:需要先登录才能访问
  • @RequiresUser:当前用户需要存在才能访问
  • @RequiresGusst:不需要认证或授权

1.1.9、统一异常处理

GlobalExceptionHandler.java:

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(IncorrectCredentialsException.class)
    public Rst<Void> err() {
        return Rst.err("用户名或密码错误");
    }

    @ExceptionHandler(Exception.class)
    public Rst<Void> err(Exception exception) {
        return Rst.err(exception.getMessage());
    }
}

1.1.10、controller

UserController.java:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;  

@AllArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/login")
    public Rst<String> login(User user) throws IncorrectCredentialsException{

        System.out.println("=== 正在登陆。。。");

        // 利用 md5算法 将密码123 和 盐值salt_123 计算3次
        System.out.println(new SimpleHash("MD5", "123","salt_123",3));

        // 从shiro提供的容器工具类SecurityUtils获取 Subject 对象,通过 Subject 对象可以执行shiro封装过的登录方法
        Subject subject = SecurityUtils.getSubject();

        // 判断是否未登录
        if (!subject.isAuthenticated()){
            // 将用户信息转化为shiro能识别的用户信息UsernamePasswordToken 
            UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPwd());  

            subject.login(token);

            return Rst.ok("登录成功",token.getUsername());
        }
        return Rst.ok("已经登录过了");
    }


    @GetMapping("/logout")
    public Rst<Void> logout() {

        System.out.println("=== 正在注销。。。");

        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return Rst.ok("注销成功");
    } 

    @GetMapping("/403")
    public Rst<Void> unAuth() {
        System.out.println("=== 没有权限。。。");
        return Rst.err("没有权限");
    }

    @GetMapping("/test1")
    public Rst<Void> test() {
        System.out.println("=== 需要有admin角色的用户才能访问。。。");
        return Rst.ok("有admin角色,权限访问成功");
    }

}

1.1.11、前端

登录页:

主页:

测试角色/权限-有权限时:

测试角色/权限-无权限时: