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

一,前言

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

  1. shiro默认的拦截跳转都是跳转url页面,这在前后端分离的项目中显然行不通
  • shiro的重定向问题

1.未登录,shiro会自动重定向到 /login
2.访问路径无权限,shiro会抛出401 http错误

  • 解决:只想用springBoot写后端api所以就必须解决这些问题
    通过查资料发现在 org.apache.shiro.web.filter.authz.AuthorizationFilter下有onAccessDenied方法,源码:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        Subject subject = this.getSubject(request, response);
        if (subject.getPrincipal() == null) {
            this.saveRequestAndRedirectToLogin(request, response); // 重定向逻辑
        } else {
            String unauthorizedUrl = this.getUnauthorizedUrl();
            if (StringUtils.hasText(unauthorizedUrl)) {
                WebUtils.issueRedirect(request, response, unauthorizedUrl); 
            } else {
                WebUtils.toHttp(response).sendError(401);
            }
        }

        return false;
    }

重写此方法即可解决重定向的问题

  1. shiro默认使用session做登录校验,分离后当然这也是不推荐的

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

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

二,相关说明

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

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

2,关于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之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户

3,关于根据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进行访问

三,主要配置

1,maven依赖


<dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-starter-webartifactId>
	dependency>


<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starterartifactId>
dependency>


<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-testartifactId>
	<scope>testscope>
dependency>


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>


<dependency>
	<groupId>mysqlgroupId>
	<artifactId>mysql-connector-javaartifactId>
dependency>


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druid-spring-boot-starterartifactId>
    <version>1.1.14version>
dependency>


<dependency>
	<groupId>org.mybatis.spring.bootgroupId>
	<artifactId>mybatis-spring-boot-starterartifactId>
	<version>2.0.0version>
dependency>


<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-springartifactId>
    <version>1.4.0version>
dependency>


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.56version>
dependency>


<dependency>
    <groupId>com.auth0groupId>
    <artifactId>java-jwtartifactId>
    <version>3.7.0version>
dependency>

2,application.properties配置

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

## 数据库配置 - 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

四,代码实现

1,统计返回对象

package com.liang.po.vo;

import com.liang.po.constant.ResultCode;

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

public class R {
    //链式编程
    private Boolean success;
    private Integer code;

    public Boolean getSuccess() {
        return success;
    }

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

    public Integer getCode() {
        return code;
    }

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

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Map<String, Object> getData() {
        return data;
    }

    public void setData(Map<String, Object> data) {
        this.data = data;
    }

    private String message;

    private Map<String,Object> data = new HashMap<>();

    private R(){}

    //操作成功,调用这个方法,返回成功的数据
    public static R ok(){
        R r = new R();
        r.setSuccess(true);
        r.setCode(ResultCode.SUCCESS);
        r.setMessage("操作成功");
        return r;
    }

    //操作失败,调用这个方法,返回失败的数据
    public static R error(){
        R r = new R();
        r.setSuccess(false);
        r.setCode(ResultCode.ERROR);
        r.setMessage("操作失败");
        return r;
    }

    //操作失败,调用这个方法,返回失败的数据
    public static R error(String message){
        R r = new R();
        r.setSuccess(false);
        r.setCode(ResultCode.ERROR);
        r.setMessage(message);
        return r;
    }

    //使用链式编程
    public R success(Boolean success){
        this.setSuccess(success);
        return this;
    }

    public R message(String message){
        this.setMessage(message);
        return this;
    }

    public R code(Integer code){
        this.setCode(code);
        return this;
    }

    public R data(String key, Object value){
        this.data.put(key, value);
        return this;
    }

    public R data(Map<String, Object> map){
        this.setData(map);
        return this;
    }
}
package com.liang.po.constant;

//定义返回数据使用的状态码
public interface ResultCode {

    int SUCCESS = 20000; //成功状态码
    int ERROR = 20001;//失败状态码
    int AUTH = 30000;//没有操作权限状态码
}

2,ShiroConfig

2.1 filter 问题

filter 中将无法正常注入bean 问题:

问题原因: web容器加载顺序导致, 加载顺序是listener——filter——servlet,当项目启动时,filter先于servlet初始化, 而Spring中默认bean的初始化是在Servlet后进行的,所以会注入失败。

解决方法: 将filter添加到spring容器

  1. 使用FilterRegistrationBean或者DelegatingFilterProxyRegistrationBean注册filter, 为什么可以这么实现呢?因为FilterRegistrationBeanDelegatingFilterProxyRegistrationBean都继承了AbstractFilterRegistrationBean抽象类,而该抽象类实现了ServletContextInitializer接口, Spring容器初始化时会遍历查找实现ServletContextInitializer接口的bean,并调用onStartup方法注册自定义的filter
  • 使用FilterRegistrationBean方式
public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        //registrationBean.setFilter(new JwtFilter());
        //registrationBean.addInitParameter("targetFilterLifecycle","true");
        //registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico");
        //registrationBean.addUrlPatterns("/*");
        //registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
        return registrationBean;
    }
  1. 将自定义Filter声明为bean,Spring初始化时自动注册
@Bean("jwtFilter")
public JwtFilter jwtFilterBean() {
	return new JwtFilter();
}
  1. 自己写一个 工具类 ,在 filter 中 通过工具类获取 springboot 容器中的 bean
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    //根据 bean 的名字 获取指定的bean对象
    //bean 有个 默认策略,类名首字母小写
    public static Object getBean(String BeanName){
        Object bean = context.getBean(BeanName);
        return bean;
    }
}

2.2 AspectJ与Shiro不兼容和Spring二次代理错误分析(重复代理)

如果你学过 spring 你应该明白 下面的 xml 是什么意思

Shiro的配置

我们可以得知Shiro在使用注解的时的配置是

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
 <property name="securityManager" ref="securityManager"/>
bean>

其基于注解的权限控制功能

  1. 根据类名可以推断是通过切面的Advisor来完成的(AuthorizationAttributeSourceAdvisor)
  2. 所以说它需要创建自己的动态代理类,是由Spring的DefaultAdvisorAutoProxyCreator动态代理创建的
SpringMVC中AspectJ的配置

根据上文,启用基于注解的AspectJ就很简单了,因为基于注解的Shiro一般也在SpringMVC的Context里,我们采用如下配置

<aop:aspectj-autoproxy proxy-target-class="true"/>

在两者分别配置的时候,配置方法都是对的但是一旦公用,会发现AspectJ会失效

失效的解决办法

  • 1,解决办法也很简单,注释掉Shiro配置中的第一句

注释之后Shiro的注解权限管理功能并不会失效,具体原因我们来细细分析

  • 2,给DefaultAdvisorAutoProxyCreator加入参数proxyTargetClasstrue
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
 <property name="proxyTargetClass" value="true"/>
bean>

配置失效的原因
原因在于二次代理:

  1. 由于使用了aop:aspectj-autoproxy 强制了 proxy-target-class
  2. 也就是说对Web层的Class(主要是Controller)使用了CGLib代理
  3. 然后在Shiro进行代理时使用DefaultAdvisorAutoProxyCreator
  4. 原本应该判断Controller,发现没有任何接口,所以使用CGLib来代理
  5. 但是由于Controller已经被CGLib代理过一次了
  6. DefaultAdvisorAutoProxyCreator拿到对不是Contoller本身,而是CGLib的代理结果
  7. CGLib的代理结果本身是有接口的,干扰了DefaultAdvisorAutoProxyCreator的内部判断
  8. 使用JDK去代理CGLib的代理结果
  9. 结果Controller的函数时去了CGLib的接口中找方法名,发现方法不存在,导致代理失败

解决方法生效的原因

  • 1, 方法一
    因为Shiro也是基于Spring的AOP类的,如果找不到合适的配置,就是默认采用同一个Context下的AOP代理配置,我们给了其proxy-target-class为true,自然就在第二次代理的时候找得到方法

  • 2,方法二
    方法二就更直接了,告诉 DefaultAdvisorAutoProxyCreator为True 就好

补充:
DefaultAdvisorAutoProxyCreator是用来扫描上下文,寻找所有的Advistor(通知器),将这些Advisor应用到所有符合切入点的Bean中。所以必须在lifecycleBeanPostProcessor创建之后创建,所以加

@DependsOn({"lifecycleBeanPostProcessor"})

保证创建DefaultAdvisorAutoProxyCreator 之前先创建LifecycleBeanPostProcessor

2.3 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;

@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;
	}

	/**
	 * 
	 * 注入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
	 * 
*/
@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(); } // AuthorizationAttributeSourceAdvisor匹配所有类,匹配所有加了认证注解的方法 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }

3,JwtFilter

3.1 Shiro过滤器导致的前端跨域问题

用springboot写前后端分离项目的时候, 用postman测试接口没问题,但丢给前端测试的时候返回了跨域问题:

