⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.im/post/5d087d605188256de9779e64 「Sans_」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一.说明

Shiro是一个安全框架,项目中主要用它做认证,授权,加密,以及用户的会话管理,虽然Shiro没有SpringSecurity功能更丰富,但是它轻量,简单,在项目中通常业务需求Shiro也都能胜任.

二.项目环境

MyBatis-Plus版本: 3.1.0

SpringBoot版本:2.1.5

JDK版本:1.8

Shiro版本:1.4

Shiro-redis插件版本:3.1.0

数据表(SQL文件在项目中):数据库中测试号的密码进行了加密,密码皆为123456

数据表名 中文表名 备注说明
sys_user 系统用户表 基础表
sys_menu 权限表 基础表
sys_role 角色表 基础表
sys_role_menu 角色与权限关系表 中间表
sys_user_role 用户与角色关系表 中间表

Maven依赖如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- AOP依赖,一定要加,否则权限拦截验证不生效 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- mybatisPlus 核心库 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- 引入阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!-- Shiro 核心依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro-redis插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- StringUtilS工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
</dependencies>

配置如下:

# 配置端口
server:
port: 8764
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
# Redis数据源
redis:
host: localhost
port: 6379
timeout: 6000
password: 123456
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
# mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: auto
#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"
field-strategy: NOT_EMPTY
#数据库类型
db-type: MYSQL
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

二.编写项目基础类

用户实体,Dao,Service等在这里省略,请参考源码

编写Exception类来处理Shiro权限拦截异常

/**
* @Description 自定义异常
* @Author Sans
* @CreateTime 2019/6/15 22:56
*/
@ControllerAdvice
public class MyShiroException {
/**
* 处理Shiro权限拦截异常
* 如果返回JSON数据格式请加上 @ResponseBody注解
* @Author Sans
* @CreateTime 2019/6/15 13:35
* @Return Map<Object> 返回结果集
*/
@ResponseBody
@ExceptionHandler(value = AuthorizationException.class)
public Map<String,Object> defaultErrorHandler(){
Map<String,Object> map = new HashMap<>();
map.put("403","权限不足");
return map;
}
}

创建SHA256Util加密工具

/**
* @Description Sha-256加密工具
* @Author Sans
* @CreateTime 2019/6/12 9:27
*/
public class SHA256Util {
/** 私有构造器 **/
private SHA256Util(){};
/** 加密算法 **/
public final static String HASH_ALGORITHM_NAME = "SHA-256";
/** 循环次数 **/
public final static int HASH_ITERATIONS = 15;
/** 执行加密-采用SHA256和盐值加密 **/
public static String sha256(String password, String salt) {
return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
}
}

创建Spring工具

/**
* @Description Spring上下文工具类
* @Author Sans
* @CreateTime 2019/6/17 13:40
*/
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext context;
/**
* Spring在bean初始化后会判断是不是ApplicationContextAware的子类
* 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去
* @Author Sans
* @CreateTime 2019/6/17 16:58
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 通过Name返回指定的Bean
* @Author Sans
* @CreateTime 2019/6/17 16:03
*/
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
}

创建Shiro工具

/**
* @Description Shiro工具类
* @Author Sans
* @CreateTime 2019/6/15 16:11
*/
public class ShiroUtils {

/** 私有构造器 **/
private ShiroUtils(){}

private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

/**
* 获取当前用户Session
* @Author Sans
* @CreateTime 2019/6/17 17:03
* @Return SysUserEntity 用户信息
*/
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}

/**
* 用户登出
* @Author Sans
* @CreateTime 2019/6/17 17:23
*/
public static void logout() {
SecurityUtils.getSubject().logout();
}

/**
* 获取当前用户信息
* @Author Sans
* @CreateTime 2019/6/17 17:03
* @Return SysUserEntity 用户信息
*/
public static SysUserEntity getUserInfo() {
return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
}

