Spring Security如何鉴权源码分析

首先,我们新建一个SpringBoot项目,包含Security,Web,Thymeleaf(pom文件关键依赖如下所示,示例用的springboot版本为2.2.5.RELEASE)。

        <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
      </parent>
      ................
      ................
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

在templates目录下新建一个Home页面(如下所示)。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
    </body>
</html>

在templates目录下新建一个Hello页面(如下所示)。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello world!</h1>
    </body>
</html>

新建login页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

配置webmvc

package com.example.securingweb;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/home").setViewName("home");
		registry.addViewController("/").setViewName("home");
		registry.addViewController("/hello").setViewName("hello");
		registry.addViewController("/login").setViewName("login");
	}

}

接下来,就是重中之重,配置我们的SpringSecurity

package com.example.securingweb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/", "/home").permitAll()
				.anyRequest().authenticated()
				.and()
			.formLogin()
				.loginPage("/login")
				.permitAll()
				.and()
			.logout()
				.permitAll();
	}

	@Bean
	@Override
	public UserDetailsService userDetailsService() {
		UserDetails user =
			 User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();

		return new InMemoryUserDetailsManager(user);
	}
}

接下来分析我们刚才写的例子,当我要访问hello页面时,必须先经过login页面登录才能访问,那么这一个过程的流程是怎么样的呢?首先我们分析springsecurity的启动过程,springsecurity是如何实现鉴权的过程的呢?实际上是通过一系列的过滤器来实现的,那么我们首先通过springboot的日志来分析springsecurity到底创建了哪些filter。

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@106d77da
org.springframework.security.web.context.SecurityContextPersistenceFilter@78e17a99
org.springframework.security.web.header.HeaderWriterFilter@6614bd4b 
org.springframework.security.web.csrf.CsrfFilter@7ff8a9dc 
org.springframework.security.web.authentication.logout.LogoutFilter@39bbd9e0 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ba6ec50
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@41dc0598
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5d96bdf8
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6f9c5048
org.springframework.security.web.session.SessionManagementFilter@7847ef2c
org.springframework.security.web.access.ExceptionTranslationFilter@655a01d8
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6af5bbd0
WebAsyncManagerIntegrationFilter为请求处理过程中可能发生的异步调用准备安全上下文获取途径
SecurityContextPersistenceFilter整个请求处理过程所需的安全上下文对象SecurityContext的准备和清理不管请求是否针对需要登录才能访问的页面,这里都会确保SecurityContextHolder中出现一个SecurityContext对象:1.未登录状态访问登录保护页面:空SecurityContext对象,所含Authentication为null2.登录状态访问某个页面:从SecurityContextRepository获取的SecurityContext对象
HeaderWriterFilterSpring Securty 使用该Filter在一个请求的处理过程中为响应对象增加一些头部信息。头部信息由外部提供,比如用于增加一些浏览器保护的头部,比如X-Frame-Options, X-XSS-Protection和X-Content-Type-Options等
LogoutFilterLogoutFilter被设计用于检测用户退出登录请求,执行相应的处理工作以及退出登录后的页面跳转
UsernamePasswordAuthenticationFilter检测用户名/密码表单登录认证请求并作相应认证处理:1.session管理,比如为新登录用户创建新session(session fixation防护)和设置新的csrf token等2.经过完全认证的Authentication对象设置到SecurityContextHolder中的SecurityContext上;3.发布登录认证成功事件InteractiveAuthenticationSuccessEvent4.登录认证成功时的Remember Me处理5.登录认证成功时的页面跳转
RequestCacheAwareFilterSpring Security Web对请求提供了缓存机制,如果某个请求被缓存,它的提取和使用是交给RequestCacheAwareFilter完成的
SecurityContextHolderAwareRequestFilterSecurityContextHolderAwareRequestFilter对请求HttpServletRequest采用Wrapper/Decorator模式包装成一个可以访问SecurityContextHolder中安全上下文的SecurityContextHolderAwareRequestWrapper。这样接口HttpServletRequest上定义的getUserPrincipal这种安全相关的方法才能访问到相应的安全信息
AnonymousAuthenticationFilter如果当前SecurityContext属性Authentication为null,将其替换为一个AnonymousAuthenticationToken
SessionManagementFilter检测从请求处理开始到目前是否有用户登录认证,如果有做相应的session管理,比如针对为新登录用户创建新的session(session fixation防护)和设置新的csrf token等。
ExceptionTranslationFilter处理AccessDeniedException和 AuthenticationException异常,将它们转换成相应的HTTP响应
FilterSecurityInterceptor一个请求处理的安全处理过滤器链的最后一个,检查用户是否已经认证,如果未认证执行必要的认证,对目标资源的权限检查,如果认证或者权限不足,抛出相应的异常:AccessDeniedException或者AuthenticationException

先挑重要的Filter来看,我们首先找到UsernamePasswordAuthenticationFilter的源码,看看UsernamePasswordAuthenticationFilter干了什么。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

首先是从request中取出用户登录时输入的用户名和密码,然后交由对应的authenticationManager来进行校验,接下来打断点,继续跟进authenticate方法,首先是找到对应的provider,通过provider的supports的方法判断该provider是否支持该authentication,如果支持就调用该provider的authenticate方法。接下来断点跟进,发现到了这一个类AbstractUserDetailsAuthenticationProvider,找到该provider的authenticate方法,发现认证的流程是这样的:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

根据用户输入的username,从我们定义的UserDetailsService取出对应的用户信息,接下来就是校验的过程(断点继续跟进,跳到this.additionalAuthenticationChecks方法):

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

可以看到调用了passwordEncoder.matches方法来判断用户输入的密码和userDetails中的密码是否一致,如果不一致就抛出异常,如果一致就将封装一个新的authentication(包含用户名,密码,以及userDetails中的赋予的权限)

protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

好了,用户校验的步骤在这里就讲完了,接下来j讲鉴权的流程,上面的表格里面讲到了鉴权是在FilterSecurityInterceptor,并且鉴权是和AccessDecisionManager这个类有关。先来简单讲讲AccessDecisionManager。
AccessDecisionManager的接口表述非常的简单,简单来说就一个主要功能——为当前的访问规则进行决策,是否给予访问的权限。无论是decide方法还是supports方法,AccessDecisionManager本身并不完成相关的逻辑,全部交由其管理的AccessDecisionVoter依次去判断与执行。而根据decide的逻辑规则不同,Spring Security中分别存在三种不同decide决策规则的AccessDecisionManager,它们分别是:
1.AffirmativeBased
2.UnanimousBased
3.ConsensusBased

在Spring Security默认设置中,使用的是AffirmativeBased。
在详细介绍三种AccessDecisionManager的实现类前,我们先再来梳理下AccessDecisionManager与AccessDecisionVoter的在决策框架中的关系。
在框架设计中AccessDecisionManager是AccessDecisionVoter的集合类,管理着对于不同规则进行判断与表决的AccessDecisionVoter们。
但不同的是,AccessDecisionVoter分别都只会对自己支持的规则进行表决,如一个资源的访问规则存在多个并行时,便不能以某一个AccessDecisionVoter的表决作为最终的访问授权结果。AccessDecisionManager的职责便是在这种场景下,汇总所有AccessDecisionVoter的表决结果后给出一个最终的决策。从而导致框架中预设了三种不同决策规则的AccessDecisionManager的实现类。
1.一票通过AffirmativeBased
2.一票否决UnanimousBased
3.少数服从多数ConsensusBased
接下来讲AccessDecisionManager的小弟:AccessDecisionVoter,它主要的职责就是对它所对应的访问规则作出判断,当前的访问规则是否可以得到授权。
AccessDecisionVoter接口的主要方法其实与之前的AuthenticationProvider非常的相似。
boolean supports(ConfigAttribute attribute);

int vote(Authentication authentication, S object,
		Collection<ConfigAttribute> attributes);

supports方法用于判断对于当前ConfigAttribute访问规则是否支持;
如果支持的情况下,vote方法对其进行判断投票返回对应的授权结果。
最终的授权结果一共有三种,分别是同意、弃权和反对。说实话这个规则和联合国安理会投票差不多性质。当前一个访问可能存在多个规则的情况下,每一个AccessDecisionVoter投出自己的那一票,最终的投票结果是还是要看当前的投票规则,比如是超过1/3还是要过半数。而投票规则的判断则是被放置了在了AccessDecisionManager进行完成。
我们来看看FilterSecurityInterceptor这个类里面SpringSecurity做了哪些事情。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        this.invoke(fi);
    }

继续跟进代码,发现在AbstractSecurityInterceptor中的beforeInvocation方法中,有这样一行代码

try {
                    this.accessDecisionManager.decide(authenticated, object, attributes);
                } catch (AccessDeniedException var7) {
                    this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
                    throw var7;
                }

继续跟进,发现跳到了AffirmativeBased的decide方法。在decide方法里,先通过iterator迭代器获得默认AccessDecisionManage:AffirmativeBased中的DecisionVoter,我们继续跟进发现代码跳到了WebExpressionVoter中,我们的鉴权过程就发生在这个类里。

public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
        assert authentication != null;

        assert fi != null;

        assert attributes != null;

        WebExpressionConfigAttribute weca = this.findConfigAttribute(attributes);
        if (weca == null) {
            return 0;
        } else {
            EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, fi);
            ctx = weca.postProcess(ctx, fi);
            return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? 1 : -1;
        }
    }

看vote方法的最后一行代码,从authentication中取出权限然后与配置中取出的权限进行比对,如果不符合就投出反对票,由于默认的AccessDecisionManager只有一个voter,所以如果唯一的voter投了反对票,那么也就意味着没有通过票,最后抛出异常,返回403错误,提示没有权限,整个过程到此就完美画上了句号。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值