springboot+shiro+jwt+redis+cache实现无状态token登录_第1张图片 这就很奇怪,所有的controller上我都加了@CrossOrigin注解,为什么还会有跨域问题呢?

这里有个需要注意的地方: xx has been blocked by CORS policy
这个CORS是个什么东西呢?简单说是一种资源共享机制。当浏览器发起ajax请求的时候,会先发起一个method为OPTIONS的请求, 这个请求我们可以简单理解为一个探路请求, 该请求不携带信息, 只是为了测试一下目标服务器是否支持跨域,如果支持跨域的话,再发出后续的请求。

springboot+shiro+jwt+redis+cache实现无状态token登录_第2张图片
通过chrome控制台可以看出, 当我们尝试调用listByAdmin接口的时候,发出了一个OPTIONS请求,该请求返回200后,浏览器又发起了一个一模一样的请求,后边这个请求才是真正的请求,当然这是正常的情况。
问题就出在这个OPTIONS请求上, 因为我的项目是完全前后端分离的,前端请求的时候会在http header上携带token,后端利用token鉴权。然而OPTIONS请求(‘OPTIONS请求’这个叫法可能不准确,各位理解我说的是啥就行)默认是不携带token信息的,后端没有收到token,导致鉴权失败。

但是鉴权失败为什么报鉴权问题,反而报了个跨域问题呢?
先看一下一开始我是处理鉴权失败的

public class JwtFilter extends BasicHttpAuthenticationFilter {

	/**
	 * 对跨域提供支持。自定义过滤器Filter进行JWT登陆令牌验证并设置响应头实现跨域
	 */
	@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);
	}
}

判断 是否是 option 请求,如果是,给OPTIONS请求直接返回正常状态。

3.2 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;


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();
			}
		}
	}

	/**
	 * 对跨域提供支持。自定义过滤器Filter进行JWT登陆令牌验证并设置响应头实现跨域
	 */
	@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);
	}
}

JwtToken对象:

package com.yby.saas.config.jwt;

import org.apache.shiro.authc.AuthenticationToken;

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;
	}
}

4,自定义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;


@Service
public class UserRealm extends AuthorizingRealm {

	@Autowired
	private RedisClient redis;

	@Autowired
	private UserCustomMapper userMapper;

	@Autowired
	private RoleCustomMapper roleMapper;

	@Autowired
	private PermissionCustomMapper permissionMapper;

	/**
	 * 大坑,必须重写此方法,不然Shiro会报错。
	 * 每一个Ream都有一个supports方法,用于检测是否支持此Toke。
	 */
	@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.)");
	}
}

5, 重写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;


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;
	}
}

6,重写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;

public class CustomCacheManager implements CacheManager {
	@Override
	public <K, V> Cache<K, V> getCache(String s) throws CacheException {
		return new CustomCache<K, V>();
	}
}

7,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;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

	// 秘钥生成器类KeyGenerator
	@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;

	}
}

8,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;

/**
 * 
 * RedisTemplate工具类:
 * 	针对所有的hash都是以h开头的方法
 * 	针对所有的Set都是以s开头的方法(不含通用方法)
 * 	针对所有的List都是以l开头的方法
 * 
* */
@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; } } }

9,自定义异常

package com.yby.saas.exception;


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();
	}
}

11,自定义异常控制处理器

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;

@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;
	}
}

11,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;

@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());
		}
	}
}

12,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;


@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());
		}
	}
}

13, 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"));
	}
}

14,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);
	}

}

五,登录方法

1,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;
}

2,获取当前登录用户

/**
 * 获取当前登录用户
 */
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 请求示例

1,获取token

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

springboot+shiro+jwt+redis+cache实现无状态token登录_第3张图片

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

springboot+shiro+jwt+redis+cache实现无状态token登录_第4张图片

2,使用 token

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

springboot+shiro+jwt+redis+cache实现无状态token登录_第5张图片

七,最后

上面的只有重要的代码,还有一些常量类没有贴出来,这些代码也是我看大佬的,其实代码并不重要。重要的是学习这种思路,以及遇到的问题,如何发现问题,分析问题,解决问题。我也是查了大量的博客,才看明白大佬的代码,也才解决,明白这过程中出现的问题。当初学习 shiro 的时候,并没有想到还有这么多的问题,看了这个代码,以及查阅大量博客后,学习到了很多。

你可能感兴趣的:(shiro,java,shiro)