Shiro实战

1. 概述

Shiro 是一个开源的 Java 安全框架,它提供了身份认证授权加密会话管理等功能。

Shiro 的三个核心概念:

  • Subject:代表当前正在执行操作的用户,但 Subject 代表的可以是人,也可以是任何第三方系统帐号。当然每个 Subject 实例都会被绑定到 SercurityManger
  • SecurityMangerSecurityManagerShiro 核心,主要协调 Shiro 内部的各种安全组件,设置自定义的 Realm
  • Realm:用户数据和 Shiro 数据交互的桥梁。比如需要用户身份认证、权限认证,都是需要通过 Realm 来读取数据。

2. ✨RABC权限模型

在这里插入图片描述

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

  • 用户 users:主体,即需要访问系统资源的个体或实体。每个用户都有一个或多个与之关联的角色。用户的身份通过身份验证过程进行确认,以确保其合法性。
  • 角色 roles:核心概念,它代表了一组权限的集合。角色通常根据业务功能或职责进行定义,例如“管理员”、“编辑员”、“访客”等。通过将角色分配给用户,可以实现用户与权限的间接关联,从而简化权限管理。
  • 权限 permissions:基本权限单位,它定义了用户对特定目标执行特定操作的授权。权限通常由角色来赋予,即角色具有一组权限,用户通过继承角色的权限来获得对目标和操作的访问权限。
  • 目标 objects:资源或资产,即用户需要访问的实体。目标可以是文件、数据库、设备、服务或任何系统管理的其他资源。每个目标都有与之关联的操作和权限。
  • 操作 operations:对目标进行的特定行为或动作,例如读取、写入、执行、删除等。每个目标都可以定义一组允许的操作,这些操作定义了用户对目标可以执行的行为。

权限定义了用户可以访问的资源,包括页面权限操作权限数据权限

  • 页面权限:即用户登录系统可以看到的页面,由菜单来控制,菜单包括一级菜单和二级菜单,只要用户有一级和二级菜单的权限,那么用户就可以访问页面
  • 操作权限:即页面的功能按钮,包括查看,新增,修改,删除,审核等,用户点击删除按钮时,后台会校验用户角色下的所有权限是否包含该删除权限。如果是,就可以进行下一步操作,反之提示无权限。也可以与前端开发配合,没有按钮权限,直接不显示该按钮。
  • 数据权限:即不同用户在同一页面看到的数据是不同的,比如一些大型的公司,全国有很多城市和分公司,比如杭州用户登录系统只能看到杭州的数据,上海用户只能看到上海的数据。

简单 RBAC 表结构:

CREATE TABLE `sys_user` (
  `id` varchar(20) NOT NULL COMMENT 'id',
  `user_name` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `user_name` (`user_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

CREATE TABLE `sys_role` (
  `id` varchar(20) NOT NULL COMMENT 'id',
  `role_name` varchar(100) DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';

CREATE TABLE `sys_perms` (
  `id` varchar(20) NOT NULL COMMENT 'id',
  `permissions_name` varchar(100) DEFAULT NULL COMMENT '权限名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';

CREATE TABLE `sys_user_role` (
  `id` varchar(20) NOT NULL COMMENT 'id',
  `user_id` varchar(20) DEFAULT NULL COMMENT '用户ID',
  `role_id` varchar(20) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户与角色对应关系';

CREATE TABLE `sys_role_perms` (
  `id` varchar(20) NOT NULL COMMENT 'id',
  `role_id` varchar(20) DEFAULT NULL COMMENT '角色ID',
  `perms_id` varchar(20) DEFAULT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色与权限对应关系';

基础数据:

-- sys_user
INSERT INTO `sys_user` (`id`, `user_name`, `password`) VALUES ('1', 'pxshen', '123456');
INSERT INTO `sys_user` (`id`, `user_name`, `password`) VALUES ('2', 'zhangsan', '123456');
--sys_role
INSERT INTO `sys_role` (`id`, `role_name`) VALUES ('1', 'admin');
INSERT INTO `sys_role` (`id`, `role_name`) VALUES ('2', 'user');
--sys_perms
INSERT INTO `sys_perms` (`id`, `permissions_name`) VALUES ('1', 'query');
INSERT INTO `sys_perms` (`id`, `permissions_name`) VALUES ('2', 'add');
--sys_user_role
INSERT INTO `sys_user_role` (`id`, `user_id`, `role_id`) VALUES ('1', '1', '1');
INSERT INTO `sys_user_role` (`id`, `user_id`, `role_id`) VALUES ('2', '2', '2');
--sys_role_perms
INSERT INTO `sys_role_perms` (`id`, `role_id`, `perms_id`) VALUES ('1', '1', '1');
INSERT INTO `sys_role_perms` (`id`, `role_id`, `perms_id`) VALUES ('2', '1', '2');
INSERT INTO `sys_role_perms` (`id`, `role_id`, `perms_id`) VALUES ('3', '2', '1');

查询用户角色权限信息:

SELECT
	u.user_name,
	u.`password`,
	r.role_name,
	p.permissions_name
FROM
	sys_user u
LEFT JOIN 
	sys_user_role ur ON u.id = ur.user_id
LEFT JOIN
	sys_role r ON ur.role_id = r.id
LEFT JOIN
	sys_role_perms rp ON r.id = rp.role_id
LEFT JOIN
	sys_perms p ON rp.perms_id = p.id;

在这里插入图片描述

用户名角色权限
pxshenadminadd、query
zhangsanuserquery

3. SpringBoot集成Shiro

SpringBoot 中集成 Shiro 相对简单,只需要两个类:ShiroConfig 类、CustomRealm 类。

  • ShiroConfig:顾名思义就是对 Shiro 的一些配置,包括:过滤的文件和权限、密码加密的算法、注解等相关功能。
  • CustomRealm:自定义的 CustomRealm 继承 AuthorizingRealm。并且重写父类中的 doGetAuthenticationInfo身份认证)、doGetAuthorizationInfo权限相关)这两个方法。

📚项目结构
📖bean
  SysPermissions
  SysRole
  SysUser
📖config
  ShiroConfig
📖shiro
  CustomRealm
📖controller
  LoginController
📖service
  impl
   SysUserServiceImpl
  SysUserService
📖mapper
  SysUserMapper
📖resources/mapper
  SysUserMapper.xml

🍂用户登录流程:

执行 subject.login(),调用自定义 Realm 类方法 doGetAuthenticationInfo() ,该方法返回认证信息,最终和 subject.login() 传入的令牌比对,比对成功后,返回一个JSESSIONID,保存在本地 Cookie

在这里插入图片描述

🍂访问资源流程:

请求头 Cookie 携带登录成功的 JSESSIONID,被 Shiro 拦截后进行判断该 JSESSIONID 是否已经认证。

在这里插入图片描述

🍂访问有权限资源流程:

  1. 请求头 Cookie 携带登录成功的 JSESSIONID,被 Shiro 拦截后进行判断该 JSESSIONID 是否已经认证。
  2. 调用 doGetAuthorizationInfo() 方法从数据源中获取用户的授权信息(如角色和权限),判断用户是否有权限。

在这里插入图片描述

3.1 导入依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.11.0</version>
</dependency>

3.2 实体类

SysUser:

@Data
@TableName("sys_user")
public class SysUser {
    private String id;
    private String userName;
    private String password;
    /**
     * 用户对应的角色集合
     */
    @TableField(exist = false)
    private Set<SysRole> roles;
}

SysRole:

@Data
@TableName("sys_role")
public class SysRole {

    private String id;
    private String roleName;
    /**
     * 角色对应权限集合
     */
    @TableField(exist = false)
    private Set<SysPermissions> permissions;
}

SysPermissions:

@Data
@TableName("sys_perms")
public class SysPermissions {
    private String id;
    private String permissionsName;
}

3.3 ✨配置类ShiroConfig

@Configuration
public class ShiroConfig {

    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm());
        // 将 sessionManager 注入到 SecurityManager 中,否则不会生效
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    // 解决输入网址地址栏出现 jsessionid 的问题
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 为了解决输入网址地址栏出现 jsessionid 的问题
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        /*//登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");*/

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");
        
        // 这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截,剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * *
     *  开启 Shiro 的注解(如@RequiresRoles、@RequiresPermissions),需借助 SpringAOP 扫描使用 Shiro 注解的类,并在必要时进行安全逻辑验证
     * *
     *  配置以下两个 bean (DefaultAdvisorAutoProxyCreator(可选)和 AuthorizationAttributeSourceAdvisor)即可实现此功能
     * * @return
     */
    // 配置 DefaultAdvisorAutoProxyCreator,执行权限注解 @RequiresPermissions 会调用两次 doGetAuthorizationInfo() 方法。故不注入该配置。
    /*@Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }*/

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
}
  • customRealm():将自定义的 Realm 加入容器中,用于 Shiro 的认证和授权。

  • securityManager()SecurityManager 是一个接口类,配置 Shiro 的安全管理器。由于项目是一个 web 项目,所以我们使用的是 DefaultWebSecurityManager,然后设置自定义的 Realm
    在这里插入图片描述

  • shiroFilter():配置 Shiro 的过滤器,用于设置过滤条件和跳转条件。可以设置登录页面(setLoginUrl)、权限不足跳转页面(setUnauthorizedUrl)、具体某些页面的权限控制或者身份认证。默认的过滤器还有:anno、authc、authcBasic、logout、noSessionCreation、perms、port、rest、roles、ssl、user过滤器。具体的大家可以查看org.apache.shiro.web.filter.mgt.DefaultFilter。常用的也就 authcanno

  • advisorAutoProxyCreator():配置 Shiro 的自动代理创建器,用于自动代理所有Advisor。

  • authorizationAttributeSourceAdvisor():配置 Shiro 的授权属性源顾问,用于获取授权信息。

3.4 ✨自定义类CustomRealm

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;

    /**
     * @MethodName doGetAuthenticationInfo
     * @Description 认证配置类,用于获取返回用户的凭证信息(用户名、密码)
     * @Param authenticationToken
     * @Return AuthenticationInfo
     * @return
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("enter doGetAuthenticationInfo");
        if (StringUtils.isEmpty((String) authenticationToken.getPrincipal())) {
            return null;
        }
        // 获取用户信息
        String userName = authenticationToken.getPrincipal().toString();
        String userPwd = new String((char[]) authenticationToken.getCredentials());
        System.out.println("传入需要认证的身份标识=" + userName + ",凭证=" + userPwd);

        SysUser user = sysUserService.getOne(new QueryWrapper<SysUser>().eq("user_name", userName));
        if (user == null) {
            // 这里返回后会报出对应异常
            return null;
        } else {
            // SimpleAuthenticationInfo 是 Apache Shiro 中的一个核心类,用于封装认证信息。它主要用于在认证过程中传递用户的身份信息和凭证信息。
            // 查看源码,其实主要比对 credentials(凭证)。
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                    user.getUserName(),  // 身份标识  封装成 SimplePrincipalCollection,传递给 doGetAuthorizationInfo() 方法
                    user.getPassword(),  // 凭证
                    getName()  // Realm 名称
            );
            return simpleAuthenticationInfo;
        }
    }
	
    /**
     * @MethodName doGetAuthorizationInfo
     * @Description 权限配置类,用于获取返回用户配置的角色和权限
     * @Param principalCollection
     * @Return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("enter doGetAuthorizationInfo");
        // 获取身份标识
        // getPrimaryPrincipal() 获取的是 doGetAuthenticationInfo() 返回对象 SimpleAuthenticationInfo 的身份标识 SimplePrincipalCollection 对象。
        String name = (String) principals.getPrimaryPrincipal();
        
        // 通过用户名获取角色权限集合
        SysUser user = sysUserService.listRolePermByName(name);
        // 添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (SysRole role : user.getRoles()) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //添加权限
            for (SysPermissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }
}

自定义 Shiro Realm 类,用于实现 Shiro 的认证和授权。该类继承了 AuthorizingRealm 类,实现了 doGetAuthenticationInfo()doGetAuthorizationInfo() 方法。

  • doGetAuthenticationInfo():用于从数据源(如数据库、LDAP 等)中获取认证信息。
    • 参数:AuthenticationToken,表示用户的认证凭据,通常是 UsernamePasswordToken 的实例,包含用户名和密码。
      • getPrincipal():获取与当前认证令牌(AuthenticationToken)关联的主要身份标识(principal)。身份标识通常是指用户的身份标识,例如用户名或用户对象。
      • getCredentials():获取与当前认证令牌(AuthenticationToken)关联的凭证(credentials)。凭证通常是用户提供的用于验证其身份的信息,例如密码
      • isRememberMe():检查是否设置了记住我。
    • 返回值:AuthenticationInfo ,用于封装认证信息。一般使用其实现类 SimpleAuthenticationInfo ,表示从数据源中获取的认证信息,通常包含用户名、密码、盐值等。主要属性:
      • principal: 身份标识(通常是用户名)。
      • credentials: 凭据(通常是密码)。
      • realmName: Realm 的名称。
      • salt:盐值(用于加密密码)。
      • authorities:角色和权限集合。
    • ✨注1:如果你使用令牌(如 JWT)进行认证,SimpleAuthenticationInfo 可以封装令牌信息,并在认证过程中进行验证。
    • ✨注2:查看源码发现,其实主要比对 credentials(凭据)。
    • ✨注3:principal(身份标识)封装成 SimplePrincipalCollection,传递给 doGetAuthorizationInfo() 方法。
  • doGetAuthorizationInfo(): 用于从数据源(如数据库、LDAP 等)中获取用户的授权信息(如角色和权限)。
    • 参数:principals,包含 认证身份标识的集合,通常是从 AuthenticationInfo 中获取的。
      • getPrimaryPrincipal():获取主要的身份标识(通常是用户名)。
    • 返回值:AuthorizationInfo封装用户的角色和权限信息。一般使用其实现类 SimpleAuthorizationInfo,从数据源中获取添加的角色和权限信息。
      • setRoles():设置用户的角色。
      • setStringPermissions():设置用户的权限。
      • addRole():添加用户的角色。
      • addStringPermission():添加用户的权限。

权限配置方法 doGetAuthorizationInfo ( PrincipalCollection principalCollection ) 中 PrincipalCollection 从何而来?

  • PrincipalCollectionShiro 用来存储一个或多个 principal(即身份标识)的对象。doGetAuthorizationInfo 方法通过传入的 PrincipalCollection 参数,可以访问到当前用户的全部已知身份信息。
  • SimpleAuthenticationInfo 是一个封装了用户认证信息的对象,其中包含了 PrincipalCollection 作为用户的身份标识,以及用户的凭证(如密码)和 Realm 名称。SimplePrincipalCollection 类型的身份标识会传递给 doGetAuthorizationInfo() 方法。

3.5 访问Controller

LoginController:

@RestController
@Slf4j
public class LoginController {

    @GetMapping(value ="/login", produces = "text/plain;charset=UTF-8")
    public String login(SysUser user) {
        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
            return "请输入用户名和密码!";
        }
        // 获取当前用户的 Subject 对象
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUserName(),
                user.getPassword()
        );
        try {
            //进行验证,这里可以捕获异常,然后返回对应信息
            subject.login(usernamePasswordToken);
            // subject.hasRole("admin");
            // subject.isPermitted("add");
        } catch (UnknownAccountException e) {
            log.error("用户名不存在!", e);
            return "用户名不存在!";
        } catch (AuthenticationException e) {
            log.error("账号或密码错误!", e);
            return "账号或密码错误!";
        } catch (AuthorizationException e) {
            log.error("没有权限!", e);
            return "没有权限";
        }

        // 检查用户是否已认证
        if (subject.isAuthenticated()) {
            return "登录成功";
        } else {
            usernamePasswordToken.clear();
            return "登录失败";
        }
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin() {
        return "admin success!";
    }

    @RequiresPermissions("query")
    @GetMapping("/query")
    public String query() {
        return "query success!";
    }

    @RequiresPermissions("add")
    @GetMapping("/add")
    public String add() {
        return "add success!";
    }

    @GetMapping("/anon")
    public String anon(ServletRequest request) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        Cookie[] cookies = httpServletRequest.getCookies();
        for (Cookie cookie : cookies) {
            System.out.println("Found specific cookie: " + cookie.getName() + " = " + cookie.getValue());
        }

        return "anon enter!";
    }
}

SecurityUtils.getSubject() 是 Apache Shiro 框架中的一个常用方法,用于获取当前的安全主题(Subject)。Subject 代表了当前执行的用户或进程,包含了该用户的身份验证、授权等信息。基本用法:

  • isAuthenticated():检查是否已认证
  • login(AuthenticationToken token):进行身份验证
  • logout():注销
  • hasRole(String):检查角色
  • isPermitted(String):检查权限
  • getSession():获取会话

认证流程:

  1. 安全管理器 (SecurityManager):调用 Subject.login(token) 进行登录认证,其会自动委托给SecurityManager
  2. 认证器 (Authenticator)SecurityManager 将认证请求转发给其内部的 Authenticator 组件。
  3. 遍历 RealmsAuthenticator 会遍历所有配置的 Realm,尝试使用每个 Realm 进行认证。Realm 是 Shiro 中用于与数据源交互以获取认证和授权信息的组件。可以自定义插入自己的实现类,比如本例的:CustomRealm
  4. 执行认证:对于每个 RealmAuthenticator 会调用 doGetAuthenticationInfo 方法,传递认证令牌。Realm 需要实现这个方法来提供认证信息。
  5. 比较凭证Authenticator 会比较从 Realm 获取的认证信息中的凭证(通常是密码)与令牌中的凭证。如果匹配成功,则认证成功;否则抛出异常。
  6. 设置认证状态:如果认证成功,Subject 会被标记为已认证状态,并且相关的认证信息会被存储在 Subject 中。如果认证失败,会抛出相应的异常(如 UnknownAccountExceptionIncorrectCredentialsException)。

3.6 业务Service

SysUserService:

public interface SysUserService extends IService<SysUser> {
    /**
     * 根据用户名,获取角色权限
     *
     * @param userName
     * @return 用户
     */
    SysUser listRolePermByName(String userName);
}

SysUserServiceImpl:

@Service("sysUserService")
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    @Override
    public SysUser listRolePermByName(String userName) {
        SysUser sysUser = this.getOne(new QueryWrapper<SysUser>().eq("user_name", userName));
        if (null ==sysUser) {
            return null;
        }
        Set<String> roles = this.baseMapper.listRolesByName(userName);
        Set<SysRole> sysRoles = new HashSet<>();
        for (String role : roles) {
            Set<SysPermissions> sysPermissions = this.baseMapper.listPermsByRoleName(role);
            SysRole sysRole = new SysRole();
            sysRole.setRoleName(role);
            sysRole.setPermissions(sysPermissions);
            sysRoles.add(sysRole);
        }
        sysUser.setRoles(sysRoles);
        return sysUser;
    }
}

3.7 Mapper

SysUserMapper:

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
    /**
     * 根据userName获取角色列表
     *
     * @param userName
     * @return 角色列表
     */
    Set<String> listRolesByName(@Param("userName") String userName);
    /**
     * 根据userName获取权限列表
     *
     * @param userName
     * @return 权限列表
     */
    Set<String> listPermsByName(@Param("userName") String userName);
    /**
     * 根据roleName获取权限列表
     *
     * @param roleName
     * @return 权限列表
     */
    Set<SysPermissions> listPermsByRoleName(@Param("roleName") String roleName);
}

resources/mapper/SysUserMapper.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.example.springboot.mapper.SysUserMapper">
    <select id="listRolesByName" resultType="String">
        SELECT DISTINCT
            r.role_name
        FROM
            sys_user u
                LEFT JOIN
            sys_user_role ur ON u.id = ur.user_id
                LEFT JOIN
            sys_role r ON ur.role_id = r.id
        WHERE
            u.user_name = #{userName}
    </select>

    <select id="listPermsByName" resultType="String">
        SELECT DISTINCT
            p.permissions_name
        FROM
            sys_user u
                LEFT JOIN
            sys_user_role ur ON u.id = ur.user_id
                LEFT JOIN
            sys_role r ON ur.role_id = r.id
                LEFT JOIN
            sys_role_perms rp ON r.id = rp.role_id
                LEFT JOIN
            sys_perms p ON rp.perms_id = p.id
        WHERE
            u.user_name = #{userName}
    </select>

    <select id="listPermsByRoleName" resultType="com.example.springboot.bean.SysPermissions">
        SELECT DISTINCT
            p.id,
            p.permissions_name
        FROM
            sys_role r
                LEFT JOIN
            sys_role_perms rp ON r.id = rp.role_id
                LEFT JOIN
            sys_perms p ON rp.perms_id = p.id
        WHERE
            r.role_name = #{roleName}
    </select>
</mapper>

3.8 测试

3.8.1 测试未登录,访问资源

📊 浏览器输入:http://localhost:9090/anon

在这里插入图片描述

未登录,访问资源报错。

3.8.2 测试普通用户登录

📊 浏览器输入:http://localhost:9090/login?userName=zhangsan&password=123456

在这里插入图片描述

在这里插入图片描述

完成登录。Shiro 默认的 Session 机制来帮助实现权限管理,用于维护用户的状态信息。登录成功会返回一个JSESSIONID,保存在本地 Cookie。

后台打印:
在这里插入图片描述

调用 doGetAuthenticationInfo() 方法从数据源获取认证信息。

3.8.3 测试登录,访问资源

📊 浏览器输入:http://localhost:9090/anon

在这里插入图片描述

资源访问成功。请求头携带的 cookie 是登录成功返回的JSESSIONID

后台打印:
在这里插入图片描述

3.8.4 测试访问有权限的资源

📊 浏览器输入:http://localhost:9090/query

在这里插入图片描述

后台打印:
在这里插入图片描述

调用 doGetAuthorizationInfo() 方法从数据源中获取用户的授权信息(如角色和权限)。

3.8.5 测试管理员账号登录

📊 浏览器输入:http://localhost:9090/login?userName=pxshen&password=123456

在这里插入图片描述

请求头携带第一次登录成功返回的JSESSIONID。所以,推断该JSESSIONID 不绑定认证信息。

后台打印:
在这里插入图片描述

3.8.6 测试管理员权限接口

📊 浏览器输入:http://localhost:9090/add

在这里插入图片描述

后台打印:
在这里插入图片描述

授权流程:

  1. 安全管理器 (SecurityManager):调用 Subject.isPermitted/hasRole 或者 @RequiresPermissions("add") 进行授权认证,其会自动委托给SecurityManager
  2. 授权器 (Authorizer)SecurityManager 将授权请求转发给其内部的 Authorizer 组件。
  3. 遍历 RealmsAuthorizer 会遍历所有配置的 Realm,尝试使用每个 Realm 进行授权。Realm 是 Shiro 中用于与数据源交互以获取认证和授权信息的组件。
  4. 执行授权:对于每个 RealmAuthorizer 会调用 doGetAuthorizationInfo 方法获取用户的授权信息。自定义 Realm 需要实现这个方法来提供授权信息。
  5. 比较凭证Authorizer 判断 Realm 的角色/权限是否和传入的匹配。如果匹配成功,则成功;否则返回 false,授权失败。

总结下测试结果:
首先,访问登录 login 接口,方法内执行 subject.login(),该方法会执行 Shiro 认证流程,调用自定义 Realm 类方法 doGetAuthenticationInfo() ,该方法返回认证信息,最终和 subject.login() 传入的令牌比对,比对成功后,返回一个JSESSIONID,保存在本地 Cookie

后续,访问其他接口,比如 anon,因为 ShiroConfig 类设置 filterChainDefinitionMap.put("/**", "authc"),对所有 url 都必须认证通过才可以访问。请求接口 anon 时,请求头 Cookie 携带登录成功的 JSESSIONID,被 Shiro 拦截后进行判断该 JSESSIONID 是否已经认证。


4. 密码加密验证

上述密码都是采用的明文方式进行比对的,Apache Shiro 提供了多种加密和哈希功能,可以用于密码存储、数据加密等场景。Shiro 使用 HashServiceCryptographicHash 接口来处理这些操作。

  • HashService :哈希。设置哈希参数,如迭代次数、盐生成策略等。
  • CryptographicHash:加密。进行数据的加密和解密。

4.1 ✨配置类ShiroConfig

ShiroConfig 中配置 HashService,并将其注入到自定义的 Realm 中。

@Configuration
public class ShiroConfig {
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        // 散列算法:这里使用 SHA-256 算法;
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("SHA-256");
        // 散列的次数,比如散列两次,相当于 SHA-256(SHA-256(""));
        credentialsMatcher.setHashIterations(1024);
        // storedCredentialsHexEncoded 默认是 true,此时用的是密码加密用的是 Hex 编码;false 时用 Base64 编码
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        // 将 HashService 注入到自定义的 Realm 中,告诉 realm,使用 hashedCredentialsMatcher 加密算法类来验证密文
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }

    ... 此处省略
}

4.2 ✨自定义类CustomRealm

CustomRealm 进行身份认证方法 doGetAuthenticationInfo 返回认证所需的盐值

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("enter doGetAuthenticationInfo");
        if (StringUtils.isEmpty((String) authenticationToken.getPrincipal())) {
            return null;
        }
        //获取用户信息
        String userName = authenticationToken.getPrincipal().toString();
        String userPwd = new String((char[]) authenticationToken.getCredentials());
        System.out.println("传入需要认证的主体=" + userName + ",凭证=" + userPwd);

        SysUser user = sysUserService.getOne(new QueryWrapper<SysUser>().eq("user_name", userName));
        if (user == null) {
            //这里返回后会报出对应异常
            return null;
        } else {
            // 获取用户的盐
            // ByteSource salt = ByteSource.Util.bytes(user.getSalt()); //可以数据库配置用户盐值
            ByteSource salt = ByteSource.Util.bytes(GLOBE_SALT);

            //SimpleAuthenticationInfo 是 Apache Shiro 中的一个核心类,用于封装认证信息。它主要用于在认证过程中传递用户的身份信息和凭证信息。
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                    user.getUserName(),  // 主体
                    user.getPassword(),  // 凭证
                    salt,  //盐
                    getName()  // Realm 名称
            );
            return simpleAuthenticationInfo;
        }
    }

4.3 注册时用户密码加密

注册界面我们就需要对密码进行盐值加密了,这里 Shiro 提供了SimpleHash 类:

public static void main(String[] args) {
    String pwd = "123456";
    /*
     * SHA-256加密:
     * 使用SimpleHash类对原始密码进行加密。
     * 第一个参数代表使用 SHA-256 方式加密
     * 第二个参数为原始密码
     * 第三个参数为盐值
     * 第四个参数为加密次数
     * 最后用toHex()方法将加密后的密码转成String
     * */
    String shaPwd = new SimpleHash("SHA-256",
            pwd,
            ByteSource.Util.bytes(GLOBE_SALT),
            1024).toHex();
    System.out.println("shaPwd=" + shaPwd);
}

shaPwd=26bdddc103795c3ff967574aa92a284465b63781567fb9ac8db29c90f5da24d8

在这里插入图片描述

用户注册时,程序将明文密码通过加密方式加密,存到数据库的是密文,登录时将密文取出来,再通过 shiro 将用户输入的密码进行加密对比,一样则成功,不一样则失败。

✨注:注册的加密方式要和Realm中设置的加密方式一样。

5. 整合JWT+Redis

本文章篇幅过长,另开一篇介绍 📖SpringBoot集成Shiro+Jwt+Redis


参考文档:
📖 SpringBoot集成Shiro

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不会叫的狼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值