springboot整合shiro+jwt+redis实现登录管理,接口权限控制。根据角色/按钮去控制接口权限

1 篇文章 0 订阅
1 篇文章 0 订阅

本文的github地址为:https://github.com/guanghuichen/shiro-jwt-redis

疫情期间在家闲的无聊,学习一下shiro和JWT的使用,平时工作中虽然一直用到但是始终没有从零到一搭建过,本文基于该github地址https://github.com/dolyw/ShiroJwt的学习文章,本文增加了swagger的本地调试。通过swagger的配置后可以灵活的进行调试代码的正确性。如有不正确的地方烦请指出。

本文有以下几个步骤

 

  1. RBCA基础表的建立,entity。mapper的编写

  2. redis的配置以及工具类的编写

  3. 编写JWTtoken

  4. 编写JWTutil生成签名以及解密信息

  5. 编写JWTFilter

  6. UserRealm的编写

  7. shiroConfig的编写


一:数据库表的建立,此处只提供最简单的表结构。后期根据个人需求拓展即可


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `action` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
  `update_time` timestamp(0) DEFAULT NULL,
  `create_time` timestamp(0) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
  `update_time` timestamp(0) DEFAULT NULL,
  `create_time` timestamp(0) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission`  (
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  `create_time` timestamp(0) DEFAULT NULL,
  INDEX `role_permission_index`(`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1 COMMENT '0:失效 1:生效',
  `update_time` timestamp(0) DEFAULT NULL,
  `create_time` timestamp(0) DEFAULT NULL,
  `account` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '账号信息',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `user_name_index`(`name`) USING BTREE,
  INDEX `create_time_index`(`create_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  `create_time` timestamp(0) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `user_role_index`(`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

二:redis的配置以及工具类的编写

此处只放配置类。工具类请直接看github地址即可

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
	/**
	 * 自定义key(消息队列 暂时用不到 自行忽略)
     * 此方法将会根据类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
	 * @Author  科帮网
	 * @return 
	 * @Date	2017年8月13日
	 * 更新日志
	 * 2017年8月13日  科帮网 首次创建
	 *
	 */
	@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());
				for (Object obj : params) {
					sb.append(obj.toString());
				}
				return sb.toString();
			}
		};
	}
    /**
     * 缓存管理器
     * @Author  科帮网
     * @param redisTemplate
     * @return  CacheManager
     * @Date	2017年8月13日
     * 更新日志
     * 2017年8月13日  科帮网  首次创建
     */
//	@SuppressWarnings("rawtypes")
//	@Bean
//	public CacheManager cacheManager(RedisTemplate redisTemplate) {
//		return new RedisCacheManager(redisTemplate);
//	}
    /**
     * 序列化Java对象
     * @Author  科帮网
     * @param factory
     * @return  RedisTemplate<Object, Object>
     * @Date	2017年8月13日
     * 更新日志
     * 2017年8月13日 科帮网  首次创建
     *
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
	@Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        template.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

三:编写JWTtoken

JWT核心部分。后期会用到

public class JWTToken implements AuthenticationToken {

	private String token;

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

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

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

}

四:编写JWTutil生成签名以及解密信息

在这里我们先要解决@Value()注入static参数的问题

  private static String encryptJWTKey;

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

然后重要的部分是sign与verify这两个方法,这个方法使用@Component提前注入或者不注入都可以。网上有好几个版本,我选择注入的版本

@Component
public class JwtUtil {

	private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

	/**
	 * 过期时间
	 */
	private static String accessTokenExpireTime;
	/**
	 * JWT认证加密私钥(Base64加密)
	 */
	private static String encryptJWTKey;

	/**
	 * 解决@Value不能修饰static的问题
	 */
	@Value("${accessTokenExpireTime}")
	public void setAccessTokenExpireTime(String accessTokenExpireTime) {
		JwtUtil.accessTokenExpireTime = accessTokenExpireTime;
	}

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

	/**
	 * 
	 * @Title: verify @Description: TODO(检验token是否有效) @param: @param
	 *         token @param: @return @return: boolean @throws
	 */
	public static boolean verify(String token) {
		try {
			// 通过token获取密码
			String secret = getClaim(token, CommonConstant.ACCOUNT) + Base64ConvertUtil.encode(encryptJWTKey);
			// 进行二次加密
			Algorithm algorithm = Algorithm.HMAC256(secret);
			// 使用JWTVerifier进行验证解密
			JWTVerifier verifier = JWT.require(algorithm).build();
			verifier.verify(token);
			return true;
		} catch (UnsupportedEncodingException e) {
			logger.error("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
			return false;
		}
	}

	/**
	 * 
	 * @Title: sign @Description: TODO(生成签名) @param: @param account @param: @param
	 *         password @param: @param currentTimeMillis @param: @return @return:
	 *         String @throws
	 */
	public static String sign(String account, String currentTimeMillis) {
		try {
			// 使用私钥进行加密
			String secret = account + Base64ConvertUtil.encode(encryptJWTKey);
			// 设置过期时间:根据当前时间计算出过期时间。 此处过期时间是以毫秒为单位,所以乘以1000。
			Date date = new Date(System.currentTimeMillis() + Long.parseLong(accessTokenExpireTime) * 1000);
			// 对私钥进行再次加密
			Algorithm algorithm = Algorithm.HMAC256(secret);
			// 生成token 附带account信息
			String token = JWT.create().withClaim("account", account).withClaim("currentTimeMillis", currentTimeMillis)
					.withExpiresAt(date).sign(algorithm);
			return token;
		} catch (UnsupportedEncodingException e) {
			logger.error("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
			throw new ApiException("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
		}

	}

	/**
	 * 
	 * @Title: getClaim @Description:
	 *         TODO(获取token中的信息就是withClaim中设置的值) @param: @param token @param: @param
	 *         claim:sign()方法中withClaim设置的值 @param: @return @return: String @throws
	 */
	public static String getClaim(String token, String claim) {
		try {
			// 对token进行解码获得解码后的jwt
			DecodedJWT jwt = JWT.decode(token);
			// 获取到指定的claim,如果是其他类型返回null
			return jwt.getClaim(claim).asString();
		} catch (JWTDecodeException e) {
			logger.error("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
			throw new ApiException("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
		}
	}
}

五:编写JWTFilter

继承了BasicHttpAuthenticationFilter 。该类主要对自己定义的url规则进行过滤。本类方法的执行顺序如下:  preHandle->isAccessAllowed->isLoginAttempt->executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {
	/**
	 * logger
	 */
	private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);

	/**
	 * 
	 * <p>
	 * Title: preHandle
	 * </p>
	 * <p>
	 * Description:
	 * </p>
	 * 对跨域提供支持 Implementation that handles path-matching behavior before a request
	 * is evaluated. If the path matches and the filter
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception
	 * @see org.apache.shiro.web.filter.PathMatchingFilter#preHandle(javax.servlet.ServletRequest,
	 *      javax.servlet.ServletResponse)
	 */
	@Override
	protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
		HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
		HttpServletResponse httpServletResponse = WebUtils.toHttp(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);
	}

	/**
	 * <p>
	 * Title: isAccessAllowed
	 * </p>
	 * <p>
	 * Description:
	 * </p>
	 * 这里是对http请求进行处理 * 这里我们详细说明下为什么最终返回的都是true,即允许访问 例如我们提供一个地址 GET /article
	 * 登入用户和游客看到的内容是不同的 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
	 * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
	 * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
	 * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
	 * 
	 * @param request
	 * @param response
	 * @param mappedValue
	 * @return
	 * @see org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#isAccessAllowed(javax.servlet.ServletRequest,
	 *      javax.servlet.ServletResponse, java.lang.Object)
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
		// 先对当前请求的URI进行判断是否放行
		HttpServletRequest req = (HttpServletRequest) request;
		String requestURI = req.getRequestURI();
		if (ReleaseAddressUtil.confirm(requestURI)) {// 对于不用进行验证的接口直接放行
			return true;
		}
		if (isLoginAttempt(request, response)) {// 有token
			// 进行Shiro的登录UserRealm
			try {
				this.executeLogin(request, response);
			} catch (Exception e) {// 这里对登录异常进行处理
				// 认证出现异常,传递错误信息msg
				String msg = e.getMessage();
				// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
				Throwable throwable = e.getCause();
				if (throwable instanceof SignatureVerificationException) {
					// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
					msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
				} else if (throwable instanceof TokenExpiredException) {
					// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
					// 刷新token
					if (refreshToken(request, response)) {
						return true;
					} else {
						msg = "Token已过期(" + throwable.getMessage() + ")";
					}
					return true;
				} else {
					// 应用异常不为空
					if (throwable != null) {
						// 获取应用异常msg
						msg = throwable.getMessage();
					}
				}
				this.response401(request, response, msg);
				return false;
			}
		} else {// 没有token
				// 没有携带Token
			HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
			// 获取当前请求类型
			String httpMethod = httpServletRequest.getMethod();
			// 获取当前请求URI
			logger.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
			// mustLoginFlag = true 开启任何请求必须登录才可访问
			if (!Config.mustLoginFlag) {// 是否开放游客权限,此处见公共配置类
				this.response401(httpServletRequest, response, "请先登录");
				return false;
			}
		}
		return true;
	}

	/**
	 * <p>
	 * Title: isLoginAttempt
	 * </p>
	 * <p>
	 * Description:
	 * </p>
	 * 检测header里是否含有Authorization字段。
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @see org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#isLoginAttempt(javax.servlet.ServletRequest,
	 *      javax.servlet.ServletResponse)
	 */
	@Override
	protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
		String token = this.getAuthzHeader(request);// 返回header中Authorization对应的token的值
		return token != null;
	}

	/**
	 * <p>
	 * Title: executeLogin
	 * </p>
	 * <p>
	 * Description:
	 * </p>
	 * 进行AccessToken登录认证授权
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception
	 * @see org.apache.shiro.web.filter.authc.AuthenticatingFilter#executeLogin(javax.servlet.ServletRequest,
	 *      javax.servlet.ServletResponse)
	 */
	@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;
	}

	/**
	 * @Title: refreshToken @Description:
	 *         TODO(刷新token,具体原理:先查看redis是否存在该key。如果存在取出时间。然后与AccessToken中的时间进行比对。相同则进行更新redis中key的失效时间。提交给shiro进行再次登录,将新生成的token写入到response中返回) @param: @param
	 *         request @param: @param response @param: @return @return:
	 *         boolean @throws
	 */
	private boolean refreshToken(ServletRequest request, ServletResponse response) {
		// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
		String token = this.getAuthzHeader(request);
		// 获取当前Token的帐号信息
		String account = JwtUtil.getClaim(token, CommonConstant.ACCOUNT);
		// 判断Redis中RefreshToken是否存在
		if (RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
			// Redis中RefreshToken还存在,获取RefreshToken的时间戳
			String currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
			// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
			if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				// 获取当前最新时间戳
				String currentTimeMillis = String.valueOf(System.currentTimeMillis());
				// 读取配置文件,获取refreshTokenExpireTime属性
				PropertiesUtil.readProperties("application.yml");
				String refreshTokenExpireTime = PropertiesUtil.getProperty("refreshTokenExpireTime");
				// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
				RedisUtil.setEx(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
						Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
				// 刷新AccessToken延长过期时间,设置时间戳为当前最新时间戳
				token = JwtUtil.sign(account, currentTimeMillis);
				// 将新刷新的AccessToken再次进行Shiro的登录
				JWTToken jwtToken = new JWTToken(token);
				// 提交给userRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
				this.getSubject(request, response).login(jwtToken);
				// 将token刷入response的header中
				HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
				httpServletResponse.setHeader("Authorization", token);
				httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
				return true;
			}
		}
		return false;
	}

	// 缺少权限内部转发至401处理
	private void response401(ServletRequest request, ServletResponse response, String msg) {
		HttpServletRequest req = (HttpServletRequest) request;
		try {
			req.getRequestDispatcher("/user/401").forward(request, response);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

六:UserRealm的编写

自定义的realm。可以完成对用户权限的写入和验证

@Service
//@Component  //经测试两个注解在此处的用户相同
public class UserRealm extends AuthorizingRealm {

	@Autowired
	private UserMapper userMapper;
	@Autowired
	private PermissionMapper permissionMapper;
	@Autowired
	private RoleMapper roleMapper;

	/**
	 * <p>
	 * Title: supports
	 * </p>
	 * <p>
	 * Description: 大坑,必须重写此方法,不然Shiro会报错
	 * </p>
	 * 
	 * @param token
	 * @return
	 * @see org.apache.shiro.realm.AuthenticatingRealm#supports(org.apache.shiro.authc.AuthenticationToken)
	 */
	@Override
	public boolean supports(AuthenticationToken authenticationToken) {
		return authenticationToken instanceof JWTToken;
	}

	/**
	 * <p>
	 * Title: doGetAuthorizationInfo
	 * </p>
	 * <p>
	 * Description: 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
	 * </p>
	 * 
	 * @param principals
	 * @return
	 * @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		// 从PrincipalCollection中获取token进行验证
		String account = JwtUtil.getClaim(principals.toString(), CommonConstant.ACCOUNT);
		UserDto userDto = new UserDto();
		userDto.setAccount(account);
		// 通过账号获取到该用户的角色信息
		List<RoleDto> roleList = roleMapper.findRoleListByAccount(userDto);
		// 遍历角色信息
		for (RoleDto roleDto : roleList) {
			if (null != roleDto) {
				// 添加该用户的角色信息
				simpleAuthorizationInfo.addRole(roleDto.getName());
				// 根据用户角色查询权限
				List<PermissionDto> permissionDtos = permissionMapper.findPermissionByRole(roleDto);
				for (PermissionDto permissionDto : permissionDtos) {
					if (null != permissionDto) {
						// 添加权限
						simpleAuthorizationInfo.addStringPermission(permissionDto.getAction());
					}
				}
			}
		}
		return simpleAuthorizationInfo;
	}

	/**
	 * <p>
	 * Title: doGetAuthenticationInfo
	 * </p>
	 * <p>
	 * Description:默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
	 * </p>
	 * 
	 * @param token
	 * @return
	 * @throws AuthenticationException
	 * @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		// 获取到token
		String token = (String) authenticationToken.getCredentials();
		// 获取到token信息用于和数据库对比
		String account = JwtUtil.getClaim(token, CommonConstant.ACCOUNT);
		if (StringUtil.isBlank(account)) {
			throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
		}
		// 去数据库查询用户是否存在
		UserEntity userDto = new UserDto();
		userDto.setAccount(account);
		userDto = userMapper.selectOne(userDto);
		if (userDto == null) {
			throw new AuthenticationException("该帐号不存在(The account does not exist.)");
		}
		// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
		if (JwtUtil.verify(token) && RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
			// 获取RefreshToken的时间戳
			// Redis中RefreshToken还存在,获取RefreshToken的时间戳
			String currentTimeMillisRedis = RedisUtil.get(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
			// 获取AccessToken时间戳,与RefreshToken的时间戳对比
			if (JwtUtil.getClaim(token, CommonConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				return new SimpleAuthenticationInfo(token, token, "userRealm");
			}
		}
		throw new AuthenticationException("Token已过期(Token expired or incorrect.)");
	}
}

七:shiroConfig的编写

shiro配置相关。添加自己的过滤规则。

@Configuration
public class ShiroConfig {
	/**
	 * @Title: defaultWebSecurityManager @Description:
	 *         TODO(配置使用自定义Realm,关闭Shiro自带的session) @param: @param userRealm
	 *         自定义的Realm @param: @return @return: DefaultWebSecurityManager @throws
	 */
	@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
	@Bean("securityManager")
	public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm) {
		DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
		// 使用自定义Realm
		defaultWebSecurityManager.setRealm(userRealm);
		// 关闭Shiro自带的session
		DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
		DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
		defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
		subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
		defaultWebSecurityManager.setSubjectDAO(subjectDAO);
		// 设置自定义Cache缓存
		defaultWebSecurityManager.setCacheManager(new UserCacheManager());
		return defaultWebSecurityManager;
	}

	/**
	 * @Title: shiroFilterFactoryBean @Description:
	 * TODO(添加自己的过滤器规则--详情见文档shiroRules) @param: @param
	 * securityManager @param: @return @return: ShiroFilterFactoryBean @throws
	 */
	@Bean("shiroFilter")
	public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
		ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
		// 添加自己的过滤器取名为jwt
		Map<String, Filter> filterMap = new HashMap<>(16);
		filterMap.put("jwt", new JWTFilter());
		factoryBean.setFilters(filterMap);
		factoryBean.setSecurityManager(securityManager);
		// 自定义url规则使用LinkedHashMap有序Map
		LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(16);
		// Swagger接口文档
		filterChainDefinitionMap.put("/v2/api-docs", "anon");
		filterChainDefinitionMap.put("/webjars/**", "anon");
		filterChainDefinitionMap.put("/swagger-resources/**", "anon");
		filterChainDefinitionMap.put("/swagger-ui.html", "anon");
		filterChainDefinitionMap.put("/doc.html", "anon");
		// 所有请求通过我们自己的JWTFilter
		filterChainDefinitionMap.put("/**", "jwt");
//		if (Config.shiroConfig) {
//			filterChainDefinitionMap.put("/**", "jwt"); // 放行所有
//		} else {
//			filterChainDefinitionMap.put("/login/**", "anon");
//		}
		
		factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return factoryBean;

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

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

八:controller,service的编写

@RestController
@RequestMapping("/user")
@ApiOperation(value = "用户管理",notes = "user manager")
public class UserController {

	@Autowired
	private UserService userService;
	
	@ApiOperation(value = "统一报错接口",notes = "error")
	@RequestMapping(value = "/401",method = RequestMethod.POST)
	@ResponseBody
	public ResponseBean login401() {
		return new ResponseBean(401,"Authc ERROR",false);
	}
	
	
	@ApiOperation(value = "用户登录接口",notes = "Login")
	@RequestMapping(value = "/login",method = RequestMethod.POST)
	@ResponseBody
	public ResponseBean login(@RequestBody UserPo user, HttpServletResponse httpServletResponse) {
		if(StringUtil.isBlank(user.getAccount())||StringUtil.isBlank(user.getPassword())) {
			return new ResponseBean(0,"用户名或者密码为空",false);
		}
		return userService.login(user,httpServletResponse);
	}
	
	@ApiOperation(value = "用户登录接口",notes = "Login")
	@RequiresRoles(value = "admin")//--通过角色来控制访问权限。管理员可以访问
	@RequestMapping(value = "/userList",method = RequestMethod.POST)
	@ResponseBody
	public ResponseBean userList(@RequestBody UserPo user) {
		return userService.userList();
	}
	
	
	@ApiOperation(value = "根据角色所绑定的权限去启用接口",notes = "Login")
	@RequiresPermissions(value = "user:add")
	@RequestMapping(value = "/testPermission",method = RequestMethod.POST)
	@ResponseBody
	public ResponseBean testPermission(@RequestBody UserPo user) {
		return new ResponseBean(100,"你所在的权限拥有  {user:add} 页面权限  ",true);
	}
}
@Service
@Transactional
public class UserServiceImpl implements UserService {

	@Autowired
	private UserMapper userMapper;

	@Value("${refreshTokenExpireTime}")
	private  String refreshTokenExpireTime;
//	@Autowired
//	private RedisUtil redisUtil;

	@Override
	public ResponseBean login(UserPo user, HttpServletResponse httpServletResponse) {
		// 对密码进行加密
		String password = MD5.GetMD5Code(user.getPassword());
		UserEntity userEntity = new UserEntity();
		userEntity.setAccount(user.getAccount());
		userEntity.setPassword(password);
		UserEntity result = userMapper.selectOne(userEntity);
		if (null == result) {
			return new ResponseBean(0, "用户名或者密码错误", false);
		}
		// 清除可能存在的Shiro权限信息缓存
		if (RedisUtil.hasKey(CommonConstant.PREFIX_SHIRO_CACHE + user.getAccount())) {
			// 删除
			RedisUtil.delete(CommonConstant.PREFIX_SHIRO_CACHE + user.getAccount());
		}
		// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
		String currentTimeMillis = String.valueOf(System.currentTimeMillis());
		RedisUtil.setEx(CommonConstant.PREFIX_SHIRO_REFRESH_TOKEN + user.getAccount(), currentTimeMillis,
				Long.parseLong(refreshTokenExpireTime), TimeUnit.SECONDS);
		// 使用jwt进行登录
		String token = JwtUtil.sign(user.getAccount(), currentTimeMillis);
		// 从Header中Authorization返回AccessToken,时间戳为当前时间戳
		httpServletResponse.setHeader("Authorization", token);
		httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
		return new ResponseBean(HttpStatus.OK.value(), "登录成功(Login Success.)", null);
	}

	@Override
	public ResponseBean userList() {
		List<UserEntity> selectAll = userMapper.selectAll();
		return new ResponseBean(HttpStatus.OK.value(), "查询成功", selectAll);
	}
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值