作者:Sans_
本文地址: juejin.im/user/5cab164a5188257d752a1e94
“
之前有小伙伴问松哥 Shiro 如何做单点登录,其实方案有很多种,刚好这两天看到一篇文章,感觉整个实现过程还比较完整,因此和各位小伙伴们来分享一下。
”
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); } }
创建 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; } }
登录成功后会返回 TOKEN, 因为是单点登录, 再次登陆的话会返回新的 TOKEN, 之前 Redis 的 TOKEN 就会失效了
当第一次访问接口后我们可以看到缓存中已经有权限数据了, 在次访问接口的时候, Shiro 会直接去缓存中拿取权限, 注意访问接口时候要设置请求头.
ADMIN 这个号现在没有 sys:info:all 这个权限的, 所以无法访问 getInfoAll 接口, 我们要动态分配权限后, 要清掉缓存, 在访问接口时候, Shiro 会去重新执行授权方法, 之后再次把权限和角色数据放入缓存中
访问添加权限测试接口, 因为是测试, 我把增加权限的用户 ADMIN 写死在里面了, 权限添加后, 调用工具类清掉缓存, 我们可以发现, Redis 中已经没有缓存了
再次访问 getInfoAll 接口, 因为缓存中没有数据, Shiro 会重新授权查询权限, 拦截通过
https://github.com/xuyulong2017/my-java-demo
谢谢大家阅读,如果喜欢,请收藏点赞,多给些 star,文章不足之处,也请给出宝贵意见.
3、 学习 Spring,看松哥这篇万余字的干货,够用了!
4、 学 Maven,看松哥这一篇就够了
5、 快来!松哥的免费资源库更新啦!
6、 40 篇原创干货,带你进入 Spring Boot 殿堂!
喜欢就点个 "在看" 呗^_^