spring security 源码分析 之 授权过程

授权

认证

在授权之前是需要用户先通过认证的,鉴权的意思就是对项目的资源进行权限的控制,**项目中的代码方法不是所有的用户都可以访问的, 访问特定的方法需要特定的权限才可以访问。**授权在security中也是帮助我们封装了相应的逻辑,我们只是需要提供相应的数据给security 就行了, 然后在需要权限控制的代码方法中进行限定即可。

在这里插入图片描述

前提准备

  • 用户
    • 项目中的使用者
  • 角色
    • 使用者拥有的角色, 比如管理员等
  • 权限值
    • 和角色是相对应的,一种角色对应一种权限值

对应数据表

1、权限表

CREATE TABLE `t_permission` (
  `id` int(32) NOT NULL,
  `code` varchar(32) NOT NULL COMMENT '权限标识符',
  `description` varchar(64) DEFAULT NULL COMMENT '描述',
  `url` varchar(128) DEFAULT NULL COMMENT '请求地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2、角色表

CREATE TABLE `t_role` (
  `id` varchar(32) NOT NULL,
  `role_name` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `status` char(1) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3、用户表

CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL COMMENT '用户id',
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  `fullname` varchar(255) NOT NULL COMMENT '用户姓名',
  `mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

4 、用户角色表

CREATE TABLE `t_user_role` (
  `user_id` varchar(32) NOT NULL,
  `role_id` varchar(32) NOT NULL,
  `create_time` datetime DEFAULT NULL,
  `creator` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

5、 角色权限表

CREATE TABLE `t_role_permission` (
  `role_id` varchar(32) NOT NULL,
  `permission_id` varchar(32) NOT NULL,
  PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

主要过程

  • 根据用户查出当前用户的角色信息
  • 根据角色信息查出权限值
  • 得到当前用户的所有权限值
  • 按照规则授权

授权分析

授权

Security的授权过程可以理解成各种 filter 处理最终完成一个授权。 在项目初始化的时候会创建Filter链

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5UqB8VQ-1597570804164)(E:\Code\my\spring_security\README.assets\1597563726543.png)]

spring security内置的各种filter:

AliasFilter ClassNamespace Element or Attribute
CHANNEL_FILTERChannelProcessingFilterhttp/intercept-url@requires-channel
SECURITY_CONTEXT_FILTERSecurityContextPersistenceFilterhttp
CONCURRENT_SESSION_FILTERConcurrentSessionFiltersession-management/concurrency-control
HEADERS_FILTERHeaderWriterFilterhttp/headers
CSRF_FILTERCsrfFilterhttp/csrf
LOGOUT_FILTERLogoutFilterhttp/logout
X509_FILTERX509AuthenticationFilterhttp/x509
PRE_AUTH_FILTERAbstractPreAuthenticatedProcessingFilter SubclassesN/A
CAS_FILTERCasAuthenticationFilterN/A
FORM_LOGIN_FILTERUsernamePasswordAuthenticationFilterhttp/form-login
BASIC_AUTH_FILTERBasicAuthenticationFilterhttp/http-basic
SERVLET_API_SUPPORT_FILTERSecurityContextHolderAwareRequestFilterhttp/@servlet-api-provision
JAAS_API_SUPPORT_FILTERJaasApiIntegrationFilterhttp/@jaas-api-provision
REMEMBER_ME_FILTERRememberMeAuthenticationFilterhttp/remember-me
ANONYMOUS_FILTERAnonymousAuthenticationFilterhttp/anonymous
SESSION_MANAGEMENT_FILTERSessionManagementFiltersession-management
EXCEPTION_TRANSLATION_FILTERExceptionTranslationFilterhttp
FILTER_SECURITY_INTERCEPTORFilterSecurityInterceptorhttp
SWITCH_USER_FILTERSwitchUserFilterN/A
  • SecurityContextPersistenceFilter 在后续的Filter中可以SecurityContext 使用来获取信息

    • 拦截我们的请求, 从session中获取SecurityContext 然后放到上下文中
    • loadContext 方法 获得 SecurityContext
    • 没有就会创建一个
  • FilterSecurityInterceptor

    • 会从SecurityMetadataSource 的子类
      DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限
    • FilterSecurityInterceptor 会调用AccessDecisionManager 进行授权决策
  • AccessDecisionManager 对于方法鉴权时候的投票机制

    • AffirmativeBased
    • ConsensusBased
    • UnanimousBased

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 这个过滤器会拦截我们的请求, 首先在他它的构造方法中会传递过一个HttpSessionSecurityContextRepository , 它是管理ServletContent 的工具(存放security的全局信息), 得到这个管理工具之后

	public SecurityContextPersistenceFilter() {
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (request.getAttribute(FILTER_APPLIED) != null) {
			// ensure that filter is only applied once per request
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();

		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
		// 封装请求
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
        // 去 HttpSessionSecurityContextRepository 中获取  SecurityContext
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			SecurityContextHolder.setContext(contextBeforeChainExecution);

			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}

repo.loadContext(holder);

HttpSessionSecurityContextRepository 获取全局的 SecurityContext, 会看到如果获取不到(用户没登录)就会创建一个。 readSecurityContextFromSession 方法中我们 可以看到是从session中取出名为SPRING_SECURITY_CONTEXT的attribute 可以看到,如果是之前登录过的,则session会关联上登录用户信息,包含用户的AuthenticationT信息,比如Principal,Granted Authorities等,这些都是重要的鉴权依据。

	public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
		HttpServletRequest request = requestResponseHolder.getRequest();
		HttpServletResponse response = requestResponseHolder.getResponse();
		HttpSession httpSession = request.getSession(false);

		SecurityContext context = readSecurityContextFromSession(httpSession);

		if (context == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("No SecurityContext was available from the HttpSession: "
						+ httpSession + ". " + "A new one will be created.");
			}
            // 创建一个
            /**	
            protected SecurityContext generateNewContext() {
				return SecurityContextHolder.createEmptyContext();
			}
			**/
			context = generateNewContext();

		}

		SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
				response, request, httpSession != null, context);
		requestResponseHolder.setResponse(wrappedResponse);

		requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(
				request, wrappedResponse));

		return context;
	}

AnonymousAuthenticationFilter

AnonymousAuthenticationFilter 在上面获得不到 SecurityContext 的时候就会到这个过滤器中 Authentication的值就是AnonymousAuthenticationToken , 然后在登录成功之后就会把 认证的用户信息 authentication写入到SecurityContextHolder的context中。

    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }
// authentication写入到SecurityContextHolder的context中。 
        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

再次 SecurityContextPersistenceFilter

try {
            SecurityContextHolder.setContext(contextBeforeChainExecution);

            chain.doFilter(holder.getRequest(), holder.getResponse());

        }
        finally {
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);

            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }

可以看到SecurityContextPersistenceFilter在filter执行完之后,finally的时候,调用了saveContext来保存context的东西到session中。 SecurityContextPersistenceFilter通过往session存取名为SPRING_SECURITY_CONTEXT,值为SecurityContext的attribute,来为后续filter建立所需的上下文,包括登录态。

AccessDecisionManager

public interface AccessDecisionManager {
/**

* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
  */
  void decide(Authentication authentication , Object object, Collection<ConfigAttribute>
  configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
  //略..
  }

这里着重说明一下decide的参数:

  • authentication:要访问资源的访问者的身份
  • object:要访问的受保护资源,web请求对应FilterInvocation
  • configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
  • decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

AccessDecisionManager 拥有三个不同的子类来实现具体的投票规则, 最终判断是否能够访问资源

授权时序图

在这里插入图片描述

测试

1、登录

  • SecurityContextPersistenceFilter 拦截
    • 可以看到contextBeforeChainExecution 是空的

在这里插入图片描述

2、加载用户信息
在这里插入图片描述

3、成功

在这里插入图片描述

4、访问接口, 显示的有当前用户的信息

  • SecurityContextPersistenceFilter 拦截
    • 可以看到contextBeforeChainExecution 显示的是当前用户的所有信息

在这里插入图片描述

  • AffirmativeBased 拦截
    • decide(Authentication authentication, Object object, Collection configAttributes) 投票

在这里插入图片描述

5、结果
在这里插入图片描述

项目地址

项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值