1. 概述
Shiro 是一个开源的 Java 安全框架,它提供了身份认证、授权、加密和会话管理等功能。
Shiro 的三个核心概念:
Subject:代表当前正在执行操作的用户,但Subject代表的可以是人,也可以是任何第三方系统帐号。当然每个Subject实例都会被绑定到SercurityManger。SecurityManger:SecurityManager是Shiro核心,主要协调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;

| 用户名 | 角色 | 权限 |
|---|---|---|
| pxshen | admin | add、query |
| zhangsan | user | query |
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 是否已经认证。

🍂访问有权限资源流程:
- 请求头
Cookie携带登录成功的JSESSIONID,被Shiro拦截后进行判断该JSESSIONID是否已经认证。 - 调用
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。常用的也就authc、anno。 -
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 从何而来?
PrincipalCollection是Shiro用来存储一个或多个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():获取会话
认证流程:
- 安全管理器 (SecurityManager):调用
Subject.login(token)进行登录认证,其会自动委托给SecurityManager。- 认证器 (Authenticator):
SecurityManager将认证请求转发给其内部的Authenticator组件。- 遍历 Realms:
Authenticator会遍历所有配置的Realm,尝试使用每个Realm进行认证。Realm是 Shiro 中用于与数据源交互以获取认证和授权信息的组件。可以自定义插入自己的实现类,比如本例的:CustomRealm- 执行认证:对于每个
Realm,Authenticator会调用doGetAuthenticationInfo方法,传递认证令牌。Realm需要实现这个方法来提供认证信息。- 比较凭证:
Authenticator会比较从Realm获取的认证信息中的凭证(通常是密码)与令牌中的凭证。如果匹配成功,则认证成功;否则抛出异常。- 设置认证状态:如果认证成功,
Subject会被标记为已认证状态,并且相关的认证信息会被存储在Subject中。如果认证失败,会抛出相应的异常(如UnknownAccountException或IncorrectCredentialsException)。
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

后台打印:

授权流程:
- 安全管理器 (SecurityManager):调用
Subject.isPermitted/hasRole或者@RequiresPermissions("add")进行授权认证,其会自动委托给SecurityManager。- 授权器 (Authorizer):
SecurityManager将授权请求转发给其内部的Authorizer组件。- 遍历 Realms:
Authorizer会遍历所有配置的Realm,尝试使用每个Realm进行授权。Realm是 Shiro 中用于与数据源交互以获取认证和授权信息的组件。- 执行授权:对于每个
Realm,Authorizer会调用doGetAuthorizationInfo方法获取用户的授权信息。自定义Realm需要实现这个方法来提供授权信息。- 比较凭证:
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 使用 HashService 和 CryptographicHash 接口来处理这些操作。
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

3859

被折叠的 条评论
为什么被折叠?