/**
* 删除用户缓存信息
* @Author Sans
* @CreateTime 2019/6/17 13:57
* @Param username 用户名称
* @Param isRemoveSession 是否删除Session
* @Return void
*/
public static void deleteCache(String username, boolean isRemoveSession){
//从缓存中获取Session
Session session = null;
Collection<Session> sessions = redisSessionDAO.getActiveSessions();
SysUserEntity sysUserEntity;
Object attribute = null;
for(Session sessionInfo : sessions){
//遍历Session,找到该用户名称对应的Session
attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (sysUserEntity == null) {
continue;
}
if (Objects.equals(sysUserEntity.getUsername(), username)) {
session=sessionInfo;
break;
}
}
if (session == null||attribute == null) {
return;
}
//删除session
if (isRemoveSession) {
redisSessionDAO.delete(session);
}
//删除Cache,在访问受限接口时会重新授权
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
}

创建Shiro的SessionId生成器

/**
* @Description 自定义SessionId生成器
* @Author Sans
* @CreateTime 2019/6/11 11:48
*/
public class ShiroSessionIdGenerator implements SessionIdGenerator {
/**
* 实现SessionId生成
* @Author Sans
* @CreateTime 2019/6/11 11:54
*/
@Override
public Serializable generateId(Session session) {
Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
return String.format("login_token_%s", sessionId);
}
}

三.编写Shiro核心类

创建Realm用于授权和认证

/**
* @Description Shiro权限匹配和账号密码匹配
* @Author Sans
* @CreateTime 2019/6/15 11:27
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
/**
* 授权权限
* 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
* @Author Sans
* @CreateTime 2019/6/12 11:44
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
//获取用户ID
Long userId =sysUserEntity.getUserId();
//这里可以进行授权和处理
Set<String> rolesSet = new HashSet<>();
Set<String> permsSet = new HashSet<>();
//查询角色和权限(这里根据业务自行查询)
List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
rolesSet.add(sysRoleEntity.getRoleName());
List<SysMenuEntity> sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
for (SysMenuEntity sysMenuEntity :sysMenuEntityList) {
permsSet.add(sysMenuEntity.getPerms());
}
}
//将查到的权限和角色分别传入authorizationInfo中
authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
return authorizationInfo;
}

/**
* 身份认证
* @Author Sans
* @CreateTime 2019/6/12 12:36
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) authenticationToken.getPrincipal();
//通过username从数据库中查找 User对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUserEntity user = sysUserService.selectUserByName(username);
//判断账号是否存在
if (user == null) {
throw new AuthenticationException();
}
//判断账号是否被冻结
if (user.getState()==null||user.getState().equals("PROHIBIT")){
throw new LockedAccountException();
}
//进行验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
//验证成功开始踢人(清除缓存和Session)
ShiroUtils.deleteCache(username,true);
return authenticationInfo;
}
}

创建SessionManager类

/**
* @Description 自定义获取Token
* @Author Sans
* @CreateTime 2019/6/13 8:34
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
//定义常量
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
//重写构造器
public ShiroSessionManager() {
super();
this.setDeleteInvalidSessions(true);
}
/**
* 重写方法实现从请求头获取Token便于接口统一
* 每次请求进来,Shiro会去从请求头找Authorization这个key对应的Value(Token)
* @Author Sans
* @CreateTime 2019/6/13 8:47
*/
@Override
public Serializable getSessionId(ServletRequest request, ServletResponse response) {
String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中存在token 则从请求头中获取token
if (!StringUtils.isEmpty(token)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
// 这里禁用掉Cookie获取方式
// 按默认规则从Cookie取Token
// return super.getSessionId(request, response);
return null;
}
}
}

创建ShiroConfig配置类

/**
* @Description Shiro配置类
* @Author Sans
* @CreateTime 2019/6/10 17:42
*/
@Configuration
public class ShiroConfig {

private final String CACHE_KEY = "shiro:cache:";
private final String SESSION_KEY = "shiro:session:";
private final int EXPIRE = 1800;

//Redis配置
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.password}")
private String password;

/**
* 开启Shiro-aop注解支持
* @Attention 使用代理方式所以需要开启代码支持
* @Author Sans
* @CreateTime 2019/6/12 8:38
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

/**
* Shiro基础配置
* @Author Sans
* @CreateTime 2019/6/12 8:42
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/userLogin/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

/**
* 安全管理器
* @Author Sans
* @CreateTime 2019/6/12 10:34
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义Ssession管理
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(cacheManager());
// 自定义Realm验证
securityManager.setRealm(shiroRealm());
return securityManager;
}

/**
* 身份验证器
* @Author Sans
* @CreateTime 2019/6/12 10:37
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}

/**
* 凭证匹配器
* 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
* @Author Sans
* @CreateTime 2019/6/12 10:48
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用SHA256算法;
shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
return shaCredentialsMatcher;
}

/**
* 配置Redis管理器
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 11:06
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}

/**
* 配置Cache管理器
* 用于往Redis存储权限和角色标识
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 12:37
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix(CACHE_KEY);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
redisCacheManager.setPrincipalIdFieldName("userId");
return redisCacheManager;
}

/**
* SessionID生成器
* @Author Sans
* @CreateTime 2019/6/12 13:12
*/
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
return new ShiroSessionIdGenerator();
}

/**
* 配置RedisSessionDAO
* @Attention 使用的是shiro-redis开源插件
* @Author Sans
* @CreateTime 2019/6/12 13:44
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setExpire(expire);
return redisSessionDAO;
}

/**
* 配置Session管理器
* @Author Sans
* @CreateTime 2019/6/12 14:25
*/
@Bean
public SessionManager sessionManager() {
ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
shiroSessionManager.setSessionDAO(redisSessionDAO());
return shiroSessionManager;
}
}

四.实现权限控制

Shiro可以用代码或者注解来控制权限,通常我们使用注解控制,不仅简单方便,而且更加灵活.Shiro注解一共有五个:

注解名称 说明
RequiresAuthentication 使用该注解标注的类,方法等在访问时,当前Subject必须在当前session中已经过认证.
RequiresGuest 使用该注解标注的类,方法等在访问时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录.
RequiresUser 验证用户是否被记忆,有两种含义:一种是成功登录的(subject.isAuthenticated()结果为true);另外一种是被记忆的(subject.isRemembered()结果为true).
RequiresPermissions 当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法.如果没有权限,则方法不会执行还会抛出AuthorizationException异常.
RequiresRoles 当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法.如果没有角色,则方法不会执行还会抛出AuthorizationException异常.

一般情况下我们在项目中做权限控制,使用最多的是RequiresPermissions和RequiresRoles,允许存在多个角色和权限,默认逻辑是AND,也就是同时拥有这些才可以访问方法,可以在注解中以参数的形式设置成OR

示例
//拥有一个角色就可以访问
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
//拥有所有权限才可以访问
@RequiresPermissions(value={"sys:user:info","sys:role:info"},logical = Logical.AND)

使用顺序:Shiro注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止,默认拦截顺序是:RequiresRoles->RequiresPermissions->RequiresAuthentication-> RequiresUser->RequiresGuest

示例
//拥有ADMIN角色同时还要有sys:role:info权限
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")

创建UserRoleController角色拦截测试类

