SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (一) 简介与配置

项目Github地址: https://github.com/baiye21/ShiroDemo

  1. SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (一) 简介与配置

  2. SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (二) 认证 

  3. SpringBoot 基于Shiro + Jwt + Redis的用户权限管理 (三) 鉴权

一,大体功能

1.登录成功返回access_token

    用户通过用户密码进行login,验证成功后生成一个access_token返回给前端,之后的请求都需要将access_token放在Request Header部Authorization字段中,才能正常访问。

2.后续请求需携带access_token 

    将自定义的JwtFilter拦截器加入到了Shiro的过滤器中,没有在过滤链中的请求URL,进入拦截器,获取请求Header的access_token,进行shiro的token登录认证授权。

3.Redis保存refresh_token 

    生成access_token时,有效载荷中加入当前时间戳与用户id,同时往Redis插入一条key为固定前缀加用户id,value为时间戳的refresh_token的记录,过期时间要比access_token长,当发给用户的access_token过期后,查找refresh_token记录,若refresh_token有效,则重新生成access_token返回给前端,之后请求需要携带新的access_token,从而实现access_token的自动刷新。

4.多个Realm 

    通过继承ModularRealmAuthenticator,实现shiro的多realm认证,正常登录使用PasswordRealm,Token认证使用DemoRealm授权,还可以扩展追加其他方式的认证Realm。

二,Shiro简介

1.ShiroAPI

    Shiro不太了解的话,建议看  Shiro官方API ,至少红色框出来的部分耐心浏览一遍,将会对Shiro的认证与鉴权流程有一个大概的了解,这里把比较重要的组成图,认证和授权流程图截取出来放在后面了。

2.Shiro主要组成

  • Authentication : 认证
  • Authorization : 授权
  • Session Management : session 管理  
  • Cryptography : 密码匹配

3.Authentication 认证流程(doGetAuthenticationInfo)

4.Authorization 授权流程(doGetAuthorizationInfo )

三,项目介绍

一,测试数据库表,

项目只是演示Demo,就不弄用户权限表,角色表之类的了,一张表搞定。表结构如下

CREATE TABLE `m_user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` varchar(20) NOT NULL COMMENT '用户ID',
  `user_name` varchar(20) NOT NULL COMMENT '用户名',
  `pass_word` varchar(255) NOT NULL COMMENT '密码',
  `account_type` char(1) NOT NULL COMMENT '账号类型',
  `permission_type` varchar(3) NOT NULL COMMENT '权限类型',
  `del_flg` char(1) DEFAULT '0' COMMENT '删除flag',
  `reg_account` varchar(20) DEFAULT NULL COMMENT '登录者',
  `reg_time` datetime DEFAULT NULL COMMENT '登录时间',
  `upd_account` varchar(20) DEFAULT NULL COMMENT '更新者',
  `upd_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表'

二,项目结构

项目结构如下图,

    数据库持久层用Mybatis,并使用Mybatis-generator反向生成数据表对应实体类等。

    日志用的og4j + slf4j + lombok @Slf4j

    pom文件就不贴全部了,有点长了,有需要移步github查看。这里只贴Shiro相关的吧

        <!--shiro权限框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>

        <!--JWT-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>
       
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

四,关键配置(说明尽量写在注释里面了)

1.ShiroConfig


package com.demo.config;

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

import javax.servlet.Filter;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
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.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import com.demo.filter.JwtFilter;
import com.demo.shiro.RedisCacheManager;
import com.demo.shiro.ShiroSessionManager;
import com.demo.shiro.realm.JwtRealm;
import com.demo.shiro.realm.PasswordRealm;
import com.demo.shiro.realm.DemoRealm;
import com.demo.shiro.realm.UserModularRealmAuthenticator;

/*
 * Author  : baiye
   Time    : 2021/06/30
   Function: Shiro配置
*/
@Configuration
public class ShiroConfig {

	/**
	 * 密码登录时指定匹配器,
	 * 
	 * @return HashedCredentialsMatcher
	 */
	@Bean("hashedCredentialsMatcher")
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
		// 设置哈希算法名称
		matcher.setHashAlgorithmName("MD5");
		// 设置哈希迭代次数
		matcher.setHashIterations(666);
		// 设置存储凭证十六进制编码
		matcher.setStoredCredentialsHexEncoded(true);
		return matcher;
	}

	/**
	 * 如果需要密码匹配器则需要进行指定 密码登录Realm
	 * @return PasswordRealm
	 */
	@Bean
	public PasswordRealm passwordRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
		PasswordRealm passwordRealm = new PasswordRealm();
		passwordRealm.setCredentialsMatcher(matcher);
		return passwordRealm;
	}

	/**
	 * jwtRealm
	 * @return JwtRealm
	 */
	@Bean
	public JwtRealm jwtRealm() {
		return new JwtRealm();
	}

	/**
	 * demoRealm
	 * @return DemoRealm
	 */
	@Bean
	public DemoRealm demoRealm() {
		return new DemoRealm();
	}

	/**
	 * Shiro内置过滤器,可以实现拦截器相关的拦截器
	 * 常用的过滤器:
	 * anon:无需认证(登录)可以访问
	 * authc:必须认证才可以访问
	 * user:如果使用rememberMe的功能可以直接访问
	 * perms:该资源必须得到资源权限才可以访问
	 * role:该资源必须得到角色权限才可以访问
	 **/
	@Bean
	public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
		
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		// 设置 SecurityManager
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 设置未登录跳转url
		// shiroFilterFactoryBean.setUnauthorizedUrl("/user/unLogin");

		Map<String, String> filterMap = new LinkedHashMap<String, String>();

		// 只有不需要权限认证(anon)的 需要明确写入filterMap
		filterMap.put("/hello", "anon");
		filterMap.put("/register/*", "anon");
		filterMap.put("/login/*", "anon");

		// 添加自定义过滤器并且取名为jwt
		Map<String, Filter> filter = new HashMap<String, Filter>(1);
		filter.put("jwt", new JwtFilter());
		shiroFilterFactoryBean.setFilters(filter);

		// 过滤链定义,从上向下顺序执行,所以放在最为下边
		filterMap.put("/**", "jwt");

		// 未授权界面返回JSON
		// shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
		// shiroFilterFactoryBean.setLoginUrl("/sys/common/403");

		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

		return shiroFilterFactoryBean;
	}

	@Bean
	public UserModularRealmAuthenticator userModularRealmAuthenticator() {
		// 自己重写的ModularRealmAuthenticator
		UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
		modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
		return modularRealmAuthenticator;
	}

	/**
	 * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
	 */
	@Bean
	public SecurityManager securityManager(
			@Qualifier("passwordRealm") PasswordRealm passwordRealm,
			@Qualifier("jwtRealm") JwtRealm jwtRealm,
			@Qualifier("demoRealm") DemoRealm demoRealm,
			@Qualifier("userModularRealmAuthenticator") UserModularRealmAuthenticator userModularRealmAuthenticator) {

		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

		// 设置realm
		securityManager.setAuthenticator(userModularRealmAuthenticator);
		List<Realm> realms = new ArrayList<>();
		// 添加多个realm
		realms.add(passwordRealm);
		realms.add(jwtRealm);
		realms.add(demoRealm);

		/*
		 * 关闭shiro自带的session,详情见文档
		 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
		 */
		DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
		DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();

		// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
		defaultSessionStorageEvaluator.setSessionStorageEnabled(false);

		subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
		securityManager.setSubjectDAO(subjectDAO);

		// 禁止创建shiro session
		// securityManager.setSubjectFactory(subjectFactory());

		//  解决第一次访问没有JSESSIONID导致404错误
		// securityManager.setSessionManager(mySessionManager());

		//  自定义sessionManager
		securityManager.setSessionManager(shiroSessionManager());

		// 注入记住我管理器
		// securityManager.setRememberMeManager(rememberMeManager());

		// ★★★★★★★★★★★★★★ 使用自定义的 redisCacheManage ★★★★★★★★★★★★★★
		// securityManager.setCacheManager(redisCacheManager());

		// securityManager设置自定义认证规则
		securityManager.setRealms(realms);

		return securityManager;
	}

	@Bean
	public ShiroSessionManager shiroSessionManager() {

		ShiroSessionManager shiroSessionManager = new ShiroSessionManager();

		// TODO redis 配置session持久化
		shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());

		return shiroSessionManager;
	}

	@Bean
	public DefaultWebSessionManager mySessionManager() {
		DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();

		// defaultWebSessionManager.setGlobalSessionTimeout(60 * 30 * 1000);
		defaultWebSessionManager.setDeleteInvalidSessions(true);
		defaultWebSessionManager.setSessionValidationSchedulerEnabled(true);
		defaultWebSessionManager.setSessionIdCookieEnabled(true);

		// 将sessionIdUrlRewritingEnabled属性设置成false
		defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
		return defaultWebSessionManager;
	}

	@Bean
	public RedisCacheManager redisCacheManager() {
		return new RedisCacheManager();
	}

	/**
	 * cookie对象; rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
	 * 
	 * @return
	 */
//	@Bean
//	public SimpleCookie rememberMeCookie() {
//
//		// 这个参数是cookie的名称
//		SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//
//		// <!-- 记住我cookie生效时间30天 ,单位秒;-->
//		simpleCookie.setMaxAge(259200);
//
//		return simpleCookie;
//	}

	/**
	 * cookie管理对象;
	 * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
	 * 
	 * @return
	 */
//	@Bean
//	public CookieRememberMeManager rememberMeManager() {
//
//		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//		cookieRememberMeManager.setCookie(rememberMeCookie());
//		// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
//		cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
//		return cookieRememberMeManager;
//	}

	/**
	 * Shiro生命周期处理器
	 */
	@Bean(name = "lifecycleBeanPostProcessor")
	public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}

	/**
	 * 
	 * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),<br>
	 * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
	 *
	 * @return DefaultAdvisorAutoProxyCreator
	 */
	@Bean
	@DependsOn("lifecycleBeanPostProcessor")
	public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
		defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
		return defaultAdvisorAutoProxyCreator;
	}

	/**
	 * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
}

2.ShiroSessionManager

package com.demo.shiro;

import java.io.Serializable;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import com.demo.common.Const;
import com.demo.util.JwtUtil;

import lombok.extern.slf4j.Slf4j;

/*
 * Author  : baiye
   Time    : 2021/06/30
   Function:   shiro自定义session管理器
*/
@Slf4j
public class ShiroSessionManager extends DefaultWebSessionManager {

	public ShiroSessionManager() {
		super();
	}

	private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

	@Override
	protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

		// 如果请求头中有 Authorization
		String token = WebUtils.toHttp(request).getHeader(Const.TOKEN_HEADER_NAME);

		if (!StringUtils.isEmpty(token)) {

			if (JwtUtil.verify(token, Const.TOKEN_SECRET)) {

				String id = JwtUtil.getClaim(token, Const.JSESSIONID);

				log.debug("ShiroSessionManager从http header 取出token中的JSESSIONID:{}", id);

				request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
						REFERENCED_SESSION_ID_SOURCE);
				request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
				request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);

				return id;
			}

			return super.getSessionId(request, response);
		} else {
			// 否则按默认规则从cookie取sessionId
			return super.getSessionId(request, response);
		}
	}

}

3.JwtFilter 


package com.demo.filter;

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

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

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.demo.common.Const;
import com.demo.common.ConstCode;
import com.demo.common.RedisConst;
import com.demo.common.ResponseCode;
import com.demo.common.ServerResponse;
import com.demo.shiro.JwtToken;
import com.demo.util.JsonConverUtil;
import com.demo.util.JwtUtil;
import com.demo.util.RedisUtil;
import com.demo.util.SysTimeUtil;

import lombok.extern.slf4j.Slf4j;

/*
 * Author  : baiye
   Time    : 2021/06/30
   Function: 核心JWT拦截器,拦截Shiro过滤链anon之外的请求
*/
/**
 * 这个类最主要的目的是:当请求需要校验权限,token是否具有权限时,构造出主体subject执行login()
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

	@Autowired
	RedisUtil redisUtil;

	@Autowired
	JsonConverUtil jsonConverUtil;

	/**
	 * 执行登录认证
	 * 
	 * @param request
	 * @param response
	 * @param mappedValue
	 * @return 是否成功
	 */
	@Override
	// 这个方法判断 尝试进行登录的操作,如果token存在,那么进行提交登录,如果不存在说明可能是正在进行登录或者做其它的事情 直接放过即可
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
		try {

			executeLogin(request, response);

			return true;
		} catch (Exception e) {
			// return false;
			// throw new AuthenticationException("Token失效请重新登录");
			// 认证出现异常,传递错误信息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已过期(TokenExpiredException),
				// 判断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;
		}
	}

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
		this.sendChallenge(request, response);
		return false;
	}

	/**
	 * 执行登录
	 */
	@Override
	protected boolean executeLogin(ServletRequest request, ServletResponse response)
			throws  AuthenticationException {

		// log.info("进入JwtFilter类中...");

		HttpServletRequest httpServletRequest = (HttpServletRequest) request;

		// head部存放的 authorization token
		String token = httpServletRequest.getHeader(Const.TOKEN_HEADER_NAME);

		// 判断token是否存在
		/*
		 * if (token == null) { return false; }
		 */
		if (!StringUtils.isNotBlank(token)) {

			log.error("获取到的token为空");

			throw new AuthenticationException();
		}

		// log.info("获取到的token是:{}", token);
		// JwtToken token = new JwtToken(this.getAuthzHeader(request))
		JwtToken jwtToken = new JwtToken(token);

		try {
			log.debug("提交UserModularRealmAuthenticator查找对应的realm...");

			getSubject(request, response).login(jwtToken);

		} catch (AuthenticationException e) {
			log.debug("捕获到身份认证异常{}", e.getMessage());
			throw e;
		}
		return true;
	}

	/**
	 * 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
	 */
	private boolean refreshToken(ServletRequest request, ServletResponse response) {
		// Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
		String token = this.getAuthzHeader(request);

		// Token的帐号信息获取
		String userid = JwtUtil.getClaim(token, Const.TOKEN_CLAIM_USERID);

		// Redis中RefreshToken的key
		String refreshTokenKey = RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userid;

		// Redis中RefreshToken是否存在
		if (redisUtil.hasKey(refreshTokenKey)) {

			// Redis中RefreshToken还存在,获取RefreshToken的时间戳
			String currentTimeMillisRedis = redisUtil.get(refreshTokenKey).toString();
			// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
			if (JwtUtil.getClaim(token, Const.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				// 获取当前最新时间戳
				String currentTimeMillis = String.valueOf(SysTimeUtil.getTime());

				/**
				 * ★★★
				 * 这里重新设置RefreshToken为一天,可能会导致RefreshToken一直续期,
				 * 可以一开始设置RefreshToken大一些
				 * 比如为7天,续期时不在重置,这样一次登录的Token Refresh有效期就是一周,一周后必须重新登录。
				 * ★★★
				 * */
				// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间
				redisUtil.set(refreshTokenKey, currentTimeMillis, Const.REFRESH_TOKEN_EXPIRE_TIME);

				// 账号类型
				String accountType = JwtUtil.getClaim(token, Const.ACCOUNT_TPYE);

				String sessionId = JwtUtil.getClaim(token,Const.JSESSIONID);
				// 刷新AccessToken,设置时间戳为当前最新时间戳
				token = JwtUtil.loginSign(userid, accountType, currentTimeMillis, sessionId,
						Const.TOKEN_SECRET);

				// 将新刷新的AccessToken再次进行Shiro的登录
				JwtToken jwtToken = new JwtToken(token);

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

				// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
				HttpServletResponse httpServletResponse = (HttpServletResponse) response;

				httpServletResponse.setHeader(Const.TOKEN_HEADER_NAME, token);

				httpServletResponse.setHeader(Const.TOKEN_ACCESS_CONTROL, Const.TOKEN_HEADER_NAME);

				return true;
			}
		}
		return false;
	}

	/**
	 * 无需转发,直接返回Response信息
	 */
	private void response401(ServletRequest req, ServletResponse response, String msg) {
		HttpServletResponse httpServletResponse = (HttpServletResponse) response;
		httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
		httpServletResponse.setCharacterEncoding("UTF-8");
		httpServletResponse.setContentType("application/json; charset=utf-8");
		PrintWriter out = null;
		try {
			out = httpServletResponse.getWriter();

			// object 转 json字符串
			String data = JSON.toJSONString(ServerResponse.createByErrorCodeMessage(ResponseCode.UNAUTHORIZED.getCode(),
					"UNAUTHORIZED"));

			out.append(data);

		} catch (IOException e) {

			log.error("response401 : {}",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"));

		if (StringUtils.isNotEmpty(httpServletRequest.getHeader("Origin"))) {
			httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
		} else {
			httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
		}

		httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE,HEAD");
		// httpServletResponse.setHeader("Access-Control-Allow-Headers",
		// httpServletRequest.getHeader("Access-Control-Request-Headers"));
		httpServletResponse.setHeader("Access-Control-Allow-Headers",
				"Content-Type,Origin,Accept,Authorization,X-Requested-With,No-Cache,If-Modified-Since,Pragma,Last-Modified,Cache-Control,Expires,X-E4M-With");

		// 跨域时会首先发送一个option请求,给option请求直接返回正常状态
		if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
			httpServletResponse.setStatus(HttpStatus.OK.value());
			return false;
		}
		return super.preHandle(request, response);
	}
}

4.UserModularRealmAuthenticator

package com.demo.shiro.realm;

import java.util.ArrayList;
import java.util.Collection;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import com.demo.common.ConstCode;
import com.demo.shiro.CustomizedToken;
import com.demo.shiro.JwtToken;

import lombok.extern.slf4j.Slf4j;

/*
 * Author  : baiye 
   Time    : 2021/06/30
   Function: 
*/
@Slf4j
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {

	// 当subject.login()方法执行,下面的代码即将执行
	@Override
	protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		log.debug("UserModularRealmAuthenticator:method doAuthenticate() 执行 ");
		// 判断getRealms()是否返回为空
		assertRealmsConfigured();

		// 所有Realm
		Collection<Realm> realms = getRealms();

		// 盛放登录类型对应的所有Realm集合
		Collection<Realm> typeRealms = new ArrayList<>();

		if(authenticationToken instanceof  JwtToken) {
			log.debug("验证的Token类型是:{}", "JwtToken");
			typeRealms.clear();
			// 获取header部的token进行强制类型转换
			JwtToken jwtToken = (JwtToken) authenticationToken;

			for (Realm realm : realms) {

				if (realm.getName().contains("Demo")) {
					typeRealms.add(realm);
				}
			}

			return doSingleRealmAuthentication(typeRealms.iterator().next(), jwtToken);
		} else {
			typeRealms.clear();
			// 这个类型转换的警告不需要再关注 如果token错误 后面将会抛出异常信息
			CustomizedToken customizedToken = (CustomizedToken) authenticationToken;
			log.debug("验证的Token类型是:{}", "CustomizedToken");
			// 登录类型
			String loginType = customizedToken.getLoginType();

			log.debug("验证的realm类型是:{}", loginType);

			for (Realm realm : realms) {
				if (realm.getName().contains(loginType)) {
					log.debug("当前realm:{}被注入:", realm.getName());
					typeRealms.add(realm);
				}
			}
			// 判断是单Realm还是多Realm
			if (typeRealms.size() == ConstCode.NUM_1) {
				log.debug("一个realm");
				return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken);
			} else {
				log.debug("多个realm");
				return doMultiRealmAuthentication(typeRealms, customizedToken);
			}
		}

	}
}

5.PasswordRealm(只做认证 doGetAuthenticationInfo,参数AuthenticationToken为自定义的CustomizedToken,认证成功的principal为用户表对应的UserMaster对象实例)

package com.demo.shiro.realm;

import com.demo.entity.UserMaster;
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.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import com.demo.service.IUserService;
import com.demo.shiro.CustomizedToken;
import lombok.extern.slf4j.Slf4j;

/*
 * Author  : baiye
   Time    : 2021/06/30
   Function:  用户密码认证Realm
*/
@Slf4j
public class PasswordRealm extends AuthorizingRealm {

	@Autowired
	@Lazy
	private IUserService iUserService;

	/**
	 * shiro 赋权
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

		log.debug("PasswordRealm 不做赋权处理");

		// password login 不做赋权处理
		return null;
	}

	/**
	 * shiro 认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {

		log.debug("PasswordRealm权限认证开始,传递的token:{}", authenticationToken);

		CustomizedToken token = (CustomizedToken) authenticationToken;

		log.debug("PasswordRealm转换的自定义token:{}", token);

		// 找出数据库中的对象 和用户输入的对象做出对比
		// 根据userid查询用户
		UserMaster userMaster = iUserService.getUserByUserId(token.getUsername());

		if (userMaster == null) {
			log.debug("该账号不存在:{}", token.getUsername());
			// 抛出账号不存在异常
			throw new UnknownAccountException();
		}

		// 用户密码
		Object hashedCredentials = userMaster.getPassWord();

		/***
		 * param1 : 数据库用户<br>
		 * param2 : 密码<br>
		 * param3 : 加密所用盐值 <br>
		 * param4 : 当前realm的名称<br>
		 */
		return new SimpleAuthenticationInfo(userMaster, hashedCredentials,
				ByteSource.Util.bytes(userMaster.getUserId()), getName());

	}
}

6.DemoRealm(使用Jwt做认证doGetAuthenticationInfo,认证成功返回的principal是token,所以鉴权doGetAuthorizationInfo时使用token解析出userid去数据库获取用户的角色和权限信息。 )

package com.demo.shiro.realm;

import com.demo.entity.UserMaster;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

import com.demo.common.Const;
import com.demo.common.RedisConst;
import com.demo.pojo.common.UserAccessInfo;
import com.demo.service.IUserService;
import com.demo.shiro.JwtToken;
import com.demo.util.JwtUtil;
import com.demo.util.RedisUtil;

import lombok.extern.slf4j.Slf4j;

/*
 * Author  : baiye
   Time    : 2021/06/30
   Function: 
*/
/**
 * 用户登录鉴权和获取用户授权
 */
@Slf4j
public class DemoRealm extends AuthorizingRealm {

	@Autowired
	@Lazy
	private IUserService iUserService;

	@Autowired
	@Lazy
	private RedisUtil redisUtil;

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

	/**
	 * 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
	 *
	 * @param principals token
	 * @return AuthorizationInfo 权限信息
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

		log.info("demoRealm doGetAuthorizationInfo 用户赋权 ");

		String userid = null;

		if (principals != null) {

			// 此处的principals为 UserMaster
			Object PrimaryPrincipal = principals.getPrimaryPrincipal();

			if (PrimaryPrincipal instanceof UserMaster) {

				UserMaster userMaster = (UserMaster) PrimaryPrincipal;

				userid = userMaster.getUserId();

			} else {

				// 此处的principals为token
				userid = JwtUtil.getClaim(principals.toString(), Const.TOKEN_CLAIM_USERID);
			}
		}

		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

		UserAccessInfo userAccessInfo = iUserService.getUserAccessInfo(userid);

		/**
		 * 设置用户拥有的角色集合,<br>
		 * 
		 * accountType = 1 管理员 admin <br>
		 * accountType = 2 领导  Leader <br>
		 * accountType = 3 普通用户 user <br>
		 * 
		 */
		info.setRoles(userAccessInfo.getRoleSet());

		// 设置用户拥有的权限集合
		info.addStringPermissions(userAccessInfo.getPermissionSet());

		return info;
	}

	/**
	 * 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
	 *
	 * @param auth 用户身份信息 token
	 * @return 返回封装了用户信息的 AuthenticationInfo 实例
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {

		String token = (String) auth.getCredentials();

		if (StringUtils.isBlank(token)) {

			log.error("token为空,身份认证失败!");

			throw new AuthenticationException("token为空!");
		}

		// 获取token携带的用户名
		String userid = JwtUtil.getClaim(token, Const.TOKEN_CLAIM_USERID);

		// 帐号为空
		if (StringUtils.isBlank(userid)) {

			throw new AuthenticationException("token中用户为空(The userid in Token is empty.)");
		}

		// UserMaster表查询用户
		UserMaster userMaster = iUserService.getUserByUserId(userid);

		if (userMaster == null) {

			throw new AuthenticationException("该帐号不存在(The account does not exist.)");
		}

		String refreshTokenKey = RedisConst.PREFIX_SHIRO_REFRESH_TOKEN + userid;

		// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
		if (JwtUtil.verify(token, Const.TOKEN_SECRET) && redisUtil.hasKey(refreshTokenKey)) {

			// 获取RefreshToken的时间戳
			String currentTimeMillisRedis = redisUtil.get(refreshTokenKey).toString();

			// 获取AccessToken时间戳,与RefreshToken的时间戳对比
			if (JwtUtil.getClaim(token, Const.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {

				// principal,集成redis时,这里必须为对象,确保key唯一,且pojo实现序列化
				// credentials 这里使用token
				return new SimpleAuthenticationInfo(token, token, "demoRealm");

			}
		}
		throw new AuthenticationException("token expired or incorrect.");

		// 校验token有效性
		// UserMaster loginUser = this.checkUserTokenIsEffect(token);

		// principal,集成redis时,这里必须为对象,确保key唯一,且pojo实现序列化
		// credentials 这里使用token
		// return new SimpleAuthenticationInfo(token, token, getName());
	}

}