Spring Security -- Spring Boot中开启Spring Security


在介绍Spring Securiry之前,我们试想一下如果我们自己去实现一个安全框架,我们需要包含哪些功能:

  • 我们需要对登录接口或者一些不需要权限的接口放行,同时我们需要对某些接口进行身份认证,例如:在基于jwt的认证体系中,我们需要校验token是否合法,token合法我们才会放行;
  • 我们希望我们写的安全框架能够做一些授权的操作,比如:我们可以限制认证后的用户访问/user接口需要什么权限,访问/group接口又需要什么权限;
  • 我们希望安全框架能够提供一个缓存,无论是TreadLocal、还是HttpServletRequest也罢,只要能够获取保存当前认证通过的用户信息即可;

试想一下,如果我们去实现这些功能,我们需要怎么做:

  • 我们需要去拦截所有的HTTP请求,我们首先想到的实现方式就是filter、Spring AOP、Intercepter,这三者的实现方式和应用场景都不一样,这里我们不去细究采用哪种方式,但是我可以告诉你Spring Security是采用了一系列的filter实现的。
  • 假设我们也是采用的filter实现,那么我们是不是也要实现一个白名单啊,比如放行/login接口啊,然后剩下的接口,就要走认证流程;
  • 认证完之后,我们怎么做授权呢,我们可以这么做,我们先获取当前登录用户所拥有的权限,比如某某用户对接口资源user具有添加权限,采用这种格式:interface:user:add,我们将若干个这种格式的权限放到一个list,然后放到缓存中
[ "interface:user:add", "nterface:group:delete", ... ]
  • 然后我们干一件什么事呢,我们搞个@PreAuthorize注解,然后在搞个注解处理器,注解处理器可以使用Spring AOP去实现。这个注解怎么用呢,我们可以将这个注解加载UserController /user/add接口上:
   @PostMapping("/user/add") @PreAuthorize("interface:user:add") public NgspResponseEntity<User> insertUser(...)
  • 如果用户去访问/user/add接口,我们就去缓存中拉取用户的权限列表,然后去校验用户是否具有访问这个接口的权限,如果有那么我们就放行。

当然了上面只是一个简单的实现,Spring Security的实现那是太太太复杂了,他为了满足各种需求,允许我们自己去配置各种过滤器,功能是强大了,但是学习起来还是比较困难的。

下面我将带领大家来学习Spring Security框架,Spring Security是一款基于Spring的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架Apache Shiro相比,它拥有更为强大的功能。Spring Security也可以轻松的自定义扩展以满足各种需求,并且对常见的Web安全攻击提供了防护支持。如果你的Web框架选择的是Spring,那么在安全方面Spring Security会是一个不错的选择。

一、开启Spring Security

1、导入依赖

创建一个Spring Boot项目springboot-springsecurity,然后引入spring-boot-starter-security:

        <!-- Spring Security的maven依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2、创建Controller

新建包com.goldwind.controller,接下来我们创建一个TestController,对外提供一个/hello服务:

package com.goldwind.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: zy * @Description: 测试 * @Date: 2020-2-9 */ @RestController public class TestController { @GetMapping("hello") public String hello() { return "hello spring security"; } }

3、新建App入口

新建入口程序App.java:

package com.goldwind; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @Author: zy * @Description: 启动程序 * @Date: 2020-2-9 */ @SpringBootApplication public class App { public static void main(String[] args){ SpringApplication.run(App.class,args); } }

这时候我们直接启动项目,访问http://localhost:8080/hello,可看到页面弹出了个formLogin认证框:

这个配置开启了一个form Login类型的认证,所有服务的访问都必须先过这个认证,默认的用户名为user,密码由Sping Security自动生成,回到IDE的控制台,可以找到密码信息:

Using generated security password: a77c9456-901e-4848-a221-3822347e52ea

输入用户名user,密码a77c9456-901e-4848-a221-3822347e52ea后,我们便可以成功访问/hello接口。

二、基于HTTP basic类型的认证

我们可以通过一些配置将表单认证修改为基于HTTP Basic的认证方式。

1、配置Spring Security

创建包com.goldwind.config,创建一个配置类WebSecurityConfig继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter这个抽象类并重写configure(HttpSecurity http)方法。WebSecurityConfigurerAdapter是由Spring Security提供的Web应用安全配置的适配器:

package com.goldwind.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * @Author: zy * @Description: spring security配置类 * @Date: 2020-2-9 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置拦截请求资源 * @param http:HTTP请求安全处理 * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() //HTTP Basic认证方式
 .and() .authorizeRequests() // 授权配置
            .anyRequest()  // 所有请求
            .authenticated(); // 都需要认证
 } }

Spring Security提供了这种链式的方法调用。上面配置指定了认证方式为HTTP Basic登录,并且所有请求都需要进行认证。

这里有一点需要注意,我没并没有在Spring Security配置类上使用@EnableWebSecurity注解。这是因为在非Spring Boot的Spring Web MVC应用中,注解@EnableWebSecurity需要开发人员自己引入以启用Web安全。而在基于Spring Boot的Spring Web MVC应用中,开发人员没有必要再次引用该注解,Spring Boot的自动配置机制WebSecurityEnablerConfiguration已经引入了该注解,如下所示:

package org.springframework.boot.autoconfigure.security.servlet; // 省略 imports 行
@Configuration // 仅在存在 WebSecurityConfigurerAdapter bean 时该注解才有可能生效 // (最终生效与否要结合其他条件综合考虑)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class) // 仅在不存在 springSecurityFilterChain 时该注解才有可能生效 // (最终生效与否要结合其他条件综合考虑)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) // 仅在 Servlet 环境下该注解才有可能生效 // (最终生效与否要结合其他条件综合考虑)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity // <====== 这里启用了 Web 安全
public class WebSecurityEnablerConfiguration { }

 实际上,一个Spring Web应用中,WebSecurityConfigurerAdapter可能有多个 , @EnableWebSecurity可以不用在任何一个WebSecurityConfigurerAdapter上,可以用在每个WebSecurityConfigurerAdapter上,也可以只用在某一个WebSecurityConfigurerAdapter上。多处使用@EnableWebSecurity注解并不会导致问题,其最终运行时效果跟使用@EnableWebSecurity一次效果是一样的。

 这时候我们重启项目,再次访问http://localhost:8080/hello,可以看到认证方式已经是HTTP Basic的方式了:

用户名依旧是user,密码由Spring Security自动生成,如果需要换回表单的认证方式,我们只需要简单修改configure方法中的配置:

@Override protected void configure(HttpSecurity http) throws Exception { // http.formLogin() // 表单方式
    http.httpBasic() // HTTP Basic方式
 .and() .authorizeRequests() // 授权配置
            .anyRequest()  // 所有请求
            .authenticated(); // 都需要认证
}

三、Spring Security基本原理

上面我们开启了一个最简单的Spring Security安全配置,下面我们来了解下Spring Security的基本原理。通过上面的的配置。

1、基本原理

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。而Spring Security对Web资源的保护是靠Filter实现的。当初始化Spring Security时,WebSecurityConfiguration会创建一个名为 springSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类。

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,下图为实际调试中创建的FilterChainProxy实例。

这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理。 Spring Security功能的实现主要是由一系列过滤器链相互配合完成,如下图(只是挑选了一些重要的Filter进行讲解):

 下面介绍几个重要的过滤器:

  • UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录验证,该过滤器默认只有当请求方法为post、请求页面为/login时过滤器才生效,如果想修改默认拦截url,只需在刚才介绍的Spring Security配置类WebSecurityConfig中配置该过滤器的拦截url:.loginProcessingUrl("url")即可;
  • BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,当通过HTTP Basic方式登录时,默认会发送post请求/login,并且在请求头携带Authorization:Basic dXNlcjoxOWEyYWIzOC1kMjBiLTQ0MTQtOTNlOC03OThkNjc2ZTZlZDM=信息,该信息是登录用户名、密码加密后的信息,然后由BasicAuthenticationFilter过滤器解析后,构建UsernamePasswordAuthenticationFilter过滤器进行认证;如果请求头没有Authorization信息,BasicAuthenticationFilter过滤器则直接放行;
  • FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常;
  • ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息;

2、认证流程

以表单方式登录验证为例,认证流程如下:

  • 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  • 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
  • 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  • SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。其中web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

3、授权策略

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。授权流程如下:

四、Spring Security filter的构造和初始化

我们已经知道Spring Security通过构造层层filter来实现登录跳转、权限验证,角色管理等功能。这里我们将通过剖析Spring Security的核心源码来说明Spring Security的filter是如何开始构造的,下面的讲解比较长,如果你不是特别感兴趣,可以直接跳到总结,总结粗略了叙述了Spring Security框架filters的构建过程。我们可以下载Spring Security源码;

1、@EnableWebSecurity

我们知道要想启动Spring Security,必须配置注解@EnableWebSecurity,我们就从该注解说起:

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value = { java.lang.annotation.ElementType.TYPE }) @Documented @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { /** * Controls debugging support for Spring Security. Default is false. * @return if true, enables debug support with Spring Security */
    boolean debug() default false; }

2、WebSecurityConfiguration类

我们可以看到该注解导入了WebSecurityConfiguration类,进入该类查看:

/* * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package org.springframework.security.config.annotation.web.configuration; import java.util.List; import java.util.Map; import javax.servlet.Filter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.ImportAware; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.Order; import org.springframework.core.type.AnnotationMetadata; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor; import org.springframework.security.context.DelegatingApplicationListener; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; /** * Uses a {@link WebSecurity} to create the {@link FilterChainProxy} that performs the web * based security for Spring Security. It then exports the necessary beans. Customizations * can be made to {@link WebSecurity} by extending {@link WebSecurityConfigurerAdapter} * and exposing it as a {@link Configuration} or implementing * {@link WebSecurityConfigurer} and exposing it as a {@link Configuration}. This * configuration is imported when using {@link EnableWebSecurity}. * * @see EnableWebSecurity * @see WebSecurity * * @author Rob Winch * @author Keesun Baik * @since 3.2 */ @Configuration(proxyBeanMethods = false) public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { private WebSecurity webSecurity; private Boolean debugEnabled; private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers; private ClassLoader beanClassLoader; @Autowired(required = false) private ObjectPostProcessor<Object> objectObjectPostProcessor; @Bean public static DelegatingApplicationListener delegatingApplicationListener() { return new DelegatingApplicationListener(); } @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() { return webSecurity.getExpressionHandler(); } /** * Creates the Spring Security Filter Chain * @return the {@link Filter} that represents the security filter chain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null
                && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } return webSecurity.build(); } /** * Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP * tag support. * @return the {@link WebInvocationPrivilegeEvaluator} */ @Bean @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public WebInvocationPrivilegeEvaluator privilegeEvaluator() { return webSecurity.getPrivilegeEvaluator(); } /** * Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} * instances used to create the web configuration. * * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a * {@link WebSecurity} instance * @param webSecurityConfigurers the * {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to * create the web configuration * @throws Exception */ @Autowired(required = false) public void setFilterChainProxySecurityConfigurer( ObjectPostProcessor<Object> objectPostProcessor, @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception { webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); } webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must be unique. Order of "
                                + order + " was already used on " + previousConfig + ", so it cannot be used on "
                                + config + " too."); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); } this.webSecurityConfigurers = webSecurityConfigurers; } @Bean public static BeanFactoryPostProcessor conversionServicePostProcessor() { return new RsaKeyConversionServicePostProcessor(); } @Bean public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( ConfigurableListableBeanFactory beanFactory) { return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory); } /** * A custom verision of the Spring provided AnnotationAwareOrderComparator that uses * {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class * instances for the {@link Order} annotation. * * @author Rob Winch * @since 3.2 */
    private static class AnnotationAwareOrderComparator extends OrderComparator { private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); @Override protected int getOrder(Object obj) { return lookupOrder(obj); } private static int lookupOrder(Object obj) { if (obj instanceof Ordered) { return ((Ordered) obj).getOrder(); } if (obj != null) { Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass()); Order order = AnnotationUtils.findAnnotation(clazz, Order.class); if (order != null) { return order.value(); } } return Ordered.LOWEST_PRECEDENCE; } } /* * (non-Javadoc) * * @see org.springframework.context.annotation.ImportAware#setImportMetadata(org. * springframework.core.type.AnnotationMetadata) */
    public void setImportMetadata(AnnotationMetadata importMetadata) { Map<String, Object> enableWebSecurityAttrMap = importMetadata .getAnnotationAttributes(EnableWebSecurity.class.getName()); AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes .fromMap(enableWebSecurityAttrMap); debugEnabled = enableWebSecurityAttrs.getBoolean("debug"); if (webSecurity != null) { webSecurity.debug(debugEnabled); } } /* * (non-Javadoc) * * @see * org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java. * lang.ClassLoader) */
    public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; } }
