• Springboot系列之Shiro、JWT、Redis 进行认证鉴权


    Shiro架构

    Apache Shiro是一个轻量级的安全框架

    Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。 Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。其基本功能点如下图所示:

    • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
    • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
    • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
    • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
    • Web Support:Web支持,可以非常容易的集成到Web环境;
    • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
    • Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
    • Testing:提供测试支持;
    • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
    • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

    Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

    接下来我们分别从外部和内部来看看Shiro的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的API, 且API契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

    可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject。

    • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
    • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
    • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

    也就是说对于我们而言,最简单的一个Shiro应用:

    1. 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
    2. 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

    从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

    接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:

    • Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
    • SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
    • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
    • Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
    • Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
    • SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
    • SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
    • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
    • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

    下面就开始代码实现Springboot Shiro JWT Redis认证鉴权(核心代码如下)

    1.   Maven依赖pom.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <groupId>org.example</groupId>
          <artifactId>springboot-shrio-jwt</artifactId>
          <version>1.0-SNAPSHOT</version>
      
          <parent>
              <artifactId>spring-boot-parent</artifactId>
              <groupId>org.springframework.boot</groupId>
              <version>2.1.3.RELEASE </version>
          </parent>
      
          <properties>
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
              <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
              <java.version>1.8</java.version>
          </properties>
      
          <dependencies>
              <!-- web -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
      
              <!-- mybatis-plus -->
              <dependency>
                  <groupId>com.baomidou</groupId>
                  <artifactId>mybatis-plus-boot-starter</artifactId>
                  <version>3.1.2</version>
              </dependency>
      
              <!-- mysql -->
              <dependency>
                  <groupId>mysql</groupId>
                  <artifactId>mysql-connector-java</artifactId>
                  <scope>runtime</scope>
              </dependency>
      
              <!-- spring热部署 -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-devtools</artifactId>
                  <scope>compile</scope>
                  <optional>true</optional>
              </dependency>
      
              <!-- lombok -->
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                  <version>1.18.4</version>
              </dependency>
      
              <!-- druid -->
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid-spring-boot-starter</artifactId>
                  <version>1.1.17</version>
              </dependency>
      
              <!-- fastjson -->
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>fastjson</artifactId>
                  <version>1.2.58</version>
              </dependency>
      
              <!--shiro-->
              <dependency>
                  <groupId>org.apache.shiro</groupId>
                  <artifactId>shiro-spring-boot-starter</artifactId>
                  <version> 1.4.1</version>
              </dependency>
      
              <!--JWT-->
              <dependency>
                  <groupId>com.auth0</groupId>
                  <artifactId>java-jwt</artifactId>
                  <version>3.7.0</version>
              </dependency>
      
              <!-- Redis -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-data-redis</artifactId>
              </dependency>
              <dependency>
                  <groupId>org.apache.commons</groupId>
                  <artifactId>commons-pool2</artifactId>
              </dependency>
          </dependencies>
      
      
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <configuration>
                          <source>1.8</source>
                          <target>1.8</target>
                      </configuration>
                  </plugin>
              </plugins>
          </build>
      </project>
    2. 核心配置类
      package com.kongliand.shiro.config;
      
      import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
      import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
      import org.mybatis.spring.annotation.MapperScan;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * mybatis-plus配置类
       * @author kevin 
       * @date 2020/7/16 
       */
      @Configuration
      @MapperScan(value = {"com.kongliand.shiro.mapper"})
      public class MybatisPlusConfig {
      
          /**
           * 分页插件
           */
          @Bean
          public PaginationInterceptor paginationInterceptor() {
              return new PaginationInterceptor();
          }
      
          /**
           * mybatis-plus SQL执行效率插件【生产环境可以关闭】
           */
          @Bean
          public PerformanceInterceptor performanceInterceptor() {
              return new PerformanceInterceptor();
          }
      }
      

               

    package com.kongliand.shiro.config;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.interceptor.KeyGenerator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.*;
    
    import javax.annotation.Resource;
    import java.lang.reflect.Method;
    
    import java.time.Duration;
    import java.util.Arrays;
    
    import static java.util.Collections.singletonMap;
    
    
    
    
    
    
    
    /**
     * redis核心配置类
     * @author kevin 
     * @date 2020/7/16 
     */
    @Configuration
    @EnableCaching // 开启缓存支持
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Resource
        private LettuceConnectionFactory lettuceConnectionFactory;
    
        /**
         * 自定义策略生成的key
         * 自定义的缓存key的生成策略 若想使用这个key
         * 只需要讲注解上keyGenerator的值设置为keyGenerator即可</br>
         */
        @Override
        @Bean
        public KeyGenerator keyGenerator() {
            return new KeyGenerator() {
                @Override
                public Object generate(Object target, Method method, Object... params) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(target.getClass().getName());
                    sb.append(method.getDeclaringClass().getName());
                    Arrays.stream(params).map(Object::toString).forEach(sb::append);
                    return sb.toString();
                }
            };
        }
    
        /**
         * RedisTemplate配置
         */
        @Bean
        public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
            // 设置序列化
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
            om.enableDefaultTyping(DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            // 配置redisTemplate
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
            redisTemplate.setConnectionFactory(lettuceConnectionFactory);
            RedisSerializer<?> stringSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringSerializer);// key序列化
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
            redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    
        /**
         * 缓存配置管理器
         */
        @Bean
        public CacheManager cacheManager(LettuceConnectionFactory factory) {
            // 配置序列化
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
            RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            // 以锁写入的方式创建RedisCacheWriter对象
            //RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
            // 创建默认缓存配置对象
            /* 默认配置,设置缓存有效期 1小时*/
            //RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
            /* 配置test的超时时间为120s*/
            RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter(factory)).cacheDefaults(redisCacheConfiguration)
                    .withInitialCacheConfigurations(singletonMap("test", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(120)).disableCachingNullValues()))
                    .transactionAware().build();
            return cacheManager;
        }
    
    }
    

        

    package com.kongliand.shiro.config;
    
    
    
    
    
    import com.kongliand.shiro.filter.JwtFilter;
    import com.kongliand.shiro.shiro.ShiroRealm;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @desc: shiro 配置类
     * @author kevin
     */
    
    @Configuration
    public class ShiroConfig {
    
        /**
         * Filter Chain定义说明
         * <p>
         * 1、一个URL可以配置多个Filter,使用逗号分隔
         * 2、当设置多个过滤器时,全部验证通过,才视为通过
         * 3、部分过滤器可指定参数,如perms,roles
         */
        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            // 拦截器
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            // 配置不会被拦截的链接 顺序判断
            filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
            filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
            filterChainDefinitionMap.put("/", "anon");
            filterChainDefinitionMap.put("/**/*.js", "anon");
            filterChainDefinitionMap.put("/**/*.css", "anon");
            filterChainDefinitionMap.put("/**/*.html", "anon");
            filterChainDefinitionMap.put("/**/*.jpg", "anon");
            filterChainDefinitionMap.put("/**/*.png", "anon");
            filterChainDefinitionMap.put("/**/*.ico", "anon");
    
            filterChainDefinitionMap.put("/druid/**", "anon");
            filterChainDefinitionMap.put("/user/test", "anon"); //测试
    
            // 添加自己的过滤器并且取名为jwt
            Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
            filterMap.put("jwt", new JwtFilter());
            shiroFilterFactoryBean.setFilters(filterMap);
            // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
            filterChainDefinitionMap.put("/**", "jwt");
    
            // 未授权界面返回JSON
            shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
            shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
    
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(myRealm);
    
            /*
             * 关闭shiro自带的session,详情见文档
             * http://shiro.apache.org/session-management.html#SessionManagement-
             * StatelessApplications%28Sessionless%29
             */
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
    
            return securityManager;
        }
    
        /**
         * 下面的代码是添加注解支持
         *
         * @return
         */
        @Bean
        @DependsOn("lifecycleBeanPostProcessor")
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(securityManager);
            return advisor;
        }
    
    }
    

    3.鉴权登录拦截器

    package com.kongliand.shiro.filter;
    
    
    import com.kongliand.shiro.constant.CommonConstant;
    import com.kongliand.shiro.entity.JwtToken;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 鉴权登录拦截器
     **/
    @Slf4j
    public class JwtFilter extends BasicHttpAuthenticationFilter {
    
        /**
         * 执行登录认证
         *
         * @param request
         * @param response
         * @param mappedValue
         * @return
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                throw new AuthenticationException(e.getMessage());
            }
        }
    
        /**
         *
         */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);
    
            JwtToken jwtToken = new JwtToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            getSubject(request, response).login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }
    
        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        }
    }
    

    4.用户登录鉴权和获取用户授权
       

    package com.kongliand.shiro.shiro;
    
    import com.kongliand.shiro.constant.CommonConstant;
    import com.kongliand.shiro.entity.JwtToken;
    import com.kongliand.shiro.entity.SysUser;
    import com.kongliand.shiro.service.ISysUserService;
    import com.kongliand.shiro.util.CommonUtils;
    import com.kongliand.shiro.util.JwtUtil;
    import com.kongliand.shiro.util.RedisUtil;
    import com.kongliand.shiro.util.SpringContextUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.springframework.beans.BeanUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.stereotype.Component;
    
    import java.util.Set;
    
    /**
     * 用户登录鉴权和获取用户授权
     */
    @Component
    @Slf4j
    public class ShiroRealm extends AuthorizingRealm {
    
        @Autowired
        @Lazy
        private ISysUserService sysUserService;
        @Autowired
        @Lazy
        private RedisUtil redisUtil;
    
        /**
         * 必须重写此方法,不然Shiro会报错
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    
        /**
         * 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
         *
         * @param principals token
         * @return AuthorizationInfo 权限信息
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            log.info("————权限认证 [ roles、permissions]————");
            SysUser sysUser = null;
            String username = null;
            if (principals != null) {
                sysUser = (SysUser) principals.getPrimaryPrincipal();
                username = sysUser.getUserName();
            }
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    
            // 设置用户拥有的角色集合,比如“admin,test”
            Set<String> roleSet = sysUserService.getUserRolesSet(username);
            info.setRoles(roleSet);
    
            // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
            Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
            info.addStringPermissions(permissionSet);
            return info;
        }
    
        /**
         * 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
         *
         * @param auth 用户身份信息 token
         * @return 返回封装了用户信息的 AuthenticationInfo 实例
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
            String token = (String) auth.getCredentials();
            if (token == null) {
                log.info("————————身份认证失败——————————IP地址:  " + CommonUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
                throw new AuthenticationException("token为空!");
            }
            // 校验token有效性
            SysUser loginUser = this.checkUserTokenIsEffect(token);
            return new SimpleAuthenticationInfo(loginUser, token, getName());
        }
    
        /**
         * 校验token的有效性
         *
         * @param token
         */
        public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
            // 解密获得username,用于和数据库进行对比
            String username = JwtUtil.getUsername(token);
            if (username == null) {
                throw new AuthenticationException("token非法无效!");
            }
    
            // 查询用户信息
            SysUser loginUser = new SysUser();
            SysUser sysUser = sysUserService.getUserByName(username);
            if (sysUser == null) {
                throw new AuthenticationException("用户不存在!");
            }
    
            // 校验token是否超时失效 & 或者账号密码是否错误
            if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
                throw new AuthenticationException("Token失效请重新登录!");
            }
    
            // 判断用户状态
            if (!"0".equals(sysUser.getDelFlag())) {
                throw new AuthenticationException("账号已被删除,请联系管理员!");
            }
    
            BeanUtils.copyProperties(sysUser, loginUser);
            return loginUser;
        }
    
        /**
         * JWTToken刷新生命周期 (解决用户一直在线操作,提供Token失效问题)
         * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
         * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
         * 3、当该用户这次请求JWTToken值还在生命周期内,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
         * 4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
         * 5、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
         * 6、每次当返回为true情况下,都会给Response的Header中设置Authorization,该Authorization映射的v为cache对应的v值。
         * 7、注:当前端接收到Response的Header中的Authorization值会存储起来,作为以后请求token使用
        
         *
         * @param userName
         * @param passWord
         * @return
         */
        public boolean jwtTokenRefresh(String token, String userName, String passWord) {
            String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
            if (CommonUtils.isNotEmpty(cacheToken)) {
                // 校验token有效性
                if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                    String newAuthorization = JwtUtil.sign(userName, passWord);
                    redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                    // 设置超时时间
                    redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
                } else {
                    redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
                    // 设置超时时间
                    redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
                }
                return true;
            }
            return false;
        }
    
    }
    

    5.application.yml配置信息
      

    server:
      port: 8088
    
    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        druid:
          url: jdbc:mysql://127.0.0.1:3306/jwt?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
          username: root
          password: admin123
          initial-size: 10
          max-active: 100
          min-idle: 10
          max-wait: 60000
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          #validation-query: SELECT 1 FROM DUAL
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            login-username: admin
            login-password: admin
          filter:
            stat:
              log-slow-sql: true
              slow-sql-millis: 1000
              merge-sql: false
            wall:
              config:
                multi-statement-allow: true
    
      #redis配置
      redis:
        database: 0
        host: 127.0.0.1
        lettuce:
          pool:
            max-active: 8   #最大连接数据库连接数,设 0 为没有限制
            max-idle: 8     #最大等待连接中的数量,设 0 为没有限制
            max-wait: -1ms  #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
            min-idle: 0     #最小等待连接中的数量,设 0 为没有限制
          shutdown-timeout: 100ms
        password: ''
        port: 6379
    
    #mybatis plus设置
    mybatis-plus:
      type-aliases-package: com.kongliand.shiro.entity
      mapper-locations: classpath:mapper/*.xml
      global-config:
        banner: false
        db-config:
          #主键类型
          id-type: auto
          # 默认数据库表下划线命名
          table-underline: true
      configuration:
        map-underscore-to-camel-case: true
        # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    #日志配置
    logging:
      level:
        com.kongliand.shiro.mapper: debug 

    最后开始验证一下
       1.获取token
         ​ 2.根据token请求接口
       

  • 相关阅读:
    使用JS实现图片轮播滚动跑马灯效果
    特殊字符和注释标签
    html常用标签总结
    html重点标签总结
    web常用开发工具
    标签分类和关系
    Web标准和骨架
    常用开浏览器内核
    常用浏览器介绍
    2019/08/20记
  • 原文地址:https://www.cnblogs.com/mscm/p/13323489.html
Copyright © 2020-2023  润新知