/**
* @Description 角色测试
* @Author Sans
* @CreateTime 2019/6/19 11:38
*/
@RestController
@RequestMapping("/role")
public class UserRoleController {

@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;

/**
* 管理员角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getAdminInfo")
@RequiresRoles("ADMIN")
public Map<String,Object> getAdminInfo(){
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有管理员角色能访问的接口");
return map;
}

/**
* 用户角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getUserInfo")
@RequiresRoles("USER")
public Map<String,Object> getUserInfo(){
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只有用户角色能访问的接口");
return map;
}

/**
* 角色测试接口
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getRoleInfo")
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
@RequiresUser
public Map<String,Object> getRoleInfo(){
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");
return map;
}

/**
* 登出(测试登出)
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getLogout")
@RequiresUser
public Map<String,Object> getLogout(){
ShiroUtils.logout();
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","登出");
return map;
}
}

创建UserMenuController权限拦截测试类

/**
* @Description 权限测试
* @Author Sans
* @CreateTime 2019/6/19 11:38
*/
@RestController
@RequestMapping("/menu")
public class UserMenuController {

@Autowired
private SysUserService sysUserService;
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private SysRoleMenuService sysRoleMenuService;

/**
* 获取用户信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:36
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getUserInfoList")
@RequiresPermissions("sys:user:info")
public Map<String,Object> getUserInfoList(){
Map<String,Object> map = new HashMap<>();
List<SysUserEntity> sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
return map;
}

/**
* 获取角色信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:37
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getRoleInfoList")
@RequiresPermissions("sys:role:info")
public Map<String,Object> getRoleInfoList(){
Map<String,Object> map = new HashMap<>();
List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
return map;
}

/**
* 获取权限信息集合
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getMenuInfoList")
@RequiresPermissions("sys:menu:info")
public Map<String,Object> getMenuInfoList(){
Map<String,Object> map = new HashMap<>();
List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}

/**
* 获取所有数据
* @Author Sans
* @CreateTime 2019/6/19 10:38
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/getInfoAll")
@RequiresPermissions("sys:info:all")
public Map<String,Object> getInfoAll(){
Map<String,Object> map = new HashMap<>();
List<SysUserEntity> sysUserEntityList = sysUserService.list();
map.put("sysUserEntityList",sysUserEntityList);
List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
map.put("sysRoleEntityList",sysRoleEntityList);
List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
map.put("sysMenuEntityList",sysMenuEntityList);
return map;
}

/**
* 添加管理员角色权限(测试动态权限更新)
* @Author Sans
* @CreateTime 2019/6/19 10:39
* @Param username 用户ID
* @Return Map<String,Object> 返回结果
*/
@RequestMapping("/addMenu")
public Map<String,Object> addMenu(){
//添加管理员角色权限
SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
sysRoleMenuEntity.setMenuId(4L);
sysRoleMenuEntity.setRoleId(1L);
sysRoleMenuService.save(sysRoleMenuEntity);
//清除缓存
String username = "admin";
ShiroUtils.deleteCache(username,false);
Map<String,Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","权限添加成功");
return map;
}
}

创建UserLoginController登录类

/**
* @Description 用户登录
* @Author Sans
* @CreateTime 2019/6/17 15:21
*/
@RestController
@RequestMapping("/userLogin")
public class UserLoginController {

/**
* 登录
* @Author Sans
* @CreateTime 2019/6/20 9:21
*/
@RequestMapping("/login")
public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
Map<String,Object> map = new HashMap<>();
//进行身份验证
try{
//验证身份和登录
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
//验证成功进行登录操作
subject.login(token);
}catch (IncorrectCredentialsException e) {
map.put("code",500);
map.put("msg","用户不存在或者密码错误");
return map;
} catch (LockedAccountException e) {
map.put("code",500);
map.put("msg","登录失败,该用户已被冻结");
return map;
} catch (AuthenticationException e) {
map.put("code",500);
map.put("msg","该用户不存在");
return map;
} catch (Exception e) {
map.put("code",500);
map.put("msg","未知异常");
return map;
}
map.put("code",0);
map.put("msg","登录成功");
map.put("token",ShiroUtils.getSession().getId().toString());
return map;
}
/**
* 未登录
* @Author Sans
* @CreateTime 2019/6/20 9:22
*/
@RequestMapping("/unauth")
public Map<String,Object> unauth(){
Map<String,Object> map = new HashMap<>();
map.put("code",500);
map.put("msg","未登录");
return map;
}
}

五.POSTMAN测试

登录成功后会返回TOKEN,因为是单点登录,再次登录的话会返回新的TOKEN,之前Redis的TOKEN就会失效了

当第一次访问接口后我们可以看到缓存中已经有权限数据了,在次访问接口的时候,Shiro会直接去缓存中拿取权限,注意访问接口时候要设置请求头.

ADMIN这个号现在没有sys:info:all这个权限的,所以无法访问getInfoAll接口,我们要动态分配权限后,要清掉缓存,在访问接口时候,Shiro会去重新执行授权方法,之后再次把权限和角色数据放入缓存中

访问添加权限测试接口,因为是测试,我把增加权限的用户ADMIN写死在里面了,权限添加后,调用工具类清掉缓存,我们可以发现,Redis中已经没有缓存了

再次访问getInfoAll接口,因为缓存中没有数据,Shiro会重新授权查询权限,拦截通过

六.项目源码

码云:https://gitee.com/liselotte/spring-boot-shiro-demo

GitHub:https://github.com/xuyulong2017/my-java-demo

文章目录
  1. 1. 一.说明
  2. 2. 二.项目环境
  3. 3. 二.编写项目基础类
  4. 4. 三.编写Shiro核心类
  5. 5. 四.实现权限控制
  6. 6. 五.POSTMAN测试
  7. 7. 六.项目源码