View Code

如果我们忽略掉细节,只看最重要的步骤,该类主要实现了如下功能:

WebSecurityConfiguration类是作为一个Spring配置源,同时定义了许多bean,这里重点看WebSecurityConfiguration#setFilterChainProxySecurityConfigurer这个方法:

    /** * Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} * instances used to create the web configuration. * * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a * {@link WebSecurity} instance * @param webSecurityConfigurers the * {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to * create the web configuration * @throws Exception */ @Autowired(required = false) public void setFilterChainProxySecurityConfigurer( ObjectPostProcessor<Object> objectPostProcessor, @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception { webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); } webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must be unique. Order of "
                                + order + " was already used on " + previousConfig + ", so it cannot be used on "
                                + config + " too."); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); } this.webSecurityConfigurers = webSecurityConfigurers; }

下面我们对每一个步骤来做相应的源代码解释,首先我们来看一下方法得第二个参数:

@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}"

可以看一下autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的源代码:

public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() { List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>(); Map<String, WebSecurityConfigurer> beansOfType = beanFactory .getBeansOfType(WebSecurityConfigurer.class); for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) { webSecurityConfigurers.add(entry.getValue()); } return webSecurityConfigurers; }

我们如果调试代码,可以发现beanFactory.getBeansOfType从Spring容器获取类型为WebSecurityConfigurer的bean,在这里也就是获取到我们编写的WebSecurityConfig配置类:

我们可以看一下WebSecurityConfig类的类图:

然后直接用new 来创建一个WebSecurity的对象,用来初始化了WebSecurityConfiguration#webSecurity属性;
        webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); }

当有多个配置项时进行排序,进行order重复验证:

webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must be unique. Order of "
                                + order + " was already used on " + previousConfig + ", so it cannot be used on "
                                + config + " too."); } previousOrder = order; previousConfig = config; }

遍历WebSecurityConfiguration#webSecurityConfigurers集合,调用webSecurity的apply方法,此时也会将我们自定义的WebSecurityConfig应用到 webSecurity.apply方法上:

for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) { webSecurity.apply(webSecurityConfigurer); }

 最后,初始化WebSecurityConfiguration#webSecurityConfigurers属性:

    this.webSecurityConfigurers = webSecurityConfigurers;

3、WebSecurity类

到这里我们知道了WebSecurityConfiguration类调用上述方法将我们配置的WebSecurityConfig类用WebSecurity类的apply方法关联起来,那么我们详细看看WebSecurity类的apply方法:

    public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception { add(configurer); return configurer; }
    private <C extends SecurityConfigurer<O, B>> void add(C configurer) { Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer .getClass(); synchronized (configurers) { if (buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers .get(clazz) : null; if (configs == null) { configs = new ArrayList<>(1); } configs.add(configurer); this.configurers.put(clazz, configs); if (buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } }

从上述代码可知,实际上就是将我们定义的WebSecurityConfig配置类放入了WebSecurity类的一个LinkedHashMap中:

该LinkedHashMap在WebSecurity中属性名为configurers:

private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();

可以看到键就是我们定义的配置类的Clazz类型,而值是List<SecurityConfigurer<Filter, WebSecurity>>类型,是一个list集合,其中只有一个元素,就是我们编写的WebSecurityConfig配置类;

我们继续回到WebSecurityConfiguration类,查看它的另外一个重要的方法:

build的方法来自于WebSecurity的父类AbstractSecurityBuilder,该方法即为Spring Security构建Filter的核心方法,通过webSecurity的build方法构建了Spring Security的Filter:

实际上调用了父类AbstractConfiguredSecurityBuilder的doBuild:

    protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result; } }

这里主要看AbstractConfiguredSecurityBuilder的init方法和WebSecurity实现的performBuild方法,首先看init方法,init方法将会遍历WebSecurity的LinkHashMap configurers中每个元素configurer,执行以下步骤:

调用configurer的init方法,init该方法来自父类WebSecurityConfigurerAdapter(这里的this指定就是WebSecurity):

 

init方法中又会调用WebSecurityConfigurerAdapter#getHttp方法:

getHttp方法首先创建一个HttpSecurity对象,用来初始化configurer的http成员:

   private HttpSecurity http;

又调用 configure 方法,最终将会执行我们在WebSecurityConfig写的configure方法:

configurer中的init方法执行完之后,WebSecurity调用addSecurityFilterChainBuilder方法将configurer创建的HttpSecurity放入了WebSecurity的一个list集合里,该list集合属性名为securityFilterChainBuilders:

public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) { this.securityFilterChainBuilders.add(securityFilterChainBuilder); return this; }

到目前为止,我们终于知道我们编写的WebSecurityConfig类的configure方法是如何被调用的了,但是仍有许多疑问没解开,比如HttpSecurity类的作用,Spring Security是如何通过HttpSecurity类构建一条拦截器链等。

这里我们先不分析HttpSecurity类的具体实现,再来看看WebSecurity的init方法执行后所执行的performBuild方法,该方法源码如下:

@Override protected Filter performBuild() throws Exception { Assert.state( !securityFilterChainBuilders.isEmpty(), () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                        + "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
                        + "More advanced users can invoke "
                        + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); List<SecurityFilterChain> securityFilterChains = new ArrayList<>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; if (debugEnabled) { logger.warn("\n\n"
                    + "********************************************************************\n"
                    + "**********        Security debugging is enabled.       *************\n"
                    + "**********    This may include sensitive information.  *************\n"
                    + "**********      Do not use in a production system!     *************\n"
                    + "********************************************************************\n\n"); result = new DebugFilter(filterChainProxy); } postBuildAction.run(); return result; }

该方法执行的操作主要如下:

(1)、遍历WebSecurity的securityFilterChainBuilders列表,一般也就一个元素,也就是我们的WebSecurityConfig配置类创建的HttpSecurity对象,并执行该对象的build方法,初始化成员属性filters,并通过filters集合构建SecurityFilterChain类;

然后将每个HttpSecurity对象构建的SecurityFilterChain对象添加到securityFilterChains列表中。

(2)、将List<SecurityFilterChain>集合构建成一个FilterChainProxy代理类,返回这个FilterChainProxy代理类;

到这里总的过程就非常明了,WebSecurityConfiguration的springSecurityFilterChain方法最终返回了一个FilterChainProxy代理类,作为Spring Security的顶层filter,而HttpSecurity主要用于注册和实例化各种filter,HttpSecurity类有个属性名为filters的List列表专门用于保存过滤器的。

到这里就分成了两路:

  • 一路是HttpSecurity的build方法构建SecurityFilterChain类的原理;
  • 一路是FilterChainProxy类的作用;
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

我们先从FilterChainProxy类开始。

4、FilterChainProxy类

当请求到达的时候,FilterChainProxy会调用dofilter方法,会遍历所有的SecurityFilterChain,对匹配到的url,则调用SecurityFilterChain中的每一个filter做认证授权。FilterChainProxy的dofilter()中调用了doFilterInternal方法,如下:

    private List<SecurityFilterChain> filterChains; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clearContext = request.getAttribute(FILTER_APPLIED) == null; if (clearContext) { try { request.setAttribute(FILTER_APPLIED, Boolean.TRUE); doFilterInternal(request, response, chain); } finally { SecurityContextHolder.clearContext(); request.removeAttribute(FILTER_APPLIED); } } else { doFilterInternal(request, response, chain); } } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); }

这里我就不贴VirtualFilterChain的源码了,实际上就是先去遍历执行我们filters中的过滤器的doFilter,最后再去执行chiin.doFilter。

我们理清了FilterChainProxy类的作用,那么这些SecurityFilterChain是从哪来的呢?从上节可知SecurityFilterChain是由HttpSecurity的build方法生成的,下面我们分析下HttpSecurity类。

5、HttpSecurity类

HttpSecurity与WebSecurity一样,都继承了AbstractConfiguredSecurityBuilder类,而WebSecurity的build和doBuild方法和LinkedHashMap属性,均来自AbstractConfiguredSecurityBuilder,故HttpSecurity的build方法代码与WebSecurity的相同,区别在于LinkedHashMap存储的东西不同:

(1)、WebSecurityConfig类的configure方法

在之前我们已经介绍了WebSecurity的doBuild是如何调用我们自己写的配置类WebSecurityConfig的configure方法;在该方法中http所调用的方法,最终的结果就是产生url-filter的关系映射:

  protected void configure(HttpSecurity http) throws Exception { http.formLogin() //HTTP Basic认证方式
 .and() .authorizeRequests() // 授权配置
                .anyRequest()  // 所有请求
                .authenticated(); // 都需要认证
 }

比如authorizeRequests(),formLogin()方法分别返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,他们都是SecurityConfigurer接口的实现类,分别代表的是不同类型的安全配置器。而这些安全配置器分别对应一个或多个filter:

  • formLogin对应UsernamePasswordAuthenticationFilter,此外我们还可以给安全过滤器FormLoginConfigurer指定其它参数,比如.login("/login"):自定义登录请求页面;.loginProcessingUrl("/login")指定UsernamePasswordAuthenticationFilter拦截的Form action;
.and() .formLogin() // 或者httpBasic()
                .loginPage("/login")  // 指定登录页的路径
                .loginProcessingUrl("/login")  // 指定自定义form表单提交请求的路径
  • authorizeRequests对应FilterSecurityInterceptor;
    public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { ApplicationContext context = getContext(); return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context)) .getRegistry(); }
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }

都调用了getOrApply方法,再来看getOrApply方法,又调用了其中的apply方法:

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

apply方法又调用了add方法,这里的add方法最终还是将该configurer加入了LinkedHashMap中:

    private <C extends SecurityConfigurer<O, B>> void add(C configurer) { Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer .getClass(); synchronized (configurers) { if (buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers .get(clazz) : null; if (configs == null) { configs = new ArrayList<>(1); } configs.add(configurer); this.configurers.put(clazz, configs); if (buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } }

故HttpSecurity在构建filter的过程中,本质还是将形如ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer等类加入了它的LinkedHashMap中,该list集合属性名为configurers :

private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();

这里有一点需要注意,

  • 在HttpSecurity的configurers列表中存放的元素都是继承自SecurityConfigurerAdapter类,Spring Security框架提供的不同类型的安全配置器主要有以下这些;

  • 而在WebSecurity的configurers列表中存放的元素都是继承自WebSecurityConfigurerAdapter类,也就是我们写的配置类:

啥时候我们会使用到SecurityConfigurerAdapter类,在后面我们介绍手机验证码登录的时候会有一个案例:

如果想使得这个配置生效,我们只需要在WebSecurityConfig配置类的configure方法添加如下代码:

 .apply(smsAuthenticationConfig); // 将短信验证码认证配置加到 Spring Security 中 添加一个安全配置其到http的configurers集合

调用http的apply方法,最终将该smsAuthenticationConfig加入了HttpSecurity的configurers列表中。

(2)、HttpSecurity的build方法构建SecurityFilterChain类的原理

那么将这些Configurer类存入LinkedHashMap的作用又是什么?

在前面我们已经说到通过调用HttpSecurity的build方法构建SecurityFilterChain类,而build方法封装了doBuild方法;

        for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); }

在我们回忆上面WebSecurity类的doBuild方法,我们知道HttpSecurity类调用的doBuild方法与WebSecurity类一样,而通过观察WebSecurity类doBuild方法里init;configure;这些语句的具体实现,实际就是遍历LinkedHashMap中的元素:

并调用其init方法和configure方法:

 @Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result; } }

我们现在来查看其中一个ExpressionUrlAuthorizationConfigurer类的configure方法的详细代码:

 @Override public void configure(H http) throws Exception { FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http); if (metadataSource == null) { return; } FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor( http, metadataSource, http.getSharedObject(AuthenticationManager.class)); if (filterSecurityInterceptorOncePerRequest != null) { securityInterceptor .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest); } securityInterceptor = postProcess(securityInterceptor); http.addFilter(securityInterceptor); http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); }

最后来看看HttpSecruity的performBuild()方法:

 @Override protected DefaultSecurityFilterChain performBuild() { filters.sort(comparator); return new DefaultSecurityFilterChain(requestMatcher, filters); }

实际上就是通过HttpSecurity的filters集合构建了SecurityFilterChain。

从上面代码可总结出,HttpSecurity内部维护一个filter列表,而HttpSecurity调用形如authorizeRequests(),formLogin()等方法实际上就是将各种filter添加入它的列表当中,最后通过performBuild()方法构建出SecurityFilterChain,至此HttpSecurity构建filter的总过程就完成了。

6、核心接口SecurityBuilder 与 SecurityConfigurer

上面我们提到的WebSecurity,HttpSecurity都是具体的类。现在,我们从更高的层面来说,从两个核心接口以及其实现类的类图来理解下。

 
public interface SecurityBuilder<O> { /** * Builds the object and returns it or null. * * @return the Object to be built or null if the implementation allows it. * @throws Exception if an error occurred when building the Object */ O build() throws Exception; }
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> { /** * Initialize the {@link SecurityBuilder}. Here only shared state should be created * and modified, but not properties on the {@link SecurityBuilder} used for building * the object. This ensures that the {@link #configure(SecurityBuilder)} method uses * the correct shared objects when building. Configurers should be applied here. * * @param builder * @throws Exception */
    void init(B builder) throws Exception; /** * Configure the {@link SecurityBuilder} by setting the necessary properties on the * {@link SecurityBuilder}. * * @param builder * @throws Exception */
    void configure(B builder) throws Exception; }
  • HttpSecurity是接口SecurityBuilder的实现类,HttpSecuirty内部维护了一个Filter的List集合,我们添加的各种安全配置器对应的Filter最终都会被加入到这个List集合中;
  • WebSecurity也是接口securityBuilder的实现类,内部维护着SecurityBuilder的列表,存储SecurityBuilder,这里主要是存储HttpSecurity;

很多官方类是XXXConfigurer,这些都是SecurityConfigurer。这些SecurityConfigurer的configure()方法,都会把对应filter添加到HttpSecurity。

7、总结

(1) WebSecurityConfiguration配置类有两个重要成员属性,调用setFilterChainProxySecurityConfigurer方法初始化webSecurity、使用我们编写的配置类WebSecurityConfig初始化webSecurityConfigurers。

private WebSecurity webSecurity; private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

需要注意的是:webSecurityConfigurers的程序都是继承自WebSecurityConfigurerAdapter类。

(2)、WebSecurity 有两三个重要成员,然后使用WebSecurityConfiguration#webSecurityConfigurers初始化webSecurity#configurers成员;

private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>(); private final List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<>();

(3) 调用WebSecurityConfiguration#springSecurityFilterChain生成FilterChainProxy,主要是通过webSecurity.build()方法实现构建,该方法主要包含两步:

a、执行父类AbstractConfiguredSecurityBuilder#init方法:

  • 遍历webSecurity#configurers成员,执行configurer的init方法;
  • init方法调用getHttp方法,去初始化configurer的http成员(HttpSecurity类型),与此同时执行我们configure(http)方法,就是去调用我们配置类WebSecurityConfig中的configure方法;、
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //HTTP Basic认证方式
 .and() .authorizeRequests() // 授权配置
                .anyRequest()  // 所有请求
                .authenticated(); // 都需要认证
 } }
  • 比如authorizeRequests(),formLogin()方法分别返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,最终会将不同类型的安全配置器添加到http的configurers,这些configure都是继承自SecurityConfigurerAdapter类。HttpSecurity也是有两个非常重要的成员:
    private List<Filter> filters = new ArrayList<>(); private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<>();
  • 然后调用webSecurity#addSecurityFilterChainBuilder方法将webSecurity#configurers中每个configurer创建的HttpSecurity放入了webSecurity的securityFilterChainBuilders集合里;

b、执行webSecurity#performBuild 

  • 遍历webSecurity的securityFilterChainBuilders列表,一般也就一个元素,也就是我们的WebSecurityConfig配置类创建的http对象,并执行http对象的build方法,生成SecurityFilterChain对象,该方法主要包含两步:
  • 执行父类AbstractConfiguredSecurityBuilder#init方法:
  • 遍历http#configurers成员,执行configurer的init方法,只是在init过程中不会创建http对象,而是把http对象传进去,以ExpressionUrlAuthorizationConfigurer为例,它会向http的filters属性添加securityInterceptor;
@Override
    public void configure(H http) throws Exception {
        FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
        if (metadataSource == null) {
            return;
        }
        FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
                http, metadataSource, http.getSharedObject(AuthenticationManager.class));
        if (filterSecurityInterceptorOncePerRequest != null) {
            securityInterceptor
                    .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
        }
        securityInterceptor = postProcess(securityInterceptor);
        http.addFilter(securityInterceptor);
        http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
    }
  • 执行http#performBuild方法,实际上就是通过HttpSecurity的filters集合构建了SecurityFilterChain;
  • 然后将每个http对象构建的SecurityFilterChain对象添加到List<SecurityFilterChain> securityFilterChains临时列表中:
  • 将securityFilterChains集合构建成一个FilterChainProxy代理类,返回这个FilterChainProxy代理类;

(4) 当请求到达的时候,FilterChainProxy会调用dofilter方法,会遍历所有的SecurityFilterChain,对匹配到的url,则调用SecurityFilterChain中的每一个filter做认证授权。

最后放一张概括图,有兴趣的朋友可以绘制出具体的时序图,这里我就不绘制了:

五、登录、验证流程分析

我们已经明白了Spring Security的filter的构造。下面我们来介绍一下filter的执行顺序(登录方式改回表单的方式)。

当我们启动项目访问http://localhost:8080/hello时:

1、请求/hello接口

由于BasicAuthenticationFilter、UsernamePasswordAuthenticationFilter默认只拦截/login post请求,因此过滤器会直接放行,代码直接跳转到FilterSecurityInteceptor上进行权限校验;

2、权限验证

此时会调用父类的beforeInvocation进行权限校验:

protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object "
                            + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                            + getSecureObjectClass()); } Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { if (rejectPublicInvocations) { throw new IllegalArgumentException( "Secure object invocation "
                                + object + " was denied as public invocations are not allowed via this interceptor. "
                                + "This indicates a configuration error because the "
                                + "rejectPublicInvocations property is set to 'true'"); } if (debug) { logger.debug("Public object - authentication not attempted"); } publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation
 } if (debug) { logger.debug("Secure object: " + object + "; Attributes: " + attributes); } if (SecurityContextHolder.getContext().getAuthentication() == null) { credentialsNotFound(messages.getMessage( "AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes); } Authentication authenticated = authenticateIfRequired(); // Attempt authorization
        try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object); } }

 this.obtainSecurityMetadataSource()会调用默认的权限资源管理器,由于我们配置了任何请求都需要经过授权:

 .anyRequest()  // 所有请求
            .authenticated(); // 都需要认证

因此/hello需要的权限为authenticated,此处我们可以通过实现FilterInvocationSecurityMetadataSource接口配置自己权限资源管理器,通过查询数据库来实现权限的动态配置,感兴趣的可以阅读:SpringSecurity动态配置权限

 

由于用户没有登录,经过AnonymousAuthenticationFilter匿名过滤器处理之后,我们从上下文中可以获取到用户的主体信息为:

 

 此时匿名用户具有的权限为ROLE_ANONYMOUS;然后经过访问决策管理器判断用户有无权限:

 this.accessDecisionManager.decide(authenticated, object, attributes);

Spring提供了3个决策管理器:

  • AffirmativeBased 一票通过,只要有一个投票器通过就允许访问;
  • ConsensusBased 有一半以上投票器通过才允许访问资源;
  • UnanimousBased 所有投票器都通过才允许访问;

 这里使用默认的决策管理进行判断有无权限,可以看到决策管理中在引入了投票器(AccessDecisionVoter)的概念,有无权限访问的最终觉得权是由投票器来决定的,这里权限无法通过:

 会抛出权限拒绝的异常,该异常会被ExceptionTranslateFilter捕获;

3、跳转到登陆页面

由于用户未登录直接访问/hello,所以抛出用户未认证的异常,所以接下来跳转到Spring Security提供的默认登录页 GET:http://localhost:8080/login,最终请求被DefaultLoginPageGeneratingFilter拦截进行处理,返回默认的登录页面:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //登录失败
        boolean loginError = isErrorPage(request); //登出请求
        boolean logoutSuccess = isLogoutSuccess(request); //是否是登录请求
        if (isLoginUrlRequest(request) || loginError || logoutSuccess) { String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.length()); response.getWriter().write(loginPageHtml); return; } chain.doFilter(request, response); }

isLoginUrlRequest(request)方法中,如果判断是/login请求则接下来生成默认的登录页面返回:

private boolean isLoginUrlRequest(HttpServletRequest request) { return matches(request, loginPageUrl); } private boolean matches(HttpServletRequest request, String url) { //首先判断是不是GET请求
        if (!"GET".equals(request.getMethod()) || url == null) { return false; } String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(';'); if (pathParamIndex > 0) { // strip everything after the first semi-colon
            uri = uri.substring(0, pathParamIndex); } if (request.getQueryString() != null) { uri += "?" + request.getQueryString(); } if ("".equals(request.getContextPath())) { return uri.equals(url); } return uri.equals(request.getContextPath() + url); }

generateLoginPageHtml方法中生成默认登录页面:

private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "none"; if (loginError) { HttpSession session = request.getSession(false); if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = ex != null ? ex.getMessage() : "none"; } } StringBuilder sb = new StringBuilder(); sb.append("<html><head><title>Login Page</title></head>"); if (formLoginEnabled) { sb.append("<body οnlοad='document.f.").append(usernameParameter) .append(".focus();'>\n"); } if (loginError) { sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: "); sb.append(errorMsg); sb.append("</font></p>"); } if (logoutSuccess) { sb.append("<p><font color='green'>You have been logged out</font></p>"); } if (formLoginEnabled) { sb.append("<h3>Login with Username and Password</h3>"); sb.append("<form name='f' action='").append(request.getContextPath()) .append(authenticationUrl).append("' method='POST'>\n"); sb.append("<table>\n"); sb.append("    <tr><td>User:</td><td><input type='text' name='"); sb.append(usernameParameter).append("' value='").append("'></td></tr>\n"); sb.append("    <tr><td>Password:</td><td><input type='password' name='") .append(passwordParameter).append("'/></td></tr>\n"); if (rememberMeParameter != null) { sb.append("    <tr><td><input type='checkbox' name='") .append(rememberMeParameter) .append("'/></td><td>Remember me on this computer.</td></tr>\n"); } sb.append("    <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n"); renderHiddenInputs(sb, request); sb.append("</table>\n"); sb.append("</form>"); } if (openIdEnabled) { sb.append("<h3>Login with OpenID Identity</h3>"); sb.append("<form name='oidf' action='").append(request.getContextPath()) .append(openIDauthenticationUrl).append("' method='POST'>\n"); sb.append("<table>\n"); sb.append("    <tr><td>Identity:</td><td><input type='text' size='30' name='"); sb.append(openIDusernameParameter).append("'/></td></tr>\n"); if (openIDrememberMeParameter != null) { sb.append("    <tr><td><input type='checkbox' name='") .append(openIDrememberMeParameter) .append("'></td><td>Remember me on this computer.</td></tr>\n"); } sb.append("    <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n"); sb.append("</table>\n"); renderHiddenInputs(sb, request); sb.append("</form>"); } sb.append("</body></html>"); return sb.toString(); }

4、开始登录

当我们输入完用户名、密码点击登录时,将会发送post请求:http://localhost:8080/login,该请求页面是在form标签action中指定的,POST请求会提交用户名和密码登录信息,此时由UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter处理,简单来说就是从请求request中获取用户名和密码进行认证操作(这里有一点需要注意,默认情况下UsernamePasswordAuthenticationFilter该过滤器默认只有当请求方法为post、请求页面为/login时过滤器才生效,如果想修改其默认拦截页面,需要在BrowserSecurityConfig中配置该过滤器的拦截url:.loginProcessingUrl("url"),也就是说只有当form标签action指定的url和.loginProcessingUrl配置的相同时,UsernamePasswordAuthenticationFilter过滤器才生效):

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //当不是登录请求 POST:/login时则直接跳过
        if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } //如果是登录请求 POST:/login则进行验证处理
        if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { //从请求中获取用户名密码进行校验
            authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication
                return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); //校验失败则跳转到登录页面
 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed //校验失败则跳转到登录页面
 unsuccessfulAuthentication(request, response, failed); return; } // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //校验成功则跳转到成功之后页面
 successfulAuthentication(request, response, chain, authResult); }

在doFilter方法中,首先会调用requiresAuthentication方法判断是不是登录请求,如果不是则直接跳过这个Filter,如果是则进行身份验证:

 如果请求是登录操作,则接下来进行身份验证相关的操作,

UsernamePasswordAuthenticationFilter处理表单方式的用户认证。在UsernamePasswordAuthenticationFilter的attemptAuthentication方法上打个断点: 调用authResult = attemptAuthentication(request, response)方法进行验证,在attemptAuthentication中会从请求中获取用户名和密码构建UsernamePasswordAuthenticationToken :

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //获取用户名密码
        String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property
 setDetails(request, authRequest); //调用AuthenticationManager的实现类进行校验
        return this.getAuthenticationManager().authenticate(authRequest); }

5、认证管理器(AuthenticationManager)进行认证

在接口AuthenticationManager的实现类ProviderManager调用authenticate方法进行校验操作。在authenticate方法中提供了一个List<AuthenticationProvider>,开发者可以提供不同的校验方式(用户名密码、手机号密码、邮箱密码等)只要其中有一个AutenticationProvider调用authenticate方法校验通过即可,当校验不通过时会抛出AuthenticationException ,当所有的AuthenticationProvider校验不通过时,直接抛出异常由ExceptionTranslationFilter捕捉处理,跳转到登录页面。

public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); //可以提供多个验证器,只要其中有一个校验通过接口
        for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using "
                        + provider.getClass().getName()); } try { //进行校验,校验不通过则直接抛出AuthenticationException 
                result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status
                throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try.
            try { result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request
 } catch (AuthenticationException e) { lastException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication
 ((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception). //最终校验不通过则抛出异常,由ExceptionTranslationFilter捕捉处理
        if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }

在接口AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider中调用authenticate进行用户名密码等的校验

  • 首先从缓存userCahce中获取,如果获取不到,则从子类DaoAuthenticationProvider的retirveUser方法中获取,该方法利用loadUserByUsername函数根据用户名从数据库获得用户详情,如果获取不到则直接抛出异常;
  • 将获取的user和authentication(从浏览器输入的用户和密码,用户名和密码会传递到Spring Security内部,最终封装成UsernamePasswordAuthenticationToken对象authentication)进行匹配,匹配成功调用createSuccessAuthentication方法创建Authentication返回。;
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username
        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 = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { preAuthenticationChecks.check(user); //判断用户名和密码等信息是否完全匹配
 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }

在子类DaoAuthenticationProvider中调用接口的UserDetailsService的loadUserByUsername方法根据用户名来查找用户信息(在正式项目中实现此接口来完成从数据库等中查找),如果查找不到还是直接抛出异常,由上层去处理:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //调用默认实现InMemoryUserDetailsManager查找用户名密码
            loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null); } throw notFound; } catch (Exception repositoryProblem) { throw new InternalAuthenticationServiceException( repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }

当找到用户信息后,还需要根据用户信息和password字段进行匹配,在additionalAuthenticationChecks中完成完整的用户名和密码认证:

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

6、权限校验

当认证通过时,进行权限校验,执行FilterSecurityInterceptor代码的doFilter方法:

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

在invoke方法中会调用父类AbstractSecurityInterceptor的beforeInvocation对Authentication对象进行权限校验:

public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking
 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking
            if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //调用父类的beforeInvocation进行权限校验
            InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }

在beforeInvocation中调用接口AccessDecisionManager的实现类,校验角色:

protected InterceptorStatusToken beforeInvocation(Object object) { //省略部分代码 //获取当前线程的Authentication对象
        Authentication authenticated = authenticateIfRequired(); // Attempt authorization
        try { //权限角色校验
            this.accessDecisionManager.decide(authenticated, object, attributes); } //校验失败抛出异常
        catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } //省略部分代码
    }

最终调用子类AffirmativeBased的decide方法,在decide方法中会获取AccessDecisionVoter对权限进行投票处理,获取投票结果,当投票结果是1时则表示有权限,否则等于-1表示没有权限,拒绝:

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

在实现类WebExpressionVoter会根据authentication结果进行校验判断,根据Authentication对象创建接口SecurityExpressionOperations的实现类SecurityExpressionRoot:

  • 当请求url被配置为permitAll,则直接返回true,校验通过;
  • 其他需要登录请求url会调用isAuthenticated方法,最终调用AuthenticationTrustResolver的AuthenticationTrustResolverImpl的方法进行判断;
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { protected final Authentication authentication; private AuthenticationTrustResolver trustResolver; private RoleHierarchy roleHierarchy; private Set<String> roles; private String defaultRolePrefix = "ROLE_"; /** Allows "permitAll" expression */
    public final boolean permitAll = true; /** Allows "denyAll" expression */
    public final boolean denyAll = false; private PermissionEvaluator permissionEvaluator; public final String read = "read"; public final String write = "write"; public final String create = "create"; public final String delete = "delete"; public final String admin = "administration"; /** * Creates a new instance * @param authentication the {@link Authentication} to use. Cannot be null. */
    public SecurityExpressionRoot(Authentication authentication) { if (authentication == null) { throw new IllegalArgumentException("Authentication object cannot be null"); } this.authentication = authentication; } public final boolean hasAuthority(String authority) { return hasAnyAuthority(authority); } public final boolean hasAnyAuthority(String... authorities) { return hasAnyAuthorityName(null, authorities); } public final boolean hasRole(String role) { return hasAnyRole(role); } public final boolean hasAnyRole(String... roles) { return hasAnyAuthorityName(defaultRolePrefix, roles); } private boolean hasAnyAuthorityName(String prefix, String... roles) { Set<String> roleSet = getAuthoritySet(); for (String role : roles) { String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; } public final Authentication getAuthentication() { return authentication; } public final boolean permitAll() { return true; } public final boolean denyAll() { return false; } public final boolean isAnonymous() { return trustResolver.isAnonymous(authentication); } public final boolean isAuthenticated() { return !isAnonymous(); } public final boolean isRememberMe() { return trustResolver.isRememberMe(authentication); } public final boolean isFullyAuthenticated() { return !trustResolver.isAnonymous(authentication) && !trustResolver.isRememberMe(authentication); } /** * Convenience method to access {@link Authentication#getPrincipal()} from * {@link #getAuthentication()} * @return
     */
    public Object getPrincipal() { return authentication.getPrincipal(); } public void setTrustResolver(AuthenticationTrustResolver trustResolver) { this.trustResolver = trustResolver; } public void setRoleHierarchy(RoleHierarchy roleHierarchy) { this.roleHierarchy = roleHierarchy; } /** * <p> * Sets the default prefix to be added to {@link #hasAnyRole(String...)} or * {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN") * is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is * "ROLE_" (default). * </p> * * <p> * If null or empty, then no default role prefix is used. * </p> * * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_". */
    public void setDefaultRolePrefix(String defaultRolePrefix) { this.defaultRolePrefix = defaultRolePrefix; } private Set<String> getAuthoritySet() { if (roles == null) { roles = new HashSet<String>(); Collection<? extends GrantedAuthority> userAuthorities = authentication .getAuthorities(); if (roleHierarchy != null) { userAuthorities = roleHierarchy .getReachableGrantedAuthorities(userAuthorities); } roles = AuthorityUtils.authorityListToSet(userAuthorities); } return roles; } public boolean hasPermission(Object target, Object permission) { return permissionEvaluator.hasPermission(authentication, target, permission); } public boolean hasPermission(Object targetId, String targetType, Object permission) { return permissionEvaluator.hasPermission(authentication, (Serializable) targetId, targetType, permission); } public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) { this.permissionEvaluator = permissionEvaluator; } /** * Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role * does not already start with defaultRolePrefix. * * @param defaultRolePrefix * @param role * @return
     */
    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) { if (role == null) { return role; } if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) { return role; } if (role.startsWith(defaultRolePrefix)) { return role; } return defaultRolePrefix + role; } }

在AuthenticationTrustResolverImpl方法isAnonymous中就是判断传递过来的Authentication对象是不是AnonymousAuthenticationToken,如果是AnonymousAuthenticationToken则表示没有登录,因为登录之后生成的对象是UsernamePasswordAuthenticationToken或其他Authentication对象,这也是Spring Security设计的最精华也是最难以理解也是最简单的方式。

private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class; public boolean isAnonymous(Authentication authentication) { if ((anonymousClass == null) || (authentication == null)) { return false; } return anonymousClass.isAssignableFrom(authentication.getClass()); }

总结:Spring Security对url的权限判断有两种方式,一种是请求是permitAll的则直接返回校验通过,另外一个是判断Authentication是不是AnonymousAuthenticationToken,因为正常登录等产生的不是这个对象,如果不是这个类型的对象则表示登录成功了。

7、权限验证通过

如果权限通过,代码最终跳转到/hello上:

浏览器页面将显示hello spring security信息;

六、代码下载

参考文章:

[1] Spring Boot中开启Spring Security(部分转载)

[2] Spring Security原理学习--用户名和密码认证(三)

[3] Spring Security原理学习--权限校验(四)

[4] Spring Security Config : 注解 EnableWebSecurity 启用Web安全

[5] Spring Security实现原理剖析(一):filter的构造和初始化

[6] Spring Security 实现原理的理解记录(推荐)

[7]springSecurity深度解析第二版

[8]SpringBoot+SpringSecurity+jwt整合及初体验


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM