SpringBoot集成shiro+redis+jwt实现无状态授权验证

前言

1.放弃Cookie,Session,使用JWT进行鉴权,完全实现无状态鉴权。

2.JWT密钥支持过期时间;jwt作为创建验证token工具,并选择一种验证方式与算法。

3.使用redis做缓存处理,缓存token等等登录信息,以实现单点登录与超时登录功能。

4.密码加密(采用AES-128 + Base64的方式)。

5.根据RefreshToken自动刷新AccessToken。

6.对跨域提供支持。

项目整体介绍

当我们开发需要简单的鉴权功能时,springboot更快捷、简单的集成JWT,使得项目更加简洁(Compact)、自包含(Self-contained)、安全(security);鉴权的流程为下。
1.用户登陆之后(user/login),使用密码对账号进行签名生成并返回token并设置过期时间;成功返回加密的AccessToken放在Response Header的Authorization属性中,失败直接返回401错误(帐号或密码不正确)。

2.前端将token保存到本地,并且每次发送请求时都在header上携带token。

3.shiro过滤器拦截到请求并获取header中的token,并提交到自定义realm的doGetAuthenticationInfo方法。

4.通过jwt解码获取token中的用户名,从数据库中查询到密码之后根据密码生成jwt效验器并对token进行验证。

项目代码详解

1、pom引入

       <!--mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-java.version}</version>
        </dependency>
        
        <!-- MyBatis增强工具-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus-generator.version}</version>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!-- Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
        </dependency>

2、application.yml配置

shiro:
  # AES密码加密私钥(Base64加密)
  encryptAESKey: V2FuZzkyNjQ1NGRTQkFQSUpXVA==
  # JWT认证加密私钥(Base64加密)
  encryptJWTKey: U0JBUElKV1RkV2FuZzkyNjQ1NA==
  # AccessToken过期时间-5分钟-5*60(秒为单位)
  accessTokenExpireTime: 3600
  # RefreshToken过期时间-60分钟-30*60(秒为单位)
  refreshTokenExpireTime: 3600
  # Shiro缓存过期时间-5分钟-5*60(秒为单位)(一般设置与AccessToken过期时间一致)
  shiroCacheExpireTime: 3600
  # 是否需要开启任何请求必须登录才可访问,开发时候可以为false,生产与测试必须为true
  mustLoginFlag: true
  #shiro排除过滤url,多个用,分割
  excludeUrl:

3、Shiro配置

         首先是Shiro的配置,定义两个类ShiroConfig以及UserRealm用来配置Shiro,以及验证部分。 这里重要的是关闭                   Session,因为我们使用JWT来传输安全信息。自定义缓存管理器,同时我们要添加一个JwttFilter,将所有的请求交由它处           理。

    1)ShiroConfig配置

package com.example.demo.shiro;

import com.example.demo.shiro.cache.CustomCacheManager;
import com.example.demo.shiro.jwt.JwtFilter;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.beans.factory.annotation.Value;
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;

/**
 * @author LST
 * @version 1.0
 * @Description: Shiro配置
 * @date 2019-12-31 16:22
 */
@Configuration
public class ShiroConfig {

    /**
     * 排除过滤url
     */
    @Value("${shiro.excludeUrl}")
    private String excludeUrl;

    /**
     * RefreshToken过期时间
     */
    @Value("${shiro.refreshTokenExpireTime}")
    private long refreshTokenExpireTime;

    /**
     * 是否需要开启任何请求必须登录才可访问
     */
    @Value("${shiro.mustLoginFlag}")
    private boolean mustLoginFlag;

    /**
     * 配置使用自定义Realm,关闭Shiro自带的session
     * 详情见文档 http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
     *
     * @param userRealm
     * @return org.apache.shiro.web.mgt.DefaultWebSecurityManager
     * @author Wang926454
     * @date 2018/8/31 10:55
     */
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(UserRealm userRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();

        // 关闭Shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        // 设置自定义Cache缓存
        manager.setCacheManager(new CustomCacheManager());
        // 使用自定义Realm
        manager.setRealm(userRealm);
        return manager;
    }

    /**
     * 添加自己的过滤器,自定义url规则
     * 详情见文档 http://shiro.apache.org/web.html#urls-
     *
     * @param securityManager
     * @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
     * @author Wang926454
     * @date 2018/8/31 10:57
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器取名为jwt
        Map<String, Filter> filterMap = new HashMap<>(16);
        filterMap.put("jwt", new JwtFilter(refreshTokenExpireTime, mustLoginFlag));
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        // 自定义url规则使用LinkedHashMap有序Map
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(16);
        if (StringUtils.isNotEmpty(excludeUrl)) {
            String[] excludeUrls = excludeUrl.split(",");
            for (String excludeUrl : excludeUrls) {
                filterChainDefinitionMap.put(excludeUrl, "anon");
            }
        }
        //登录和swagger页面放开
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/webjars/springfox-swagger-ui/*", "anon");
        filterChainDefinitionMap.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

 2)UserRealm配置

package com.example.demo.shiro;


import com.auth0.jwt.exceptions.TokenExpiredException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.constants.Constant;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.shiro.jwt.JwtToken;
import com.example.demo.utils.JwtUtil;
import com.example.demo.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;


/**
 * @author LST
 * @version 1.0
 * @Description: 自定义Realm
 * @date 2019-12-31 16:22
 */
@Service
@Slf4j
public class UserRealm extends AuthorizingRealm {

    private final UserMapper userMapper;

    /**
     * RefreshToken过期时间
     */
    @Value("${shiro.refreshTokenExpireTime}")
    private long refreshTokenExpireTime;

    @Autowired
    @Lazy
    private RedisUtil redisUtil;

    @Autowired
    @Lazy
    public UserRealm(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    /**
     * 大坑,必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的(授权)
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return (AuthorizationInfo) principals;
    }

    /**
     *  默认使用此方法进行用户名正确与否验证,错误抛出异常即可。(认证)
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得account,用于和数据库进行对比
        String uid = JwtUtil.getClaim(token, Constant.UID);
        String serviceType = JwtUtil.getClaim(token, Constant.SERVICETYPE);
        // 帐号为空
        if (StringUtils.isBlank(uid)) {
            throw new AuthenticationException("Token中帐号为空");
        }
        User user = (User) redisUtil.getValue(Constant.PREFIX_SHIRO_USER + uid);
        if (user == null) {
            //如果数据库中还没有就说明账号不存在
            // 查询用户是否存在
            QueryWrapper<User> wrapper = new QueryWrapper();
            wrapper.lambda().eq(User::getId, uid);
            user = userMapper.selectOne(wrapper);
            if (user == null) {
                throw new AuthenticationException("该帐号不存在");
            }
            //redis中在设置一次
            redisUtil.setValue(Constant.PREFIX_SHIRO_USER + user.getId(), user, refreshTokenExpireTime);
        }
        // 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
        if (JwtUtil.verify(token)) {
            if (redisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + uid + ":" + serviceType)) {
                // 获取RefreshToken的时间戳
                String currentTimeMillisRedis = redisUtil.getValue(Constant.PREFIX_SHIRO_REFRESH_TOKEN + uid + ":" + serviceType).toString();
                // 获取AccessToken时间戳,与RefreshToken的时间戳对比
                if (JwtUtil.getClaim(token, Constant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
                    return new SimpleAuthenticationInfo(token, token, "userRealm");
                }
            } else {
                throw new AuthenticationException("Token授权异常");
            }
        }
        throw new TokenExpiredException("Token已过期");
    }
}

4、JWT 相关配置

    1)jwtToken自定义一个对象用来包装token。

package com.example.demo.shiro.jwt;


import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author LST
 * @version 1.0
 * @Description: JwtToken
 * @date 2019-12-31 16:22
 */
public class JwtToken implements AuthenticationToken {
    /**
     * Token
     */
    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

   2)JwtUtil工具类用来进行签名和效验Token(jwtUtil配置)

package com.example.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.demo.constants.Constant;
import com.example.demo.exception.SXException;
import com.example.demo.exception.ServiceExceptionEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.util.Date;

/**
 * @author LST
 * @version 1.0
 * @Description: JAVA-JWT工具类
 * @date 2019-12-31 16:22
 */
@Component
@Slf4j
public class JwtUtil {

    /**
     * 过期时间改为从配置文件获取
     */
    private static String accessTokenExpireTime;

    /**
     * JWT认证加密私钥(Base64加密)
     */
    private static String encryptJWTKey;

    @Value("${shiro.accessTokenExpireTime}")
    public void setAccessTokenExpireTime(String accessTokenExpireTime) {
        JwtUtil.accessTokenExpireTime = accessTokenExpireTime;
    }

    @Value("${shiro.encryptJWTKey}")
    public void setEncryptJWTKey(String encryptJWTKey) {
        JwtUtil.encryptJWTKey = encryptJWTKey;
    }

    /**
     * 校验token是否正确
     * @param token Token
     * @return
     * @author lst
     * @date 2019-12-31 16:22
     */
    public static boolean verify(String token) {
        try {
            // 帐号加JWT私钥解密
            String secret = getClaim(token, Constant.UID) + Base64ConvertUtil.decode(encryptJWTKey);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .build();
            verifier.verify(token);
        } catch (UnsupportedEncodingException e) {
            log.error("JWTToken认证解密UnsupportedEncodingException异常:" + e.getMessage());
            return false;
        } catch (Exception e) {
            log.error("JWTToken认证解密异常:" + e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 获得Token中的信息无需secret解密也能获得
     *
     * @param token
     * @param claim
     * @return java.lang.String
     * @author lst
     * @date 2019-12-31 16:22
     */
    public static String getClaim(String token, String claim) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            // 只能输出String类型,如果是其他类型返回null
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
            log.error("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
            throw new SXException(ServiceExceptionEnum.JWTTOEN_JWTDECODE);
        }
    }

    /**
     * 生成签名
     * @param uid 帐号
     * @param currentTimeMillis 获取当前最新时间戳
     * @param serviceType 服务渠道
     * @return 返回加密的Token
     * @author lst
     * @date 2019-12-31 16:22
     */
    public static String sign(String uid, String currentTimeMillis, String serviceType) {
        try {
            // 帐号加JWT私钥加密
            String secret = uid + Base64ConvertUtil.decode(encryptJWTKey);
            // 此处过期时间是以毫秒为单位,所以乘以1000
            Date date = new Date(System.currentTimeMillis() + Long.parseLong(accessTokenExpireTime) * 1000);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带account帐号信息
            return JWT.create()
                    .withClaim("uid", uid)
                    .withClaim(Constant.CURRENT_TIME_MILLIS, currentTimeMillis)
                    .withClaim("serviceType", serviceType)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            log.error("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
            throw new SXException(ServiceExceptionEnum.JWTTOEN_UNSUPPORTEDENCODING);
        }
    }
}

       3)接下来是Jwt的Fiter,集成自Shiro的BasicHttpAuthenticationFilter,使用shiro来拦截token。(jwtFilter配置)

package com.example.demo.shiro.jwt;

import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.constants.Constant;
import com.example.demo.result.RestResponse;
import com.example.demo.service.SpringContextHolder;
import com.example.demo.utils.JsonConvertUtil;
import com.example.demo.utils.JwtUtil;
import com.example.demo.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
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;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author LST
 * @version 1.0
 * @Description: JWT过滤
 * @date 2019-12-31 16:22
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    private RedisUtil redisUtil;
    /**
     * RefreshToken过期时间
     */
    private long refreshTokenExpireTime;

    /**
     * 是否需要开启任何请求必须登录才可访问,开发时候可以为false,生产与测试必须为tru.
     */
    private boolean mustLoginFlag;

    /**
     * 初始化
     * @param refreshTokenExpireTime 刷新token时间
     * @param mustLoginFlag 是否需要登录
     */
    public JwtFilter(long refreshTokenExpireTime, boolean mustLoginFlag) {
        this.refreshTokenExpireTime = refreshTokenExpireTime;
        this.mustLoginFlag = mustLoginFlag;
    }

    /**
     * 是否允许访问
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 查看当前Header中是否携带Authorization属性(Token),有的话就进行登录认证授权
        if (this.isLoginAttempt(request, response)) {
            try {
                // 进行Shiro的登录UserRealm
                this.executeLogin(request, response);
            } catch (Exception e) {
                // 认证出现异常,传递错误信息msg
                String msg = e.getMessage();
                // 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
                Throwable throwable = e.getCause();
                if (throwable instanceof SignatureVerificationException) {
                    // 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
                    //msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
                    msg = "登录已过期";
                } else if (throwable instanceof TokenExpiredException) {
                    // 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新

                    if (this.refreshToken(request, response)) {
                        return true;
                    } else {
                        // msg = "Token已过期(" + throwable.getMessage() + ")";
                        msg = "登录已过期";
                    }
                } else {
                    // 应用异常不为空
                    if (throwable != null) {
                        // 获取应用异常msg
                        msg = throwable.getMessage();
                    }
                }
                /*
                  错误两种处理方式
                  1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息
                  2. 无需转发,直接返回Response信息
                  一般使用第二种(更方便)
                 */
                // 直接返回Response信息
                this.response401(request, response, msg);
                return false;
            }
        } else {
            // 没有携带Token
            HttpServletRequest httpRequest = WebUtils.toHttp(request);
            // 获取当前请求类型
            String httpMethod = httpRequest.getMethod();
            // 获取当前请求URI
            String requestURI = httpRequest.getRequestURI();
            log.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
            // mustLoginFlag = true 开启任何请求必须登录才可访问
            if (mustLoginFlag) {
                this.response401(request, response, "请先登录");
                return false;
            }
        }
        return true;
    }

    /**
     * 这里我们详细说明下为什么重写
     * 可以对比父类方法,只是将executeLogin方法调用去除了
     * 如果没有去除将会循环调用doGetAuthenticationInfo方法
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return false;
    }

    /**
     * 检测Header里面是否包含Authorization字段,有就进行Token登录认证授权
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
        String token = this.getAuthzHeader(request);
        return token != null;
    }

    /**
     * 进行AccessToken登录认证授权
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
        JwtToken token = new JwtToken(this.getAuthzHeader(request));

        // 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获
        this.getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 获取请求渠道 1pc 2移动
     *
     * @param request
     * @return
     */
    private String getServiceType(ServletRequest request) {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        String serviceType = "1";
        if (!StringUtils.isEmpty(httpRequest.getHeader("serviceType"))) {
            serviceType = httpRequest.getHeader("serviceType");
        }
        return serviceType;
    }

    /**
     * 此处为AccessToken刷新,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
     * synchronized 并发请求的时候会出错,目前先添加synchronized处理--可以后期优化--https://www.sundayfine.com/jwt-refresh-token/解决方案
     */
    private synchronized boolean refreshToken(ServletRequest request, ServletResponse response) {
        // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
        String token = this.getAuthzHeader(request);
        String serviceType = getServiceType(request);
        // 获取当前Token的帐号信息
        String uid = JwtUtil.getClaim(token, Constant.UID);
        if (redisUtil == null) {
            redisUtil = SpringContextHolder.getBean(RedisUtil.class);
        }
        String oldRedisToken = (String) redisUtil.getValue("token_blacklist:" + token);

        if (StringUtils.isNotEmpty(oldRedisToken)) {
            // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Authorization", oldRedisToken);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
            return true;
        }
        // 判断Redis中RefreshToken是否存在
        if (redisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + uid + ":" + serviceType)) {
            // Redis中RefreshToken还存在,获取RefreshToken的时间戳
            String currentTimeMillisRedis = redisUtil.getValue(Constant.PREFIX_SHIRO_REFRESH_TOKEN + uid + ":" + serviceType).toString();
            // 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
            if (JwtUtil.getClaim(token, Constant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
                // 获取当前最新时间戳
                String currentTimeMillis = String.valueOf(System.currentTimeMillis());
                // 读取配置文件,获取refreshTokenExpireTime属性
                // 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
                redisUtil.setValue(Constant.PREFIX_SHIRO_REFRESH_TOKEN + uid + ":" + serviceType,
                        currentTimeMillis, refreshTokenExpireTime);
                // 刷新AccessToken,设置时间戳为当前最新时间戳
                String newToken = JwtUtil.sign(uid, currentTimeMillis, serviceType);
                //并发请求下会造成token过期
                redisUtil.setValue("token_blacklist:" + token + ":" + serviceType, newToken, 50L);
                // 将新刷新的AccessToken再次进行Shiro的登录
                JwtToken jwtToken = new JwtToken(newToken);
                // 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
                this.getSubject(request, response).login(jwtToken);
                // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader("Authorization", newToken);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                return true;
            }
        }
        return false;
    }

    /**
     * 无需转发,直接返回Response信息
     */
    private void response401(ServletRequest req, ServletResponse resp, String msg) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setStatus(HttpStatus.OK.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()) {
            String data = JsonConvertUtil.objectToJson(new RestResponse(HttpStatus.UNAUTHORIZED.value(), "无权访问:" + msg, null));
            out.append(data);
        } catch (IOException e) {
            log.error("直接返回Response信息出现IOException异常:" + e.getMessage());
            // throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage());
        }
    }

    /**
     * 对跨域提供支持
     */
    @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"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

5、全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)

package com.example.demo.exception;

import com.example.demo.result.RestResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;


/**
 * @author LST
 * @version 1.0
 * @Description: 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)
 * @date 2019-12-31 16:22
 */
@Slf4j
public class BaseControllerExceptionHandler {

    /**
     * 捕捉所有Shiro异常
     *
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    @ResponseBody
    public RestResponse<String> handle401(ShiroException e) {
        log.error("无权访问==========message:{}", e.getMessage());
        return new RestResponse(HttpStatus.UNAUTHORIZED.value(), "登录已过期", "");
    }

    /**
     * 单独捕捉Shiro(UnauthorizedException)异常
     * 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
     *
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public RestResponse<String> handle401(UnauthorizedException e) {
        log.error("无权访问:当前Subject没有此请求所需权限==========message:{}", e.getMessage());
        return new RestResponse(HttpStatus.UNAUTHORIZED.value(), "无权访问", "");
    }

    /**
     * 单独捕捉Shiro(UnauthenticatedException)异常
     * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
     *
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthenticatedException.class)
    @ResponseBody
    public RestResponse<String> handle401(UnauthenticatedException e) {
        log.error("无权访问:当前Subject是匿名Subject,请先登录==========message:{}", e.getMessage());
        return new RestResponse(HttpStatus.UNAUTHORIZED.value(), "无权访问", "");
    }

    /**
     * 捕捉校验异常(MethodArgumentNotValidException)
     *
     * @return
     */

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public RestResponse validException(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        String message = fieldErrors.get(0).getDefaultMessage();
        return new RestResponse(ServiceExceptionEnum.ARGUMENT_ERROR.getCode(), message, "");
    }


    /**
     * 捕捉404异常
     *
     * @return
     */
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseBody
    public RestResponse<String> handle(NoHandlerFoundException e) {
        log.error("404异常==========message:{}", e.getMessage());
        return new RestResponse(HttpStatus.NOT_FOUND.value(), e.getMessage(), "");
    }

    /**
     * 系统全局异常
     *
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public RestResponse<Exception> defaultErrorHandler(HttpServletRequest req, Exception e) {
        RestResponse<Exception> response;
        log.error("occur bie error-->>errorUrl:{}----->>>errorMessage:{}", req.getRequestURL(), e);
        //数据库异常
        if (e instanceof DataIntegrityViolationException) {
            response = new RestResponse<>(ServiceExceptionEnum.SERVER_ERROR.getCode(), ServiceExceptionEnum.SERVER_ERROR.getMessage(), null);
            return response;
        }
        String msg = "发生错误请联系管理员";
        if(StringUtils.isBlank(e.getMessage())){
            response = new RestResponse<>(ServiceExceptionEnum.SERVER_ERROR.getCode(), msg, null);
        }else{
            response = new RestResponse<>(ServiceExceptionEnum.SERVER_ERROR.getCode(), e.getMessage(), null);
        }
        //运行时异常
        return response;
    }

    /**
     * 自定义异常
     * @param req
     * @param e
     * @return
     */
    @ResponseBody
    @ExceptionHandler(SXException.class)
    public RestResponse handle(HttpServletRequest req, SXException e) {
        log.error("自定义异常==========message:{},errorUrl:{}", e.getMessage(), req.getRequestURL());
        return new RestResponse(e.getCode(), e.getMessage(), "");
    }

    /**
     * 参数校验异常
     *
     * @param e
     * @return
     */
    @ResponseBody
    @ExceptionHandler(ConstraintViolationException.class)
    public RestResponse constraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator();
        List<String> msgList = new ArrayList<>();
        while (iterator.hasNext()) {
            ConstraintViolation<?> cvl = iterator.next();
            msgList.add(cvl.getMessageTemplate());
        }
        return new RestResponse(ServiceExceptionEnum.ERROR_IN_PARAM.getCode(),
                ServiceExceptionEnum.ERROR_IN_PARAM.getMessage(), msgList);
    }
}

6、登录接口

       1)LoginController登录控制层。

package com.example.demo.controller;

import com.example.demo.result.RestResponse;
import com.example.demo.service.LoginService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;


/**
 * @author LST
 * @version 1.0
 * @Description: 登录
 * @date 2019-12-31 16:22
 */
@RestController
@RequestMapping("/user")
@Api(value = "LoginController", tags = "登录")
public class LoginController {

    @Autowired
    private LoginService loginService;

    /**
     * 登录
     * @param loginName 登录名
     * @param passWord 密码
     * @param request
     * @param response
     * @return
     */
    @PostMapping(value = "/login", produces = "application/json; charset=utf-8")
    @ApiOperation(value = "登录授权", notes = "登录授权", code = 200, produces = "application/json")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "string", name = "loginName", value = "登录名"),
            @ApiImplicitParam(paramType = "query", dataType = "string", name = "passWord", value = "密码")})
    public RestResponse<Map<String, Object>> login(@RequestParam(name = "loginName", required = true) String loginName,@RequestParam(name = "passWord", required = true) String passWord,
                                                   HttpServletRequest request, HttpServletResponse response) {
        return loginService.login(loginName,passWord, request, response);

    }
}

2)LoginServiceImpl登录实现层。

package com.example.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.constants.Constant;
import com.example.demo.entity.User;
import com.example.demo.exception.ServiceExceptionEnum;
import com.example.demo.mapper.UserMapper;
import com.example.demo.result.RestResponse;
import com.example.demo.result.ResultGenerator;
import com.example.demo.service.LoginService;
import com.example.demo.utils.AesCipherUtil;
import com.example.demo.utils.JwtUtil;
import com.example.demo.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @author LST
 * @version 1.0
 * @Description: 登录
 * @date 2019-12-31 16:22
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * RefreshToken过期时间
     */
    @Value("${shiro.refreshTokenExpireTime}")
    private long refreshTokenExpireTime;


    /**
     * 登录
     * @param loginName 登录名
     * @param passWord 密码
     * @param httpServletResponse
     * @return
     */
    @Override
    public RestResponse<Map<String, Object>> login(String loginName, String passWord, HttpServletRequest request, HttpServletResponse httpServletResponse) {
        // 查询数据库中的帐号信息
        QueryWrapper<User> wrapper = new QueryWrapper();
        wrapper.lambda().eq(User::getLoginName, loginName);
        User user = userMapper.selectOne(wrapper);
        if (user == null) {
            return ResultGenerator.genFailResult(ServiceExceptionEnum.USER_NOT_EXIST);
        }
        // 密码进行AES解密
        String password = AesCipherUtil.enCrypto(loginName + passWord);
        // 因为密码加密是以帐号+密码的形式进行加密的,所以解密后的对比是帐号+密码(loginFlag=1 免密登录)
        if (password.equals(user.getPassword())) {
            // 清除可能存在的Shiro权限信息缓存
            if (redisUtil.exists(Constant.PREFIX_SHIRO_CACHE + user.getId())) {
                redisUtil.deleteValue(Constant.PREFIX_SHIRO_CACHE + user.getId());
            }
            //1 pc端 2移动端
            String serviceType = "1";
            if (!StringUtils.isEmpty(request.getHeader("serviceType"))) {
                serviceType = request.getHeader("serviceType");
            }
            // 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
            String currentTimeMillis = String.valueOf(System.currentTimeMillis());
            redisUtil.setValue(Constant.PREFIX_SHIRO_REFRESH_TOKEN + user.getId() + ":" + serviceType, currentTimeMillis, refreshTokenExpireTime);

            //把用户设置到redis中,不需要重复查询
            redisUtil.setValue(Constant.PREFIX_SHIRO_USER + user.getId(), user, refreshTokenExpireTime);

            // 从Header中Authorization返回AccessToken,时间戳为当前时间戳
            String token = JwtUtil.sign(user.getId(), currentTimeMillis, serviceType);
            httpServletResponse.setHeader("Authorization", token);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
            Map<String, Object> result = new HashMap(16);

            result.put("userName", user.getUserName());
            return new RestResponse(HttpStatus.OK.value(), "登录成功", result);
        } else {
            return ResultGenerator.genFailResult(ServiceExceptionEnum.USER_PASSWORD_ERROR);
        }
    }
}

7、相关类。

非常重要的还有AesCipherUtil(AES加密解密工具类)、CustomCache(重写Shiro的Cache保存读取)、CustomCacheManager(重写Shiro缓存管理器)、Base64ConvertUtil(Base64工具)。

8、测试

     1)通过http://127.0.0.1:8090/user/82d50ae31e435ceaa8b2bf030cc95363访问查询用户详情,通过postMan测试提示{ "msg": "无权访问:请先登录","statusCode": 401};说明拦截未登录成功了。那接下来就先调用登录接口获取Authorization字段的token,然后通过这个接口的请求头加上Authorization给服务的鉴权。

     2)通过http://127.0.0.1:8090/user/login?loginName=lst&passWord=1登录接口获取当前用户的验证和返回token。

         3)可以看到上图中已经返回了Authorization的token。再看看控制台和redis保存的数据。

  4)已经获取到了token,那么就刚才查询用户信息将request的headers加上Authorization的token即可。

我的项目目录结构

能访问证明成功,如果你看到,希望能够给我一些建议,感谢!!!

相关源码链接:https://download.csdn.net/download/qq_33612228/12076675

发布了13 篇原创文章 · 获赞 7 · 访问量 584

猜你喜欢

转载自blog.csdn.net/qq_33612228/article/details/103815054