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請求接口
   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM