springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(1)

springsecurity和apach shiro 都是目前常用的为企业应用系统提供安全访问控制方案的框架,其中shiro相对于Spring Security 更加轻量级,配置容易,功能也相对简单。适用于传统SSM 项目。而spring security比shiro功能上要多一点,上手较难,但它和spring框架无缝对接,比较适用于springboot项目。

首先需要在idea中创建一个springboot项目:
在这里插入图片描述

spring boot版本默认选择2.2.6.勾选上spring web,mysqldriver,springsecurity,mybatis 依赖。

点击next后等待idea下载项目依赖完毕。

完成后发现idea自动给我们下载了很多springsecurit的相关依赖,springsecurity版本为5.2.2 查阅官方文档可知,我们仅需要添加
spring-boot-starter-security 这一个依赖即可在项目中使用springsecurity
在这里插入图片描述

从如下官方文档截图中可知,在springboot中使用springSecurity只要在pom文件中添加spring-boot-starter-security即可,springboot 会为我们自动生成一些springsecutiry的默认配置:
在这里插入图片描述
springboot为我们做了以下默认配置:
1.创建并注册了一个servlet过滤器:springSecurityFilterChain,这个过滤器负责了我们程序中的所有安全相关的功能,包括程序的url保护,提交的用户名密码验证,重定向到登录表单等等

2.所有和该应用程序的交互都需要一个被认证过的用户。创建了一个userDetailService类的 bean对象,其中用户名为“user”,随机生成的密码会在程序启动时输出到控制台上,通过该用户名及密码才能登录应用。

3.为程序自动生成了一个默认的登录表单页面,会用BCrypt方式为密码提供存储加密,并且提供了用户退出登录功能。

4.提供了针对CRSF攻击(跨站请求伪造)和sessionFixation(会话固定攻击)的防护

5.对请求头的一些集成安全措施:包括HTTP严格安全传输功能(告诉浏览器只能通过HTTPS访问当前资源, 禁止HTTP方式。),响应头content-type类型集成安全性配置(X-Content-Type-Options ),缓存控制(应用可以重新配置该项以便缓存静态资源),Xss(跨站脚本攻击)防护,响应头iframe点击劫持攻击的集成安全性配置。

6,实现了一些servletApi (HttpServletRequest) 的方法,包括:
getRemoteUser, getUserPrincipal, isUserInRole, login,logOut

仅靠springboot的默认配置肯定不够,我们需要新建 WebSecurityConfig 配置类,继承WebSecurityConfigAdapter类,WebSecurityConfigAdapter是springSecurity提供的默认配置适配器类,并重写三个configure方法。之后我们对 springsecurity 的个性化配置基本都会在这几个方法中进行

在这里插入图片描述

配置用户未认证请求处理方案

这时候我们直接启动项目访问localhost:8080/,可以看到访问路径直接跳转到了springSecurity默认提供的登陆表单页面:
在这里插入图片描述
这是由于第一次访问时session中没有记录登录信息,于是请求被拦截并默认跳转到登录页面,重定向url为/login。在前后端分离项目中,若用户没有登录,则应该返回json信息给前端以提示用户登录,而不是直接跳转,因此需要屏蔽 springSecurity 的这一默认行为。为实现该功能我们必须先了解框架内部默认跳转行为的实现机制。
看一下控制台的输出日志:
在这里插入图片描述
可以看到 “/” 请求路径似乎经过了15 个框架内部提供的过滤器,最后在 AffirmativeBased 类中抛出了 AccessDeniedException 异常:

if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

然后在 ExceptionTranslationFilter 过滤器中捕获异常并处理:

else if (exception instanceof AccessDeniedException) {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				logger.debug(
						"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
						exception);

				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				logger.debug(
						"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
						exception);

				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}

看其中的 sendStartAuthentication 方法的逻辑:

protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// SEC-112: Clear the SecurityContextHolder's Authentication, as the
		// existing Authentication is no longer considered valid
		SecurityContextHolder.getContext().setAuthentication(null);
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
		authenticationEntryPoint.commence(request, response, reason);
	}

最后一步调用了authenticationEntryPoint 类的commence方法,这是一个接口,debug一下,看看具体调用了哪个实现类:
在这里插入图片描述
发现是调用了 DelegatingAuthenticationEntryPoint 类的 commence 方法,

public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {

		for (RequestMatcher requestMatcher : entryPoints.keySet()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Trying to match using " + requestMatcher);
			}
			if (requestMatcher.matches(request)) {
				AuthenticationEntryPoint entryPoint = entryPoints.get(requestMatcher);
				if (logger.isDebugEnabled()) {
					logger.debug("Match found! Executing " + entryPoint);
				}
				entryPoint.commence(request, response, authException);
				return;
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("No match found. Using default entry point " + defaultEntryPoint);
		}

		// No EntryPoint matched, use defaultEntryPoint
		defaultEntryPoint.commence(request, response, authException);
	}

在这里插入图片描述
接着debug,发现最终调用了 LoginUrlAuthenticationEntryPoint 的commence方法,接着调试:

public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {

		String redirectUrl = null;

		if (useForward) {

			if (forceHttps && "http".equals(request.getScheme())) {
				// First redirect the current request to HTTPS.
				// When that request is received, the forward to the login page will be
				// used.
				redirectUrl = buildHttpsRedirectUrlForRequest(request);
			}

			if (redirectUrl == null) {
				String loginForm = determineUrlToUseForThisRequest(request, response,
						authException);

				if (logger.isDebugEnabled()) {
					logger.debug("Server side forward to: " + loginForm);
				}

				RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

				dispatcher.forward(request, response);

				return;
			}
		}
		else {
			// redirect to login page. Use https if forceHttps true

			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

		}

		redirectStrategy.sendRedirect(request, response, redirectUrl);
	}

在这里插入图片描述
到这一步准备跳转至 “/login" ,基于以上分析,我们发现要想改变框架的默认跳转行为,首先我们要实现自己的 AuthenticationEntryPoint 类 并重写 commence 方法。其次,要让框架使用我们自定义的 AuthenticationEntryPoint 实现类。

我们接着分析:
首先还是回到 ExceptionTranslationFilter.sendStartAuthentication 方法中,之前调试的时候发现此时的 authenticationEntryPoint 实现类是 DelegatingAuthenticationEntryPoint ,我们要将这个类换成自定义的AuthenticationEntryPoint 实现类,而authenticationEntryPoint 是 ExceptionTranslationFilter 的一个属性,看一下它的构造方法:

public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
			RequestCache requestCache) {
		Assert.notNull(authenticationEntryPoint,
				"authenticationEntryPoint cannot be null");
		Assert.notNull(requestCache, "requestCache cannot be null");
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}

也就是说在 ExceptionTranslationFilter 被创建时 会设置其 authenticationEntryPoint 属性,看一下此时的调用堆栈信息
在这里插入图片描述找到 ExceptionHandlingConfigurer 的 configure方法:

@Override
	public void configure(H http) {
		AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
		ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
				entryPoint, getRequestCache(http));
		AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
		exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
		exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
		http.addFilter(exceptionTranslationFilter);
	}

在这个方法中实例化了一个 ExceptionTranslationFilter 对象 并传入一个 AuthenticationEntryPoint 对象 ,而这个对象是由getAuthenticationEntryPoint 方法 返回的:

AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
		AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
		if (entryPoint == null) {
			entryPoint = createDefaultEntryPoint(http);
		}
		return entryPoint;
	}

这个方法首先判断 entryPoint 属性是否为空,不为空则返回,为空则创建一个默认的 entryPoint

private AccessDeniedHandler createDefaultDeniedHandler(H http) {
		if (this.defaultDeniedHandlerMappings.isEmpty()) {
			return new AccessDeniedHandlerImpl();
		}
		if (this.defaultDeniedHandlerMappings.size() == 1) {
			return this.defaultDeniedHandlerMappings.values().iterator().next();
		}
		return new RequestMatcherDelegatingAccessDeniedHandler(
				this.defaultDeniedHandlerMappings,
				new AccessDeniedHandlerImpl());
	}

因此我们可以自定义一个 entryPoint 并用来设置 ExceptionHandlingConfigurer 对象的 entryPoint 属性,同样的,还是在 ExceptionHandlingConfigurer 的构造方法上打断点,看看它的调用栈:
在这里插入图片描述
在这里插入图片描述
找到 HttpSecurity 的 exceptionHandling 方法

public ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling() throws Exception {
		return getOrApply(new ExceptionHandlingConfigurer<>());
	}
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
			C configurer) throws Exception {
		C existingConfig = (C) getConfigurer(configurer.getClass());
		if (existingConfig != null) {
			return existingConfig;
		}
		return apply(configurer);
	}

首先调用 getConfigurer 方法 获取一个 ExceptionHandlingConfigurer ,若不存在则调用apply 方法 创建一个,getConfigurer 和apply 方法的源码就不再深入分析了,总的来说,这个方法 只在第一次调用的时候才会创建一个 ExceptionHandlingConfigurer ,而默认情况下 ExceptionHandlingConfigurer 的 entryPoint 属性为空,只能通过调用它的 authenticationEntryPoint 方法来设置:

public ExceptionHandlingConfigurer<H> authenticationEntryPoint(
			AuthenticationEntryPoint authenticationEntryPoint) {
		this.authenticationEntryPoint = authenticationEntryPoint;
		return this;
	}

因此我们可以在 WebSecurityConfig 配置类中的 configurer(HttpSecurity http) 方法中调用httpSecurity 的 exceptionHandling 方法,获取 ExceptionHandlingConfigurer 对象,然后调用它的 authenticationEntryPoint 方法设置自定义的 entryPoint,具体配置如下:

//自定义用户未登录时异常处理类
    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests().antMatchers("/login").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().and().httpBasic();
        http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint);   //用户未登录的异常处理逻辑
        http.csrf().disable();
    }
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
    {
        response.setContentType("text/json;charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        printWriter.write("用户未登陆!");
        printWriter.flush();
        printWriter.close();

    }
}

重启应用访问 localhost:8080/ ,这次就不会再跳转到登录页面了
在这里插入图片描述
这样前端项目拿到json信息后可以再做页面跳转等相关操作。
按照如上的配置,显然在用户登录成功之前,自定义的 AuthenticationEntryPoint 会拦截所有的请求,这就会导致用户正常的登录请求也被拦截,因此我们需要单独配置正常登录请求不被拦截。
首先先我们要找出框架默认的登录请求路径。我们先把刚才的自定义 entryPoint 配置注释掉,重启项目访问localhost:8080/login, 此时跳转至默认的登录页面。之前说到,springSecurity默认创建了一个用户user用于登录,登录密码会在项目启动时打印到控制台。

在这里插入图片描述

输入用户名user,密码, 点击登录,看一下控制台输出:
在这里插入图片描述
可以看到,默认的登录请求路径为 “/login” 并且应该是个 Post 请求 .
接下类还是把自定义entryPoint 配置 加上,使用 PostMan 发送Post请求至 localhost:8080/login
在这里插入图片描述
和预想的一样,请求被拦截了,看一下控制台输出:
在这里插入图片描述
控制台输出和之前分析的基本类似,是在 AffirmativeBased 类中抛出了异常然后被 ExceptionTranslationFilter 捕获最终 跳转到 entryPoint 中。看一下 AffirmativeBased 的 代码 ,只有 vote 方法的返回值为-1 才会抛出异常,接着debug,进入vote 方法:

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;

		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);

		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

最后会调用 ExpressionUtils 类的 evaluateAsBoolean 方法,
在这里插入图片描述
在这里调用了 Spel 表达式的 getValue 方法 获取表达式的值,表达式 exp 为 “authenticated”
在这里插入图片描述
ctx 为standardEvaluationContext 类对象,其中的rootObject 为 WebExpressionRoot 对象,按照spel表达式的用法,这里会调用 WebExpressionRoot 对象的 isAuthenticated 方法
在这里插入图片描述

public final boolean isAnonymous() {
		return trustResolver.isAnonymous(authentication);
	}

	public final boolean isAuthenticated() {
		return !isAnonymous();
	}

isAuthenticated() 方法其实就是调用了 isAnonymous 方法,只不过对其结果进行取反,接着看 isAnonymous 方法:

public boolean isAnonymous(Authentication authentication) {
		if ((anonymousClass == null) || (authentication == null)) {
			return false;
		}

		return anonymousClass.isAssignableFrom(authentication.getClass());
	}

这个方法的大意是,如果 autnenticatin 对象类是 anonymousClass 的子类,则返回true,相应的 isAuthenticated 方法会返回false,最终导致 vote 方法返回-1。 而 anonymousClass 为 AnonymousAuthenticationToken。
在这里插入图片描述
因此 expression 的值是决定请求是否被拦截的关键,
在 SecurityExpressionRoot 中 有一个 permitAll 属性 其值始终为true。这个属性的作用我们之后再分析。

在这里插入图片描述

我们从 vote 方法开始按照调用堆栈,往上找 expression 的来源:

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

		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);

		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

在vote 方法中 WebExpressionConfigAttribute 变量 通过 findConfigAttribute 方法获取。

private WebExpressionConfigAttribute findConfigAttribute(
			Collection<ConfigAttribute> attributes) {
		for (ConfigAttribute attribute : attributes) {
			if (attribute instanceof WebExpressionConfigAttribute) {
				return (WebExpressionConfigAttribute) attribute;
			}
		}
		return null;
	}

在 findConfigAttribute 方法 中 遍历 attributes 变量,如果 当前遍历的 ConfigAttribute 类型是 WebExpressionConfigAttribute 则强制类型转换后返回。而expression 是通过 调用 WebExpressionConfigAttribute 的 .getAuthorizeExpression() 获取的,因此 expression 是 WebExpressionConfigAttribute 类的 authorizeExpression 属性值。
在这里插入图片描述

接着往上找 ConfigAttribute 集合 的来源:
在这里插入图片描述

在 AbstractSecurityInterceptor 类 的 beforeInvocation 方法中 调用了 DefaultFilterInvocationSecurityMetadataSource 类的 getAttributes 方法获取 attributes ,进入这个方法:

public Collection<ConfigAttribute> getAttributes(Object object) {
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
				.entrySet()) {
			if (entry.getKey().matches(request)) {
				return entry.getValue();
			}
		}
		return null;
	}

在这里插入图片描述
requesretMap 是 Map<RequestMatcher, Collection> 类型,
遍历 requestMap 调用 RequestMatcher的matches 方法,匹配 request 对象
匹配成功则 返回value 值即 ConfigAttribute集合。

当前 requestMap 中只有唯一的 key AnyRequestMacher 对象,其 matches 方法始终返回 true ,可以匹配所有请求路径,因此直接返回其value 值 WebExpressionConfigAttribute 对象。
requestMap 是 DefaultFilterInvocationSecurityMetadataSource 类的属性。在其构造方法中 会将 requestMap 注入进来,在构造方法打断点,重启应用查看调用堆栈,找到 requestMap 的初始化方法:
在这里插入图片描述
在 ExpressionUrlAuthorizationConfigurer 类的 createMetadataSource 方法中,调用 REGISTRY.createRequestMap() 方法,该方法其实是遍历了 REGISTRY 的 urlMapping 属性来创建 requestMap,而此时 urlMapping 属性已经有值了,REGISTRY 会在 ExpressionUrlAuthorizationConfigurer 的构造方法中进行注入,同样的套路 在构造方法打断点,看调用堆栈找到 REGISTRY 的 urlMapping 属性的赋值时机:
在这里插入图片描述
在这里插入图片描述
WebSecurityConfigurerAdapter 是我们自定义 配置类的父类。在它的 configure(HttpSecurity http) 方法中 调用了httpSecurity 的 authorizeRequests() 方法,这个方法初始化了 ExpressionUrlAuthorizationConfigurer

protected void configure(HttpSecurity http) throws Exception {
		logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

		http
			.authorizeRequests()
				.anyRequest().authenticated()
				.and()
			.formLogin().and()
			.httpBasic();
	}
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
			throws Exception {
		ApplicationContext context = getContext();
		return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context))
				.getRegistry();
	}

类似于之前分析过的,getOrApply 方法只在第一次调用的时候会初始化 ExpressionUrlAuthorizationConfigurer 对象,之后调用时仅仅获取已存在的实例,获取到之后在返回 ExpressionUrlAuthorizationConfigurer 对象的 REGISTRY 属性。但此时 REGISTRY 中的 urlMapping 属性为空,说明 urlMapping 属性是在之后的处理中被初始化的。
因此接着看 anyRequest() 方法:

public C anyRequest() {
		Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
		C configurer = requestMatchers(ANY_REQUEST);
		this.anyRequestConfigured = true;
		return configurer;
	}

public C requestMatchers(RequestMatcher... requestMatchers) {
		Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
		return chainRequestMatchers(Arrays.asList(requestMatchers));
	}
protected final C chainRequestMatchers(List<RequestMatcher> requestMatchers) {
		this.unmappedMatchers = requestMatchers;
		return chainRequestMatchersInternal(requestMatchers);
	}
@Override
		protected final AuthorizedUrl chainRequestMatchersInternal(
				List<RequestMatcher> requestMatchers) {
			return new AuthorizedUrl(requestMatchers);
		}

这个方法的作用主要是传入了一个 AnyRequestMatcher 类的实例,将其放入一个 list 中,然后构造一个 AuthorizedUrl 对象,并将这个 list 赋给 AuthorizedUrl 的 requestMatchers 属性 然后返回 AuthorizedUrl 对象。
AuthorizedUrl 类是 ExpressionUrlAuthorizationConfigurer 的内部类。它的具体源码就不再深入分析了
接着来看 authenticated() 方法:

public ExpressionInterceptUrlRegistry authenticated() {
			return access(authenticated);
		}

access 方法的参数 authenticated 是一个String 类型的常量。
在这里插入图片描述
总算是找到了 “authenticated” 的来源,access 方法传入了“authenticated” 字符串,

public ExpressionInterceptUrlRegistry access(String attribute) {
			if (not) {
				attribute = "!" + attribute;
			}
			interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
			return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
		}

这里的 createList 方法是将 attributeauthen(即”authenticated“字符串)封装为一个 SecurityConfig 对象,SecurityConfig 是 ConfigAttribute 的实现类然后放入一个 list 中返回。

public static List<ConfigAttribute> createList(String... attributeNames) {
		Assert.notNull(attributeNames, "You must supply an array of attribute names");
		List<ConfigAttribute> attributes = new ArrayList<>(
				attributeNames.length);

		for (String attribute : attributeNames) {
			attributes.add(new SecurityConfig(attribute.trim()));
		}

		return attributes;
	}

再看 interceptUrl 方法:

private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
			Collection<ConfigAttribute> configAttributes) {
		for (RequestMatcher requestMatcher : requestMatchers) {
			REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
					requestMatcher, configAttributes));
		}
	}

这里的UrlMapping 方法 将 requestMatcher 和 之前 createList 方法封装好的 ConfigAttribute 集合对象 一起封装为一个 UrlMapping 对象。

static final class UrlMapping {
		private RequestMatcher requestMatcher;
		private Collection<ConfigAttribute> configAttrs;

		UrlMapping(RequestMatcher requestMatcher, Collection<ConfigAttribute> configAttrs) {
			this.requestMatcher = requestMatcher;
			this.configAttrs = configAttrs;
		}

这里的 requestMatcher 集合 就是 之前 调用 authenticated() 方法 封装好的 AuthorizedUrl 对象 的 requestMatchers 属性。
然后调用 addMapping 方法将 UrlMapping 对象添加到 REGISTRY 的 urlMappings 属性中(该属性类型为 UrlMapping 对象集合)。

final void addMapping(UrlMapping urlMapping) {
		this.unmappedMatchers = null;
		this.urlMappings.add(urlMapping);
	}

至此我们发现,WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法主要就是 初始化了 ExpressionUrlAuthorizationConfigurer,并且 在其 REGISTRY 属性的 urlMappings 属性中填入 UrlMapping 对象(RequestMatcher 和 ConfigAttribute 集合 的对应关系)。

WebSecurityConfigurerAdapter 中默认配置如下:

http.authorizeRequests().anyRequest().authenticated()

anyRequest()方法 生成了一个AnyRequestMacher 对象 拦截所有请求。

authenticated() 方法 传入 “authenticated” 字符串生成 ConfigAttribute 代表 所有请求都需要经过认证,未经过验证的请求都会被拦截。

因此,我们可以多次调用 httpSecurity 的 authorizeRequests() 方法 对不同请求设置不同的拦截和认证机制。

看一下和 anyRequest() 在同一个类中的 antMatchers(String… antPatterns) 方法。这个方法的实现和 anyRequest() 类似,区别在于可以传入多个请求路径字符串,对每一个 请求路径都生成一个对应的 AntPathRequestMatcher。

public C antMatchers(String... antPatterns) {
		Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
		return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
	}
public static List<RequestMatcher> antMatchers(String... antPatterns) {
			return antMatchers(null, antPatterns);
		}
public static List<RequestMatcher> antMatchers(HttpMethod httpMethod,
				String... antPatterns) {
			String method = httpMethod == null ? null : httpMethod.toString();
			List<RequestMatcher> matchers = new ArrayList<>();
			for (String pattern : antPatterns) {
				matchers.add(new AntPathRequestMatcher(pattern, method));
			}
			return matchers;
		}

接着再找到和 authenticated() 在同一类中的 permitAll() 方法. 该方法的实现和 authenticated() 类似,区别在于用于生成 ConfigAttribute 的 字符串 是 “permitAll”。
permitAll 表示 无需认证。和 authenticated 正好相反。
而在 SecurityExpressionRoot 中 正好有一个 permitAll 属性,
在这里插入图片描述
spel 的 getValue 方法会返回 permitAll 属性的值,而这个属性值始终为 true,这样就能保证请求不被拦截。因此我们可以在 configure(HttpSecurity http) 中 增加如下配置:

http.authorizeRequests().antMatchers("/login").permitAll();

保证 “/login" 不被拦截。

这里需要注意,这段配置必须放在 WebSecurityConfigurerAdapter 类 configure(HttpSecurity http) 方法中的默认配置 http.authorizeRequests().anyRequest().authenticated() 之前,否则会报错。

因为,在之前分析过的 DefaultFilterInvocationSecurityMetadataSource类 的 getAttributes 方法中,会遍历 requestMap,(requestMap 是通过遍历REGISTRY 的 urlMapping 属性得到的)。

只要请求和 requestMap 中的key(即 RequestMatcher)匹配,就直接返回 value值(ConfigAttribute集合),如果将 默认配置放在前面,那么无论什么请求都能匹配 AnyRequestMatcher,这样的话后面的 RequestMatcher 配置就不会生效。框架内部为了防止发生这种错误,如果将默认配置放在后面,在项目启动过程中会直接抛出异常。

配置完成后启动项目,访问localhost:8080/login ,此时就不会再出现未登录提示了
在这里插入图片描述

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值