springboot+shiro+jwt+redis+cache实现无状态token登录

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stilll123456/article/details/88370355

一、前言

网上关于shiro的整合文章不少,但很多并不适用于前后端分离/移动端的项目

  1. shiro默认的拦截跳转都是跳转url页面,这在前后端分离的项目中显然行不通
  2. shiro默认使用session做登录校验,分离后当然这也是不推荐的。

强行使用也可以,但是就必须做其它大量的工作,除了需要解决刚提到的跳转路径问题,其它的如跨域sessionId问题、或需修改为传递sessionId做shiro登录校验,以及集群下session共享问题。emmm,还有session机制本身的安全问题等等。
这显然是费时费力的,那么有没有其它的办法可以解决呢?

答案当然是有的,我们可以在整合shiro的基础上继续整合jwt,或者oauth2.0等,或者自定义登录校验,使其成为支持服务端无状态登录,即token登录。

ps:该篇文章在实现过程中参考了不少资料,实现匆忙未记录下来,如有侵权请与我联系

Here we go…


二、相关说明

◇ Shiro + Java-JWT实现无状态鉴权机制

  1. 首先post用户名与密码到login进行登入,如果成功返回一个加密的Authorization,失败的话直接返回10001未登录等状态码,以后访问都带上这个Authorization即可。
  2. 鉴权流程主要是重写了shiro的入口过滤器JwtFilter(BasicHttpAuthenticationFilter),判断请求Header里面是否包含Authorization字段,有就进行shiro的token登录认证授权(用户访问每一个需要权限的请求必须在Header中添加Authorization字段存放AccessToken),没有就以游客直接访问(有权限管控的话,以游客访问就会被拦截)

◇ 关于Redis中保存RefreshToken信息(做到JWT的可控性)

  1. 登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证。
  2. Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户

◇ 关于根据RefreshToken自动刷新AccessToken

  1. 本身AccessToken的过期时间为5分钟(配置文件可配置),RefreshToken过期时间为30分钟(配置文件可配置),当登录后时间过了5分钟之后,当前AccessToken便会过期失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过期,开始判断是否要进行AccessToken刷新,首先redis查询RefreshToken是否存在,以及时间戳和过期AccessToken所携带的时间戳是否一致,如果存在且一致就进行AccessToken刷新。
  2. 刷新后新的AccessToken过期时间依旧为5分钟(配置文件可配置),时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期(配置文件可配置),最终将刷新的AccessToken存放在Response的Header中的Authorization字段返回。
  3. 同时前端进行获取替换,下次用新的AccessToken进行访问

三、主要配置

◇ maven依赖

此处搭建了maven子项目,使用的spring boot版本为较新的2.1.2,主要依赖如下:

		<!-- web模块 -->
		<dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-web</artifactId>
 		</dependency>
	
		<!-- 核心模块,包括自动配置支持、日志和YAML -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
	
		<!-- 浏览模块,包括JUnit、Hamcrest、Mockito -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<!-- redis -->
		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		
		<!-- mysql连接器 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		
		<!-- ali druid连接池 -->
		<dependency>
		    <groupId>com.alibaba</groupId>
		    <artifactId>druid-spring-boot-starter</artifactId>
		    <version>1.1.14</version>
		</dependency>
		
		<!-- myBatis -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.0.0</version>
		</dependency>
		
		<!-- shiro -->
		<dependency>
		    <groupId>org.apache.shiro</groupId>
		    <artifactId>shiro-spring</artifactId>
		    <version>1.4.0</version>
		</dependency>
		
		<!-- ali json -->
		<dependency>
		    <groupId>com.alibaba</groupId>
		    <artifactId>fastjson</artifactId>
		    <version>1.2.56</version>
		</dependency>
		
		<!-- jwt -->
		<dependency>
		    <groupId>com.auth0</groupId>
		    <artifactId>java-jwt</artifactId>
		    <version>3.7.0</version>
		</dependency>


◇ application.properties配置

## tomcat配置 - start
# 指定服务端口
server.port=8999
## tomcat配置 - end

# 禁止对外提供Spring MBeans
# spring.jmx.enabled=false

## 数据库配置 - start
#spring.datasource.url=jdbc:mysql://localhost:3306/springboot-test?serverTimezone=GMT%2B8
spring.datasource.url=jdbc:mysql://192.168.5.58:3306/181_saas?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## 数据库配置 - end

# druid连接池配置 - start
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.filters=stat
spring.datasource.druid.max-active=20
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=select 'x'
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-open-prepared-statements=20
# druid连接池配置 - end

## myBatis - start
# 打印SQL语句
mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
## myBatis - end

## Redis配置 - start
# Redis数据库索引(默认为0)
spring.redis.database=1
# Redis服务器地址
spring.redis.host=192.168.5.58
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=yibayi_181~jishubu*007
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000
## Redis配置 - end

## 其它参数配置 - start
# AES密码加密私钥(Base64加密)
encryptAESKey=V2FuZzkyNuYSKIuwqTQkFQSUpXVA
# JWT认证加密私钥(Base64加密)
encryptJWTKey=U0JBUElOENhspJrzkyNjQ1NA
# AccessToken过期时间-5分钟-5*60(秒为单位)
accessTokenExpireTime=300
# RefreshToken过期时间-30分钟-30*60(秒为单位)
refreshTokenExpireTime=1800
# Shiro缓存过期时间-5分钟-5*60(秒为单位)(一般设置与AccessToken过期时间一致)
shiroCacheExpireTime=300
## 其它参数配置 - end

## 时间格式配置 - start
# spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
# spring.jackson.time-zone=GMT+8
## 时间格式配置 - end

## log配置 - start
logging.path=/data/tomcat_log/181-saas/181-saas_log_error.log
logging.level.com.favorites=DEBUG
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=ERROR
## log配置 - end

四、代码实现

ps:关于shiro/jwt/redis等的基础知识不在该篇文章的探讨范围,如有不清楚的请自行百度

◇ 统一Josn封装

既然是前后台分离的项目,那么首先返回的对象就必须要有统一,封装如下:

package com.yby.saas.po.vo;

import java.io.Serializable;

import com.alibaba.fastjson.JSONObject;
import com.yby.saas.po.constant.StatusCode;

/***
 * JSON封装
 * 
 * @author lwx
 */
public class JsonVo implements Serializable {

	private static final long serialVersionUID = 8178937610421199532L;

	/**
	 * 请求标识,默认为失败状态
	 */
	private boolean success = false;

	/**
	 * 状态码,默认为失败状态
	 */
	private Integer code = StatusCode.ERROR;

	/***
	 * 操作信息
	 */
	private String msg;

	/**
	 * 返回数据
	 */
	private Object obj = new JSONObject();

	/**
	 * 成功响应
	 */
	public void OK() {
		this.success = true;
		this.code = StatusCode.SUCCESS;
	}

	/**
	 * 请求成功,但业务逻辑处理不通过
	 */
	public void NO() {
		this.success = true;
		this.code = StatusCode.ERROR;
	}

	public JsonVo() {
		super();
	}

	public JsonVo(int code, String msg) {
		super();
		this.success = true;
		this.code = code;
		this.msg = msg;
	}

	public boolean isSuccess() {
		return success;
	}

	public void setSuccess(boolean success) {
		this.success = success;
	}

	public Integer getCode() {
		return code;
	}

	public void setCode(Integer code) {
		this.success = true;
		this.code = code;
	}

	public Object getObj() {
		return obj;
	}

	public void setObj(Object obj) {
		this.success = true;
		this.code = StatusCode.SUCCESS;
		if (obj == null) {
			obj = new JSONObject();
		}
		this.obj = obj;
	}

	public String getMsg() {
		return msg;
	}

	public void setMsg(String msg) {
		this.msg = msg;
	}

	@Override
	public String toString() {
		return "JsonVo [success=" + success + ", code=" + code + ", msg=" + msg + ", obj=" + obj + "]";
	}

}

◇ ShiroConfig

注:这里注意下关于JwtFilter的配置,由于spring boot的加载顺序原因,spring boot中的filter默认情况下是无法注入bean的,因此此处在ShiroConfig下做了配置注入。

package com.yby.saas.config.shiro;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;

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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import com.yby.saas.config.jwt.JwtFilter;
import com.yby.saas.config.shiro.cache.CustomCacheManager;

/**
 * Shiro配置
 * 
 * @author lwx
 * @date 2019/03/08
 */
@Configuration
public class ShiroConfig {

	/**
	 * 配置使用自定义Realm,关闭Shiro自带的session 详情见文档
	 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
	 */
	@Bean("securityManager")
	public DefaultWebSecurityManager getManager(UserRealm userRealm) {
		DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
		// 使用自定义Realm
		manager.setRealm(userRealm);
		// 关闭Shiro自带的session
		DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
		DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
		defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
		subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
		manager.setSubjectDAO(subjectDAO);
		// 设置自定义Cache缓存
		manager.setCacheManager(new CustomCacheManager());
		return manager;
	}

	/**
	 * 添加自己的过滤器,自定义url规则 详情见文档 http://shiro.apache.org/web.html#urls-
	 */
	@Bean("shiroFilter")
	public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
		ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
		// 添加自己的过滤器取名为jwt
		Map<String, Filter> filterMap = new HashMap<>(16);
		filterMap.put("jwtFilter", jwtFilterBean());
		factoryBean.setFilters(filterMap);
		factoryBean.setSecurityManager(securityManager);
		// 自定义url规则
		Map<String, String> filterRuleMap = new HashMap<>(16);
		// 所有请求通过我们自己的JWTFilter
		filterRuleMap.put("/**", "jwtFilter");
		factoryBean.setFilterChainDefinitionMap(filterRuleMap);
		return factoryBean;
	}

	/**
	 * <pre>
	 * 注入bean,此处应注意: 
	 * 
	 * (1)代码顺序,应放置于shiroFilter后面,否则报错:
	 * 	No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.
	 *	ThreadContext or as a vm static singleton. This is an invalid application configuration.
	 * 
	 * (2)如不在此注册,在filter中将无法正常注入bean
	 * </pre>
	 */
	@Bean("jwtFilter")
	public JwtFilter jwtFilterBean() {
		return new JwtFilter();
	}

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


◇ JwtFilter

既然我们需要更改shiro默认的登录拦截,那首先就需得重写shiro中的BasicHttpAuthenticationFilter,此处使用jwt做登录拦截

package com.yby.saas.config.jwt;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.po.constant.StatusCode;
import com.yby.saas.po.vo.JsonVo;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;
import com.yby.saas.util.common.JsonConvertUtil;

/**
 * JWT过滤
 * 
 * @author lwx
 * @date 2019/03/09
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

	@Value("${refreshTokenExpireTime}")
	private String refreshTokenExpireTime;

	@Autowired
	private RedisClient redis;

	/**
	 * LOGGER
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(JwtFilter.class);

	/**
	 * 这里我们详细说明下为什么最终返回的都是true,即允许访问 例如我们提供一个地址 GET /article 登入用户和游客看到的内容是不同的
	 * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西 所以我们在这里返回true,Controller中可以通过
	 * subject.isAuthenticated() 来判断用户是否登入
	 * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
	 * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
		// 判断用户是否想要登入
		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 != null && throwable instanceof SignatureVerificationException) {
					// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
					msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
				} else if (throwable != null && throwable instanceof TokenExpiredException) {
					// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
					if (this.refreshToken(request, response)) {
						return true;
					} else {
						msg = "token已过期(" + throwable.getMessage() + ")";
					}
				} else {
					// 应用异常不为空
					if (throwable != null) {
						// 获取应用异常msg
						msg = throwable.getMessage();
					}
				}
				/**
				 * 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2.
				 * 无需转发,直接返回Response信息 一般使用第二种(更方便)
				 */
				// 直接返回Response信息
				this.response401(request, response, msg);
				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登录认证授权
	 */
	@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;
	}

	/**
	 * 此处为AccessToken刷新,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
	 */
	private boolean refreshToken(ServletRequest request, ServletResponse response) {
		// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
		String token = this.getAuthzHeader(request);
		// 获取当前Token的帐号信息
		String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
		// 判断Redis中RefreshToken是否存在
		if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
			// Redis中RefreshToken还存在,获取RefreshToken的时间戳
			String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
			// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
			if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				// 获取当前最新时间戳
				String currentTimeMillis = String.valueOf(System.currentTimeMillis());
				// 读取配置文件,获取refreshTokenExpireTime属性
				// PropertiesUtil.readProperties("config.properties");
				// String refreshTokenExpireTime =
				// PropertiesUtil.getProperty("refreshTokenExpireTime");
				// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
				redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
						Integer.parseInt(refreshTokenExpireTime));
				// 刷新AccessToken,设置时间戳为当前最新时间戳
				token = JwtUtil.sign(account, currentTimeMillis);
				// 将新刷新的AccessToken再次进行Shiro的登录
				JwtToken jwtToken = new JwtToken(token);
				// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
				this.getSubject(request, response).login(jwtToken);
				// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
				HttpServletResponse httpServletResponse = (HttpServletResponse) response;
				httpServletResponse.setHeader("Authorization", token);
				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.UNAUTHORIZED.value());
		httpServletResponse.setCharacterEncoding("UTF-8");
		httpServletResponse.setContentType("application/json; charset=utf-8");
		PrintWriter out = null;
		try {
			out = httpServletResponse.getWriter();
			String data = JsonConvertUtil.objectToJson(new JsonVo(StatusCode.NOT_LOGIN, "无权访问(Unauthorized):" + msg));
			out.append(data);
		} catch (IOException e) {
			LOGGER.error("直接返回Response信息出现IOException异常:" + e.getMessage());
			throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage());
		} finally {
			if (out != null) {
				out.close();
			}
		}
	}

	/**
	 * 对跨域提供支持
	 */
	@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);
	}
}

package com.yby.saas.config.jwt;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * JwtToken
 * 
 * @author lwx
 */
public class JwtToken implements AuthenticationToken {

	private static final long serialVersionUID = 1900286977895826147L;

	/**
	 * Token
	 */
	private String token;

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

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

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


◇ 自定义Realm

package com.yby.saas.config.shiro;

import java.util.List;

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.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.yby.saas.config.jwt.JwtToken;
import com.yby.saas.dao.permission.PermissionCustomMapper;
import com.yby.saas.dao.role.RoleCustomMapper;
import com.yby.saas.dao.user.UserCustomMapper;
import com.yby.saas.po.Permission;
import com.yby.saas.po.Role;
import com.yby.saas.po.User;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;
import com.yby.saas.util.common.StringUtil;

/**
 * 自定义Realm
 * 
 * @author lwx
 * @date 2019/03/08
 */
@Service
public class UserRealm extends AuthorizingRealm {

	@Autowired
	private RedisClient redis;

	@Autowired
	private UserCustomMapper userMapper;

	@Autowired
	private RoleCustomMapper roleMapper;

	@Autowired
	private PermissionCustomMapper permissionMapper;

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

	/**
	 * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		String account = JwtUtil.getClaim(principals.toString(), JwtConstant.ACCOUNT);
		// 查询用户角色
		List<Role> roles = roleMapper.getRoleByMobile(account);
		for (int i = 0, roleLen = roles.size(); i < roleLen; i++) {
			Role role = roles.get(i);
			// 添加角色
			simpleAuthorizationInfo.addRole(role.getName());
			// 根据用户角色查询权限
			List<Permission> permissions = permissionMapper.getPermissionByRoleId(role.getId());
			for (int j = 0, perLen = permissions.size(); j < perLen; j++) {
				Permission permission = permissions.get(j);
				// 添加权限
				simpleAuthorizationInfo.addStringPermission(permission.getSn());
			}
		}
		return simpleAuthorizationInfo;
	}

	/**
	 * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
		String token = (String) auth.getCredentials();
		// 解密获得account,用于和数据库进行对比
		String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
		// 帐号为空
		if (StringUtil.isBlank(account)) {
			throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
		}
		// 查询用户是否存在
		User user = userMapper.getByMobile(account);
		if (user == null) {
			throw new AuthenticationException("该帐号不存在(The account does not exist.)");
		}
		// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
		if (JwtUtil.verify(token) && redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
			// 获取RefreshToken的时间戳
			String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
			// 获取AccessToken时间戳,与RefreshToken的时间戳对比
			if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				return new SimpleAuthenticationInfo(token, token, "userRealm");
			}
		}
		throw new AuthenticationException("Token已过期(Token expired or incorrect.)");
	}
}


◇ 重写shiro cache

package com.yby.saas.config.shiro.cache;

import java.util.Collection;
import java.util.Set;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;

/**
 * 重写Shiro的Cache保存读取
 * 
 * @author lwx
 */
public class CustomCache<K, V> implements Cache<K, V> {

	@Value("${shiroCacheExpireTime}")
	private String shiroCacheExpireTime;

	@Autowired
	private RedisClient redis;

	/**
	 * 缓存的key名称获取为shiro:cache:account
	 * 
	 * @param key
	 * @return java.lang.String
	 * @author Wang926454
	 * @date 2018/9/4 18:33
	 */
	private String getKey(Object key) {
		return RedisConstant.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), JwtConstant.ACCOUNT);
	}

	/**
	 * 获取缓存
	 */
	@Override
	public Object get(Object key) throws CacheException {
		if (!redis.hasKey(this.getKey(key))) {
			return null;
		}
		return redis.get(this.getKey(key));
	}

	/**
	 * 保存缓存
	 */
	@Override
	public Object put(Object key, Object value) throws CacheException {
		// 读取配置文件,获取Redis的Shiro缓存过期时间
		// PropertiesUtil.readProperties("config.properties");
		// String shiroCacheExpireTime =
		// PropertiesUtil.getProperty("shiroCacheExpireTime");
		// 设置Redis的Shiro缓存
		return redis.set(this.getKey(key), value, Integer.parseInt(shiroCacheExpireTime));
	}

	/**
	 * 移除缓存
	 */
	@Override
	public Object remove(Object key) throws CacheException {
		if (!redis.hasKey(this.getKey(key))) {
			return null;
		}
		redis.del(this.getKey(key));
		return null;
	}

	/**
	 * 清空所有缓存
	 */
	@Override
	public void clear() throws CacheException {
		// TODO Auto-generated method stub

	}

	/**
	 * 缓存的个数
	 */
	@Override
	public Set<K> keys() {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * 获取所有的key
	 */
	@Override
	public int size() {
		// TODO Auto-generated method stub
		return 0;
	}

	/**
	 * 获取所有的value
	 */
	@Override
	public Collection<V> values() {
		// TODO Auto-generated method stub
		return null;
	}
}


◇ 重写shiro CacheManager

package com.yby.saas.config.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

/**
 * 重写Shiro缓存管理器
 * 
 * @author lwx
 */
public class CustomCacheManager implements CacheManager {
	@Override
	public <K, V> Cache<K, V> getCache(String s) throws CacheException {
		return new CustomCache<K, V>();
	}
}


◇ RedisConfig

package com.yby.saas.config.redis;

import java.lang.reflect.Method;

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.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Redis缓存配置
 * 
 * @author lwx
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

	@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.getName());
				if (params != null && params.length > 0 && params[0] != null) {
					for (Object obj : params) {
						sb.append(obj.toString());
					}
				}
				return sb.toString();
			}
		};
	}

	/**
	 * RedisTemplate
	 */
	@Bean
	@SuppressWarnings("all")
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

		RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
		template.setConnectionFactory(factory);
		Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

		ObjectMapper om = new ObjectMapper();
		om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
		om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

		jackson2JsonRedisSerializer.setObjectMapper(om);
		StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

		// key采用String的序列化方式
		template.setKeySerializer(stringRedisSerializer);
		// hash的key也采用String的序列化方式
		template.setHashKeySerializer(stringRedisSerializer);
		// value序列化方式采用jackson
		template.setValueSerializer(jackson2JsonRedisSerializer);
		// hash的value序列化方式采用jackson
		template.setHashValueSerializer(jackson2JsonRedisSerializer);
		template.afterPropertiesSet();

		return template;

	}
}

◇ RedisClient工具类

package com.yby.saas.redis;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

/**
 * <pre>
 * RedisTemplate工具类:
 * 	针对所有的hash都是以h开头的方法
 * 	针对所有的Set都是以s开头的方法(不含通用方法)
 * 	针对所有的List都是以l开头的方法
 * </pre>
 * 
 * @author lwx
 */
@Component
public class RedisClient {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate;

	public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
		this.redisTemplate = redisTemplate;
	}

	// =============================common============================
	/**
	 * 指定缓存失效时间
	 * 
	 * @param key
	 *            键
	 * @param time
	 *            时间(秒)
	 * @return
	 */
	public boolean expire(String key, long time) {
		try {
			if (time > 0) {
				redisTemplate.expire(key, time, TimeUnit.SECONDS);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 根据key 获取过期时间
	 * 
	 * @param key
	 *            键 不能为null
	 * @return 时间(秒) 返回0代表为永久有效
	 */
	public long getExpire(String key) {
		return redisTemplate.getExpire(key, TimeUnit.SECONDS);
	}

	/**
	 * 判断key是否存在
	 * 
	 * @param key
	 *            键
	 * @return true 存在 false不存在
	 */
	public boolean hasKey(String key) {
		try {
			return redisTemplate.hasKey(key);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 删除缓存
	 * 
	 * @param key
	 *            可以传一个值 或多个
	 */
	@SuppressWarnings("unchecked")
	public void del(String... key) {
		if (key != null && key.length > 0) {
			if (key.length == 1) {
				redisTemplate.delete(key[0]);
			} else {
				redisTemplate.delete(CollectionUtils.arrayToList(key));
			}
		}
	}

	// ============================String=============================
	/**
	 * 普通缓存获取
	 * 
	 * @param key
	 *            键
	 * @return 值
	 */
	public Object get(String key) {
		return key == null ? null : redisTemplate.opsForValue().get(key);
	}

	/**
	 * 普通缓存放入
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @return true成功 false失败
	 */
	public boolean set(String key, Object value) {
		try {
			redisTemplate.opsForValue().set(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 普通缓存放入并设置时间
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒) time要大于0 如果time小于等于0 将设置无限期
	 * @return true成功 false 失败
	 */
	public boolean set(String key, Object value, long time) {
		try {
			if (time > 0) {
				redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
			} else {
				set(key, value);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 递增
	 * 
	 * @param key
	 *            键
	 * @param by
	 *            要增加几(大于0)
	 * @return
	 */
	public long incr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递增因子必须大于0");
		}
		return redisTemplate.opsForValue().increment(key, delta);
	}

	/**
	 * 递减
	 * 
	 * @param key
	 *            键
	 * @param by
	 *            要减少几(小于0)
	 * @return
	 */
	public long decr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递减因子必须大于0");
		}
		return redisTemplate.opsForValue().increment(key, -delta);
	}

	// ================================Map=================================
	/**
	 * HashGet
	 * 
	 * @param key
	 *            键 不能为null
	 * @param item
	 *            项 不能为null
	 * @return 值
	 */
	public Object hget(String key, String item) {
		return redisTemplate.opsForHash().get(key, item);
	}

	/**
	 * 获取hashKey对应的所有键值
	 * 
	 * @param key
	 *            键
	 * @return 对应的多个键值
	 */
	public Map<Object, Object> hmget(String key) {
		return redisTemplate.opsForHash().entries(key);
	}

	/**
	 * HashSet
	 * 
	 * @param key
	 *            键
	 * @param map
	 *            对应多个键值
	 * @return true 成功 false 失败
	 */
	public boolean hmset(String key, Map<String, Object> map) {
		try {
			redisTemplate.opsForHash().putAll(key, map);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * HashSet 并设置时间
	 * 
	 * @param key
	 *            键
	 * @param map
	 *            对应多个键值
	 * @param time
	 *            时间(秒)
	 * @return true成功 false失败
	 */
	public boolean hmset(String key, Map<String, Object> map, long time) {
		try {
			redisTemplate.opsForHash().putAll(key, map);
			if (time > 0) {
				expire(key, time);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 向一张hash表中放入数据,如果不存在将创建
	 * 
	 * @param key
	 *            键
	 * @param item
	 *            项
	 * @param value
	 *            值
	 * @return true 成功 false失败
	 */
	public boolean hset(String key, String item, Object value) {
		try {
			redisTemplate.opsForHash().put(key, item, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 向一张hash表中放入数据,如果不存在将创建
	 * 
	 * @param key
	 *            键
	 * @param item
	 *            项
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
	 * @return true 成功 false失败
	 */
	public boolean hset(String key, String item, Object value, long time) {
		try {
			redisTemplate.opsForHash().put(key, item, value);
			if (time > 0) {
				expire(key, time);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 删除hash表中的值
	 * 
	 * @param key
	 *            键 不能为null
	 * @param item
	 *            项 可以使多个 不能为null
	 */
	public void hdel(String key, Object... item) {
		redisTemplate.opsForHash().delete(key, item);
	}

	/**
	 * 判断hash表中是否有该项的值
	 * 
	 * @param key
	 *            键 不能为null
	 * @param item
	 *            项 不能为null
	 * @return true 存在 false不存在
	 */
	public boolean hHasKey(String key, String item) {
		return redisTemplate.opsForHash().hasKey(key, item);
	}

	/**
	 * hash递增 如果不存在,就会创建一个 并把新增后的值返回
	 * 
	 * @param key
	 *            键
	 * @param item
	 *            项
	 * @param by
	 *            要增加几(大于0)
	 * @return
	 */
	public double hincr(String key, String item, double by) {
		return redisTemplate.opsForHash().increment(key, item, by);
	}

	/**
	 * hash递减
	 * 
	 * @param key
	 *            键
	 * @param item
	 *            项
	 * @param by
	 *            要减少记(小于0)
	 * @return
	 */
	public double hdecr(String key, String item, double by) {
		return redisTemplate.opsForHash().increment(key, item, -by);
	}

	// ============================set=============================
	/**
	 * 根据key获取Set中的所有值
	 * 
	 * @param key
	 *            键
	 * @return
	 */
	public Set<Object> sGet(String key) {
		try {
			return redisTemplate.opsForSet().members(key);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 根据value从一个set中查询,是否存在
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @return true 存在 false不存在
	 */
	public boolean sHasKey(String key, Object value) {
		try {
			return redisTemplate.opsForSet().isMember(key, value);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将数据放入set缓存
	 * 
	 * @param key
	 *            键
	 * @param values
	 *            值 可以是多个
	 * @return 成功个数
	 */
	public long sSet(String key, Object... values) {
		try {
			return redisTemplate.opsForSet().add(key, values);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 将set数据放入缓存
	 * 
	 * @param key
	 *            键
	 * @param time
	 *            时间(秒)
	 * @param values
	 *            值 可以是多个
	 * @return 成功个数
	 */
	public long sSetAndTime(String key, long time, Object... values) {
		try {
			Long count = redisTemplate.opsForSet().add(key, values);
			if (time > 0)
				expire(key, time);
			return count;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 获取set缓存的长度
	 * 
	 * @param key
	 *            键
	 * @return
	 */
	public long sGetSetSize(String key) {
		try {
			return redisTemplate.opsForSet().size(key);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 移除值为value的
	 * 
	 * @param key
	 *            键
	 * @param values
	 *            值 可以是多个
	 * @return 移除的个数
	 */
	public long setRemove(String key, Object... values) {
		try {
			Long count = redisTemplate.opsForSet().remove(key, values);
			return count;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
	// ===============================list=================================

	/**
	 * 获取list缓存的内容
	 * 
	 * @param key
	 *            键
	 * @param start
	 *            开始
	 * @param end
	 *            结束 0 到 -1代表所有值
	 * @return
	 */
	public List<Object> lGet(String key, long start, long end) {
		try {
			return redisTemplate.opsForList().range(key, start, end);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 获取list缓存的长度
	 * 
	 * @param key
	 *            键
	 * @return
	 */
	public long lGetListSize(String key) {
		try {
			return redisTemplate.opsForList().size(key);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}

	/**
	 * 通过索引 获取list中的值
	 * 
	 * @param key
	 *            键
	 * @param index
	 *            索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
	 * @return
	 */
	public Object lGetIndex(String key, long index) {
		try {
			return redisTemplate.opsForList().index(key, index);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 将list放入缓存
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒)
	 * @return
	 */
	public boolean lSet(String key, Object value) {
		try {
			redisTemplate.opsForList().rightPush(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒)
	 * @return
	 */
	public boolean lSet(String key, Object value, long time) {
		try {
			redisTemplate.opsForList().rightPush(key, value);
			if (time > 0)
				expire(key, time);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒)
	 * @return
	 */
	public boolean lSet(String key, List<Object> value) {
		try {
			redisTemplate.opsForList().rightPushAll(key, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 将list放入缓存
	 * 
	 * @param key
	 *            键
	 * @param value
	 *            值
	 * @param time
	 *            时间(秒)
	 * @return
	 */
	public boolean lSet(String key, List<Object> value, long time) {
		try {
			redisTemplate.opsForList().rightPushAll(key, value);
			if (time > 0)
				expire(key, time);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 根据索引修改list中的某条数据
	 * 
	 * @param key
	 *            键
	 * @param index
	 *            索引
	 * @param value
	 *            值
	 * @return
	 */
	public boolean lUpdateIndex(String key, long index, Object value) {
		try {
			redisTemplate.opsForList().set(key, index, value);
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	/**
	 * 移除N个值为value
	 * 
	 * @param key
	 *            键
	 * @param count
	 *            移除多少个
	 * @param value
	 *            值
	 * @return 移除的个数
	 */
	public long lRemove(String key, long count, Object value) {
		try {
			Long remove = redisTemplate.opsForList().remove(key, count, value);
			return remove;
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
	}
}

◇ 自定义异常

package com.yby.saas.exception;

/**
 * 自定义异常(CustomException)
 * 
 * @author lwx
 */
public class CustomException extends RuntimeException {

	private static final long serialVersionUID = -6736944294947154413L;

	public CustomException(String msg) {
		super(msg);
	}

	public CustomException() {
		super();
	}
}

package com.yby.saas.exception;

/**
 * 自定义401无权限异常(UnauthorizedException)
 */
public class CustomUnauthorizedException extends RuntimeException {

	private static final long serialVersionUID = -3993376696547776573L;

	public CustomUnauthorizedException(String msg) {
		super(msg);
	}

	public CustomUnauthorizedException() {
		super();
	}
}


◇ 自定义异常控制处理器

package com.yby.saas.config;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import com.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.StatusCode;
import com.yby.saas.po.vo.JsonVo;

/**
 * 异常控制处理器
 * 
 * @author lwx
 */
@RestControllerAdvice
public class ExceptionAdvice {

	/**
	 * 捕捉所有Shiro异常
	 */
	@ResponseStatus(HttpStatus.UNAUTHORIZED)
	@ExceptionHandler(ShiroException.class)
	public JsonVo handle401(ShiroException e) {
		JsonVo vo = new JsonVo();
		vo.setCode(StatusCode.UNLAWFUL);
		vo.setMsg("无权访问(Unauthorized):" + e.getMessage());
		return vo;
	}

	/**
	 * 单独捕捉Shiro(UnauthorizedException)异常 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
	 */
	@ResponseStatus(HttpStatus.UNAUTHORIZED)
	@ExceptionHandler(UnauthorizedException.class)
	public JsonVo handle401(UnauthorizedException e) {
		JsonVo vo = new JsonVo();
		vo.setCode(StatusCode.UNLAWFUL);
		vo.setMsg("无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")");
		return vo;
	}

	/**
	 * 单独捕捉Shiro(UnauthenticatedException)异常
	 * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
	 */
	@ResponseStatus(HttpStatus.UNAUTHORIZED)
	@ExceptionHandler(UnauthenticatedException.class)
	public JsonVo handle401(UnauthenticatedException e) {
		JsonVo vo = new JsonVo();
		vo.setCode(StatusCode.UNLAWFUL);
		vo.setMsg("无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
		return vo;
	}

	/**
	 * 捕捉校验异常(BindException)
	 */
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ExceptionHandler(BindException.class)
	public JsonVo validException(BindException e) {
		JsonVo vo = new JsonVo();
		List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
		Map<String, Object> result = this.getValidError(fieldErrors);
		vo.setCode(StatusCode.ERROR);
		vo.setMsg(result.get("errorMsg").toString());
		vo.setObj(result.get("errorList"));
		return vo;
	}

	/**
	 * 捕捉校验异常(MethodArgumentNotValidException)
	 */
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public JsonVo validException(MethodArgumentNotValidException e) {
		JsonVo vo = new JsonVo();
		List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
		Map<String, Object> result = this.getValidError(fieldErrors);
		vo.setCode(StatusCode.ERROR);
		vo.setMsg(result.get("errorMsg").toString());
		vo.setObj(result.get("errorList"));
		return vo;
	}

	/**
	 * 捕捉404异常
	 */
	@ResponseStatus(HttpStatus.NOT_FOUND)
	@ExceptionHandler(NoHandlerFoundException.class)
	public JsonVo handle(NoHandlerFoundException e) {
		JsonVo vo = new JsonVo();
		vo.setCode(StatusCode.NOT_FOUND);
		vo.setMsg(e.getMessage());
		return vo;
	}

	/**
	 * 捕捉其他所有异常
	 */
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ExceptionHandler(Exception.class)
	public JsonVo globalException(HttpServletRequest request, Throwable ex) {
		// return new JsonVo(this.getStatus(request).value(), ex.toString() + ": " +
		// ex.getMessage(), null);
		JsonVo vo = new JsonVo();
		vo.setCode(StatusCode.SERVER_ERROR);
		vo.setMsg(ex.toString() + ": " + ex.getMessage());
		return vo;
	}

	/**
	 * 捕捉其他所有自定义异常
	 */
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ExceptionHandler(CustomException.class)
	public JsonVo handle(CustomException e) {
		JsonVo vo = new JsonVo();
		vo.NO();
		vo.setMsg(e.getMessage());
		return vo;
	}

	/**
	 * 获取状态码
	 */
	private HttpStatus getStatus(HttpServletRequest request) {
		Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
		if (statusCode == null) {
			return HttpStatus.INTERNAL_SERVER_ERROR;
		}
		return HttpStatus.valueOf(statusCode);
	}

	/**
	 * 获取校验错误信息
	 */
	private Map<String, Object> getValidError(List<FieldError> fieldErrors) {
		Map<String, Object> result = new HashMap<String, Object>(16);
		List<String> errorList = new ArrayList<String>();
		StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
		for (FieldError error : fieldErrors) {
			errorList.add(error.getField() + "-" + error.getDefaultMessage());
			errorMsg.append(error.getField() + "-" + error.getDefaultMessage() + ".");
		}
		result.put("errorList", errorList);
		result.put("errorMsg", errorMsg);
		return result;
	}
}


◇ JWT工具类

package com.yby.saas.util;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.util.common.Base64Util;

/**
 * JAVA-JWT工具类
 * 
 * @author lwx
 */
@Component
public class JwtUtil {

	/**
	 * LOGGER
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtil.class);

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

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

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

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

	/**
	 * 校验token是否正确
	 * 
	 * @param token
	 *            Token
	 * @return boolean 是否正确
	 * @author Wang926454
	 * @date 2018/8/31 9:05
	 */
	public static boolean verify(String token) {
		try {
			// 帐号加JWT私钥解密
			String secret = getClaim(token, JwtConstant.ACCOUNT) + Base64Util.decodeThrowsException(encryptJWTKey);
			Algorithm algorithm = Algorithm.HMAC256(secret);
			JWTVerifier verifier = JWT.require(algorithm).build();
			DecodedJWT jwt = verifier.verify(token);
			return true;
		} catch (UnsupportedEncodingException e) {
			LOGGER.error("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
			throw new CustomException("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
		}
	}

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

	/**
	 * 生成签名
	 * 
	 * @param account
	 *            帐号
	 * @return java.lang.String 返回加密的Token
	 * @author Wang926454
	 * @date 2018/8/31 9:07
	 */
	public static String sign(String account, String currentTimeMillis) {
		try {
			// 帐号加JWT私钥加密
			String secret = account + Base64Util.decodeThrowsException(encryptJWTKey);
			// 此处过期时间是以毫秒为单位,所以乘以1000
			Date date = new Date(System.currentTimeMillis() + Long.parseLong(accessTokenExpireTime) * 1000);
			Algorithm algorithm = Algorithm.HMAC256(secret);
			// 附带account帐号信息
			return JWT.create().withClaim("account", account).withClaim("currentTimeMillis", currentTimeMillis)
					.withExpiresAt(date).sign(algorithm);
		} catch (UnsupportedEncodingException e) {
			LOGGER.error("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
			throw new CustomException("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
		}
	}
}


◇ AES工具类

package com.yby.saas.util;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.yby.saas.exception.CustomUnauthorizedException;
import com.yby.saas.util.common.Base64Util;
import com.yby.saas.util.common.HexConvertUtil;

/**
 * AES加密解密工具类
 * 
 * @author lwx
 */
@Component
public class AesUtil {

	/**
	 * AES密码加密私钥(Base64加密)
	 */
	private static String encryptAESKey = "V2FuZzkyNuYSKIuwqTQkFQSUpXVA";
	// private static final byte[] KEY = { 1, 1, 33, 82, -32, -85, -128, -65 };

	@Value("${encryptAESKey}")
	public void setEncryptAESKey(String encryptAESKey) {
		AesUtil.encryptAESKey = encryptAESKey;
	}

	/**
	 * LOGGER
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(AesUtil.class);

	/**
	 * 加密
	 */
	public static String encode(String str) {
		try {
			Security.addProvider(new com.sun.crypto.provider.SunJCE());
			// 实例化支持AES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
			// KeyGenerator 提供对称密钥生成器的功能,支持各种算法
			KeyGenerator keygen = KeyGenerator.getInstance("AES");
			// 将私钥encryptAESKey先Base64解密后转换为byte[]数组按128位初始化
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			secureRandom.setSeed(Base64Util.decodeThrowsException(encryptAESKey).getBytes());
			keygen.init(128, secureRandom);
			// SecretKey 负责保存对称密钥 生成密钥
			SecretKey deskey = keygen.generateKey();
			// 生成Cipher对象,指定其支持的AES算法,Cipher负责完成加密或解密工作
			Cipher c = Cipher.getInstance("AES");
			// 根据密钥,对Cipher对象进行初始化,ENCRYPT_MODE表示加密模式
			c.init(Cipher.ENCRYPT_MODE, deskey);
			byte[] src = str.getBytes();
			// 该字节数组负责保存加密的结果
			byte[] cipherByte = c.doFinal(src);
			// 先将二进制转换成16进制,再返回Bsae64加密后的String
			return Base64Util.encodeThrowsException(HexConvertUtil.parseByte2HexStr(cipherByte));
		} catch (NoSuchAlgorithmException e) {
			LOGGER.error("getInstance()方法异常:" + e.getMessage());
			throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
		} catch (UnsupportedEncodingException e) {
			LOGGER.error("Bsae64加密异常:" + e.getMessage());
			throw new CustomUnauthorizedException("Bsae64加密异常:" + e.getMessage());
		} catch (NoSuchPaddingException e) {
			LOGGER.error("getInstance()方法异常:" + e.getMessage());
			throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
		} catch (InvalidKeyException e) {
			LOGGER.error("初始化Cipher对象异常:" + e.getMessage());
			throw new CustomUnauthorizedException("初始化Cipher对象异常:" + e.getMessage());
		} catch (IllegalBlockSizeException e) {
			LOGGER.error("加密异常,密钥有误:" + e.getMessage());
			throw new CustomUnauthorizedException("加密异常,密钥有误:" + e.getMessage());
		} catch (BadPaddingException e) {
			LOGGER.error("加密异常,密钥有误:" + e.getMessage());
			throw new CustomUnauthorizedException("加密异常,密钥有误:" + e.getMessage());
		}
	}

	/**
	 * 解密
	 */
	public static String decode(String str) {
		try {
			Security.addProvider(new com.sun.crypto.provider.SunJCE());
			// 实例化支持AES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
			// KeyGenerator 提供对称密钥生成器的功能,支持各种算法
			KeyGenerator keygen = KeyGenerator.getInstance("AES");
			// 将私钥encryptAESKey先Base64解密后转换为byte[]数组按128位初始化
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			secureRandom.setSeed(Base64Util.decodeThrowsException(encryptAESKey).getBytes());
			keygen.init(128, secureRandom);
			// SecretKey 负责保存对称密钥 生成密钥
			SecretKey deskey = keygen.generateKey();
			// 生成Cipher对象,指定其支持的AES算法,Cipher负责完成加密或解密工作
			Cipher c = Cipher.getInstance("AES");
			// 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示解密模式
			c.init(Cipher.DECRYPT_MODE, deskey);
			// 该字节数组负责保存加密的结果,先对str进行Bsae64解密,将16进制转换为二进制
			
			String base64 = Base64Util.decodeThrowsException(str);
			byte[] cipherByte = c.doFinal(HexConvertUtil.parseHexStr2Byte(base64));
			return new String(cipherByte);
		} catch (NoSuchAlgorithmException e) {
			LOGGER.error("getInstance()方法异常:" + e.getMessage());
			throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
		} catch (UnsupportedEncodingException e) {
			LOGGER.error("Bsae64加密异常:" + e.getMessage());
			throw new CustomUnauthorizedException("Bsae64加密异常:" + e.getMessage());
		} catch (NoSuchPaddingException e) {
			LOGGER.error("getInstance()方法异常:" + e.getMessage());
			throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
		} catch (InvalidKeyException e) {
			LOGGER.error("初始化Cipher对象异常:" + e.getMessage());
			throw new CustomUnauthorizedException("初始化Cipher对象异常:" + e.getMessage());
		} catch (IllegalBlockSizeException e) {
			LOGGER.error("解密异常,密钥有误:" + e.getMessage());
			throw new CustomUnauthorizedException("解密异常,密钥有误:" + e.getMessage());
		} catch (BadPaddingException e) {
			LOGGER.error("解密异常,密钥有误:" + e.getMessage());
			throw new CustomUnauthorizedException("解密异常,密钥有误:" + e.getMessage());
		}
	}
}


◇ MD5工具类

package com.yby.saas.util.common;

import java.security.MessageDigest;
import java.util.UUID;

public class Md5Util {

	public static String encode(String str) {
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update(str.getBytes());
			byte b[] = md.digest();

			int i;

			StringBuffer buf = new StringBuffer("");
			for (int offset = 0; offset < b.length; offset++) {
				i = b[offset];
				if (i < 0)
					i += 256;
				if (i < 16)
					buf.append("0");
				buf.append(Integer.toHexString(i));
			}
			str = buf.toString();
		} catch (Exception e) {
			e.printStackTrace();

		}
		return str;
	}

	/**
	 * 带盐值加密
	 * 
	 * @param str
	 *            待加密字符串
	 * @param salt
	 *            盐值
	 */
	public static String encode(String str, String salt) {

		return encode(str + salt);
	}

	public static void main(String[] args) {
		String salt = UUID.randomUUID().toString().replace("-", "");
		System.out.println("salt:" + salt);
		System.out.println(encode("a123456" + "de93210d922540cb8b4686b7aca08d49"));
	}
}


◇ Base64工具类

package com.yby.saas.util.common;

import java.io.UnsupportedEncodingException;
import java.util.Base64;

/**
 * Base64工具
 * 
 * @author lwx
 */
public class Base64Util {

	/**
	 * 加密JDK1.8
	 */
	public static String encode(String str) {
		try {
			byte[] encodeBytes = Base64.getEncoder().encode(str.getBytes("utf-8"));
			return new String(encodeBytes);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 解密JDK1.8
	 */
	public static String decode(String str) {
		try {
			byte[] decodeBytes = Base64.getDecoder().decode(str.getBytes("utf-8"));
			return new String(decodeBytes);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 加密JDK1.8
	 */
	public static String encodeThrowsException(String str) throws UnsupportedEncodingException {
		byte[] encodeBytes = Base64.getEncoder().encode(str.getBytes("utf-8"));
		return new String(encodeBytes);
	}

	/**
	 * 解密JDK1.8
	 */
	public static String decodeThrowsException(String str) throws UnsupportedEncodingException {
		byte[] decodeBytes = Base64.getDecoder().decode(str.getBytes("utf-8"));
		return new String(decodeBytes);
	}

}


五、登录方法

◇ LoginController

/**
 * 登录
 */
@PostMapping("/login")
public JsonVo login(String account, String password, HttpServletResponse response) {

	JsonVo vo = new JsonVo();

	if (StringUtils.isEmpty(account) || StringUtils.isEmpty(password)) {
		vo.setCode(StatusCode.PARAM_ERROR);
		return vo;
	}

	// 查询数据库中的帐号信息
	User affirm = userService.getByMobile(account);
	if (affirm == null) {
		vo.setCode(StatusCode.NOT_FOUND);
		return vo;
	}
	// Md5加密
	if (!Md5Util.encode(password + affirm.getSalt()).equals(affirm.getPassword())) {
		vo.setCode(StatusCode.PASSWORD_ERROR);
		return vo;
	}

	// 清除可能存在的shiro权限信息缓存
	if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + account)) {
		redis.del(RedisConstant.PREFIX_SHIRO_CACHE + account);
	}

	// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
	String currentTimeMillis = String.valueOf(System.currentTimeMillis());
	redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
			Integer.parseInt(refreshTokenExpireTime));

	// 从Header中Authorization返回AccessToken,时间戳为当前时间戳
	String token = JwtUtil.sign(account, currentTimeMillis);
	response.setHeader("Authorization", token);
	response.setHeader("Access-Control-Expose-Headers", "Authorization");

	vo.OK();
	return vo;
}

◇ 获取当前登录用户

/**
 * 获取当前登录用户
 */
public User getCurrent() {
	try {
		Subject subject = SecurityUtils.getSubject();
		if (subject != null) {
			String token = (String) subject.getPrincipal();
			if (StringUtil.isNotBlank(token)) {
				String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
				if (StringUtil.isNotBlank(account)) {
					return userService.getByMobile(account);
				}
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	return null;
}

/**
 * 获取当前登录用户ID
 */
public Long getCurrentId() {
	User current = getCurrent();
	if (current != null) {
		return current.getId();
	}
	return null;
}

六、PostMan请求示例

◇ 获取token

用POST方式访问登录接口:http://localhost:8999/login?account=15813922171&password=123456
登录成功后返回如下信息:
POST调用登录接口1


重点在返回的Headers,红框部分即我们想要的token,前端请求接口时携带此token即可:

POST调用登录接口2


◇ 使用token

在Headers处添加参数如下:
Content-Type:application/json
Authorization:上一步获取到的token
使用token请求接口

好了,基本主要的相关代码都在上面了,如有疑问请留言。

The end.

猜你喜欢

转载自blog.csdn.net/stilll123456/article/details/88370355