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的用户权限管理 (三) 鉴权

一,ShiroFilterFactoryBean配置

FilterChainDefinitionMap排除需要权限才能访问的URL,这样才能进入自定义过滤器JwtFilter中进行后续验证。

比如,测试权限的URl为/role/OneRole.do,它不需要写入FilterChainDefinitionMap中,如下图

 二,自定义ShiroSessionManager

认证成功后,将shiro生成的sessionid保存到了access_token中,所以携带token访问时,需要从token将其取出。

com.demo.config.ShiroConfig中

  /**
	 * 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);

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

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

		return securityManager;
	}

@Bean
  public ShiroSessionManager shiroSessionManager() {

    ShiroSessionManager shiroSessionManager = new ShiroSessionManager();

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

    return shiroSessionManager;
  }

com.demo.shiro.ShiroSessionManager

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

}

三,JwtFilter执行executeLogin();

/**
	 * 执行登录认证
	 * 
	 * @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;
		}
	}

四,UserModularRealmAuthenticator加载DemoRealm

login的时候创建的是CustomizedToken,之后的请求Header携带的都是JwtToken,因此区分加载哪一种类型的Realm。

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

五,DemoRealm进行鉴权(doGetAuthorizationInfo)

/**
	 * 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
	 *
	 * @param principals
	 * @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;
	}

六,getUserAccessInfo

com.demo.service.impl.UserServiceImpl的getUserAccessInfo方法

/**
	 * 获取用户角色set与权限set
	 *
	 * @param userId
	 * @return
	 */
	public UserAccessInfo getUserAccessInfo(String userId) {

		UserAccessInfo userAccessInfo = new UserAccessInfo();

		// 用户角色set
		Set<String> roleSet = new HashSet<String>();

		// 用户权限set
		Set<String> permissionSet = new HashSet<String>();

		UserMaster userMaster = userMasterMapper.selectByUserId(userId);

		// 账号类型
		String accountType = userMaster.getAccountType();

		// 管理员 accountType = 1
		if (Const.ADMIN_USER_CODE.equals(accountType)) {

			// admin
			roleSet.add(Const.ADMIN_USER);

			// Leader accountType = 2
		} else if (Const.LEADWER_USER_CODE.equals(accountType)) {

			// Leader
			roleSet.add(Const.LEADER_USER);

			// 普通用户 accountType = 3
		} else if (Const.NORMAL_USER_CODE.equals(accountType)) {

			// user
			roleSet.add(Const.NORMAL_USER);
		}

		// 用户权限类型
		String permissionType = userMaster.getPermissionType();

		if (Const.LEVEL_001_CODE.equals(permissionType)) {
			permissionSet.add(Const.LEVEL_001);

		} else if (Const.LEVEL_002_CODE.equals(permissionType)) {
			permissionSet.add(Const.LEVEL_002);

		} else if (Const.LEVEL_003_CODE.equals(permissionType)) {
			permissionSet.add(Const.LEVEL_003);

		} else if (Const.LEVEL_004_CODE.equals(permissionType)) {
			permissionSet.add(Const.LEVEL_004);

		} else if (Const.LEVEL_005_CODE.equals(permissionType)) {
			permissionSet.add(Const.LEVEL_005);
		}

		userAccessInfo.setRoleSet(roleSet);

		userAccessInfo.setPermissionSet(permissionSet);

		return userAccessInfo;
	}

七,简单测试

测试用的TestRoleController ,主要测试以下权限:

ps:由于之前测试用户表设计一个用户只对应一种角色一种权限,所以第四个测试只有失败的情况

  1.   @RequiresRoles(Const.ADMIN_USER)  角色-- 只有管理员用户才能访问
  2.   @RequiresRoles(value = { Const.ADMIN_USER, Const.LEADER_USER }, logical = Logical.OR)  角色-- 管理员用户和Leader用户都能访问
  3.   @RequiresPermissions(Const.LEVEL_004)  权限-- 001 能访问
  4.   @RequiresPermissions(value = {Const.LEVEL_004,Const.LEVEL_005}, logical = Logical.AND)  权限-- 004 且 005 才能访问
/*
 * Author  : baiye <baiye_21@163.com>
   Time    : 2021/06/30
   Function:
*/
@RestController
@RequestMapping("/role/")
public class TestRoleController {

    /**
     * 只有管理员用户才能访问
     *
     * @return
     * @throws Exception
     */
    @RequiresRoles(Const.ADMIN_USER)
    @RequestMapping(value = "/OneRole.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> OneRole() throws Exception {

        return ServerResponse.createBySuccessMessage("One Role");
    }

    /**
     * 管理员用户和Leader用户能访问
     *
     * @return
     * @throws Exception
     */
    @RequiresRoles(value = { Const.ADMIN_USER, Const.LEADER_USER }, logical = Logical.OR)
    @RequestMapping(value = "/TwoRole.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> TwoRole() throws Exception {

        return ServerResponse.createBySuccessMessage("Two Role");
    }

    /**
     * Permission 001 能访问
     *
     * @return
     * @throws Exception
     */
    @RequiresPermissions(Const.LEVEL_004)
    @RequestMapping(value = "/OnePermission.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> OnePermission() throws Exception {

        return ServerResponse.createBySuccessMessage("One Permission");
    }

    /**
     * Permission 004 且 005 能访问
     *
     * @return
     * @throws Exception
     */
    @RequiresPermissions(value = {Const.LEVEL_004,Const.LEVEL_005}, logical = Logical.AND)
    @RequestMapping(value = "/TwoPermission.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<String> TwoPermission() throws Exception {

        return ServerResponse.createBySuccessMessage("Two Permission");
    }
}

准备的测试用户

使用demo002用户登录

localhost:9999/role/OneRole.do

localhost:9999/role/TwoRole.do

 localhost:9999/role/OnePermission.do

 localhost:9999/role/TwoPermission.do