SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第1张图片

来源:juejin.im/post/5d087d605188256de9779e64

一. 说明

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

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第2张图片

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>	
        <!-- StringUitlS工具 -->	
        <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	
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段	
    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;	
            }	
        }	
        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取token	
            return super.getSessionId(request, response);	
        }	
    }	
}

创建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注解一共有五个:

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第3张图片

一般情况下我们在项目中做权限控制,使用最多的是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->

示例:

//拥有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 {	

	
    @Autowired	
    private SysUserService sysUserService;	

	
    /**	
     * 登录	
     * @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就会失效了

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第4张图片

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

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第5张图片

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第6张图片

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

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第7张图片

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

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第8张图片

640?wx_fmt=png

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

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第9张图片

七. 项目源码

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

(完)

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第10张图片

Java团长

专注于Java干货分享

SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录_第11张图片

扫描上方二维码获取更多Java干货

你可能感兴趣的:(SpringBoot整合Shiro实现动态权限加载更新+Session共享+单点登录)