打开Spring Security的官网,从其首页的预览上就能够看见以下文字:java
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.git
这段文字的大体意思是:web
身份验证和访问控制是应用安全的两个重要方面,也经常被称为“认证”和“受权”。spring
上面的两点是应用安全的基本关注点,Spring Security存在的意义就是帮助开发者更加便捷地实现了应用的认证和受权能力。segmentfault
Spring Security的前身是Acegi Security,后来成为了Spring在安全领域的顶级项目,并正式改名到Spring名下,成为Spring全家桶中的一员,因此Spring Security很容易地集成到基于Spring的应用中来。Spring Security在用户认证方面支持众多主流认证标准,包括但不限于HTTP基本认证、HTTP表单验证、HTTP摘要认证、OpenID和LDAP等,在用户受权方面,Spring Security不只支持最经常使用的基于URL的Web请求受权,还支持基于角色的访问控制(Role-Based Access Control,RBAC)以及访问控制列表(Access Control List,ACL)等。后端
学习Spring Security不只仅是要学会如何使用,也要经过其设计精良的源码来进行深刻地学习,学习它在认证与受权方面的设计思想,由于这些思想是能够脱离具体语言,应用到其余应用中。数组
本篇文章是连载系列文章:《Spring Security入门到实践》的一个入门文章,后面将围绕Spring Security进行深刻源码解读,作到不只会用,也知其因此然。浏览器
咱们使用IntelliJ IDEA的Spring Initializr工具建立一个Spring Boot项目,在其pom文件中加入以下的经常使用依赖:安全
<dependencies> <!-- Spring Security的核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Boot Web的核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
依赖添加完毕以后,再声明一个index路由,返回一段文字:“Welcome to learn Spring Security!”,具体代码以下所示:微信
@Slf4j @Controller @RequestMapping("/demo") public class DemoController { @GetMapping @ResponseBody public String index() { return "Welcome to learn Spring Security!"; } }
此时就能够启动LearningSpringSecurityMainApplication的main方法,咱们的简单应用就在8080端口启动起来了,咱们在浏览器里访问http://localhost:8008/demo
接口,按照原来的思路,那么浏览器将接收到来自后端程序的问候:“Welcome to learn Spring Security!”,可是实际运行中,咱们发现,咱们访问的接口被拦截了,要求咱们登陆后才能继续访问/demo路由,以下图所示:
这是由于Spring Boot项目引入了Spring Security之后,自动装配了Spring Security的环境,Spring Security的默认配置是要求通过了HTTP Basic认证成功后才能够访问到URL对应的资源,且默认的用户名是user,密码则是一串UUID字符串,输出到了控制台日志里,以下图所示:
咱们在登陆窗口输入用户名和密码后,就正确返回了“Welcome to learn Spring Security!”
很明显,自动生成随机密码的方式并非最经常使用的方法,可是在学习阶段,对于这种简单的认证方式,也是须要进行研究的,对于HTTP Basic认证,咱们能够在resources中的application.properties中进行配置用户名和密码:
# 配置用户名和密码 spring.security.user.name=user spring.security.user.password=1234
配置了用户名和密码后,那么再次启动应用,咱们发如今控制台中就没有再生成新的随机密码了,使用咱们配置用户名和密码就能够登陆并正确访问到/demo路由了。
事实上,这种简易的认证方式并不能知足企业级权限系统的要求,咱们须要根据企业的实际状况开发出复杂的权限系统。虽然这种简易方式并不经常使用,可是咱们也是须要了解其运行机制和原理,接下来,咱们一块儿深刻了解这种基本方式运行原理。
HTTP Basic认证是一种较为简单的HTTP认证方式,客户端经过将用户名和密码按照必定规则(用户名:密码)进行Base64编码进行“加密”(可反向解密,等同于明文),将加密后的字符串添加到请求头发送到服务端进行认证的方式。可想而知,HTTP Basic是个不安全的认证方式,一般须要配合HTTPS来保证信息的传输安全。基本的时序图以下所示:
咱们经过Postman来测试HTTP Basic的认证过程:
返回的结果显示该路由的访问前提条件是必须通过认证,没有通过认证是访问不到结果的,且咱们观察返回头中包含了WWW-Authenticate: Basic realm="Realm"
,若是在浏览器中,当浏览器检测到返回头中包含这个属性,那么会弹出一个要求输入用户名和密码的对话框。返回头的具体信息以下图所示:
HTTP Basic的认证方式在企业级开发中不多使用,但也常见于一些中间件中,好比ActiveMQ的管理页面,Tomcat的管理页面等,都采用的HTTP Basic认证。
Spring Security在没有通过任何配置的状况下,默认也支持了HTTP Basic认证,整个Spring Security的基本原理就是一个拦截器链,以下图所示:
其中绿色部分的每一种过滤器表明着一种认证方式,主要工做检查当前请求有没有关于用户信息,若是当前的没有,就会跳入到下一个绿色的过滤器中,请求成功会打标记。绿色认证方式能够配置,好比短信认证,微信。好比若是咱们不配置BasicAuthenticationFilter的话,那么它就不会生效。
FilterSecurityInterceptor过滤器是最后一个,它会决定当前的请求可不能够访问Controller,判断规则放在这个里面。当不经过时会把异常抛给在这个过滤器的前面的ExceptionTranslationFilter过滤器。
ExceptionTranslationFilter接收到异常信息时,将跳转页面引导用户进行认证。橘黄色和蓝色的位置不可更改。当没有认证的request进入过滤器链时,首先进入到FilterSecurityInterceptor,判断当前是否进行了认证,若是没有认证则进入到ExceptionTranslationFilter,进行抛出异常,而后跳转到认证页面(登陆界面)。
上面的简单原理分析中提到,每个过滤器都是通过配置后才会真正地生效,那么默认的相关配置在哪里呢?在Spring Security的官方文档中提到了WebSecurityConfigurerAdapter类,HTTP相关的认证配置都在这个类的configure(HttpSecurity http)方法中,具体代码以下:
protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() // 拦截请求,建立了FilterSecurityInterceptor拦截器 .anyRequest().authenticated() // 设置全部请求都得通过认证后才能够访问 .and() // 用and来表示配置过滤器结束,以便进行下一个过滤器的建立和配置 .formLogin() // 设置表单登陆,建立UsernamePasswordAuthenticationFilter拦截器 .and() .httpBasic(); // 开启HTTP Basic,建立BasicAuthenticationFilter拦截器 }
这个方法中配置了三个拦截器,第一个是FilterSecurityInterceptor,第二个是基于表单登陆的UsernamePasswordAuthenticationFilter,第三个是基于HTTP Basic的BasicAuthenticationFilter,进入到authorizeRequests()、formLogin()、httpBasic()方法中,这三个方法的具体实现都在HttpSecurity类中,观察三个方法的具体实现,分别建立了各自的配置类对象,分别是:ExpressionUrlAuthorizationConfigurer对象、FormLoginConfigurer对象以及HttpBasicConfigurer对象,这三个配置类有一个公共的父接口SecurityConfigurer,它有一个configure方法,每个子类都会去实现这个方法,从而在这个方法里面配置各个拦截器(也并不是全部的拦截器都在configure方法中配置,好比UsernamePasswordAuthenticationFilter就是在构造方法中配置,后面会讨论)以及其余信息。本节将重点介绍BasicAuthenticationFilter,后面的文章中将继续介绍其余的认证方式。
咱们一块儿来解读一下HttpBasicConfigurer的configure方法,具体源码以下所示:
@Override public void configure(B http) throws Exception { AuthenticationManager authenticationManager = http .getSharedObject(AuthenticationManager.class); // 建立一个BasicAuthenticationFilter过滤器 BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( authenticationManager, this.authenticationEntryPoint); if (this.authenticationDetailsSource != null) { basicAuthenticationFilter .setAuthenticationDetailsSource(this.authenticationDetailsSource); } RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); if (rememberMeServices != null) { basicAuthenticationFilter.setRememberMeServices(rememberMeServices); } basicAuthenticationFilter = postProcess(basicAuthenticationFilter); // 将当前的BasicAuthenticationFilter对象添加到拦截器链中 http.addFilter(basicAuthenticationFilter); }
建立对象以及设置部分其余属性也能够在后面慢慢理解,那么最后一行代码将当前这个BasicAuthenticationFilter对象加入到了拦截器链中,咱们应该在此刻就要理解清楚。咱们都很清楚,做为拦截器链,链中的每一个拦截器都是有前后顺序的,那么这个BasicAuthenticationFilter拦截器是如何加入到拦截器链中的呢?我进入到addFilter方法中一探究竟。
public HttpSecurity addFilter(Filter filter) { Class<? extends Filter> filterClass = filter.getClass(); if (!comparator.isRegistered(filterClass)) { throw new IllegalArgumentException( "The Filter class " + filterClass.getName() + " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead."); } // 加入到拦截器列表中 this.filters.add(filter); return this; }
该addFilter方法在HttpSecurity类中,再加入到拦截器链以前,进行了一次检测,判断当前类型的拦截器是否已经注册到了默认的拦截器链Map集合中,返回的结果是拦截器的顺序值是否等于null的对比值。这个Map集合是以拦截器全限定类名为键,拦截器顺序值为值,且默认起始拦截器顺序为100,每一个拦截器之间的顺序值相隔100,这就为拦截器先后添加其余拦截器提供了预留位置,是一个很好的设计。
public boolean isRegistered(Class<? extends Filter> filter) { return getOrder(filter) != null; }
上述代码就是经过拦截器对象来获取拦截器的顺序值,而且与null相比,继续进入到getOrder方法:
private Integer getOrder(Class<?> clazz) { while (clazz != null) { Integer result = filterToOrder.get(clazz.getName()); if (result != null) { return result; } clazz = clazz.getSuperclass(); } return null; }
filterToOrder就是拦截器的Map集合,该集合中存储了多种拦截器,并规定了拦截器的顺序。由于BasicAuthenticationFilter类型的拦截器已经事先添加到了这个Map集合中,因此就返回了BasicAuthenticationFilter在整个拦截器链Map中的顺序值,这样isRegistered方法就会返回true,从而最后加入到了拦截器链中(拦截器链是一个List列表),这个Map集合中预先设置了多种拦截器,代码以下所示:
FilterComparator() { Step order = new Step(INITIAL_ORDER, ORDER_STEP); put(ChannelProcessingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(WebAsyncManagerIntegrationFilter.class, order.next()); put(SecurityContextPersistenceFilter.class, order.next()); put(HeaderWriterFilter.class, order.next()); put(CorsFilter.class, order.next()); put(CsrfFilter.class, order.next()); put(LogoutFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next()); put(X509AuthenticationFilter.class, order.next()); put(AbstractPreAuthenticatedProcessingFilter.class, order.next()); filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next()); put(UsernamePasswordAuthenticationFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); filterToOrder.put( "org.springframework.security.openid.OpenIDAuthenticationFilter", order.next()); put(DefaultLoginPageGeneratingFilter.class, order.next()); put(DefaultLogoutPageGeneratingFilter.class, order.next()); put(ConcurrentSessionFilter.class, order.next()); put(DigestAuthenticationFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next()); put(BasicAuthenticationFilter.class, order.next()); put(RequestCacheAwareFilter.class, order.next()); put(SecurityContextHolderAwareRequestFilter.class, order.next()); put(JaasApiIntegrationFilter.class, order.next()); put(RememberMeAuthenticationFilter.class, order.next()); put(AnonymousAuthenticationFilter.class, order.next()); filterToOrder.put( "org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next()); put(SessionManagementFilter.class, order.next()); put(ExceptionTranslationFilter.class, order.next()); put(FilterSecurityInterceptor.class, order.next()); put(SwitchUserFilter.class, order.next()); }
这是从代码层面解读到了各个拦截器的具体顺序,咱们从Spring Security的官方文档中也能够看到上述代码所规定顺序表,以下图所示:
上图中并无列出全部的拦截器,从图中咱们能够看出,BasicAuthenticationFilter位于UsernamePasswordAuthenticationFilter以后,ExceptionTranslationFilter和FilterSecurityInterceptor顺序与前面的Spring Security的基本原理图保持了一致。
若是咱们建立的Filter没有在预先设置的Map集合中,那么就会抛出一个IllegalArgumentException异常,并提示咱们使用addFilterBefore或者addFilterAfter方法将自定义的拦截器加入到拦截器链中,这一提示颇有用,由于本系列文章后面会讲到表单登陆原理的时候加入图形验证码功能将用到这一特性(将图形验证码的验证拦截器加入到UsernamePasswordAuthenticationFilter以前)。
上面的内容都是解释了BasicAuthenticationFilter是如何加入到拦截器链中的,属于知识前置铺垫,接下来咱们经过源码分析BasicAuthenticationFilter是如何进行验证的。
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { final boolean debug = this.logger.isDebugEnabled(); // 从请求头中获取头属性为Authorization的值 String header = request.getHeader("Authorization"); // 判断请求头中是否含有该属性或者该属性的值是不是以basic开头的 if (header == null || !header.toLowerCase().startsWith("basic ")) { // 说明不是HTTP Basic认证方式,因此进入到拦截器链的下一个拦截器中,本拦截器不做处理 chain.doFilter(request, response); return; } try { // extractAndDecodeHeader是解码Base64编码后的字符串,获取用户名和密码组成的字符数组 String[] tokens = extractAndDecodeHeader(header, request); assert tokens.length == 2; // 数组的第一个值是用户名,第二个值是密码 String username = tokens[0]; if (debug) { this.logger .debug("Basic Authentication Authorization header found for user '" + username + "'"); } // 判断当前请求是否须要认证,具体的判断标准能够进入到authenticationIsRequired中查看,这里简单表述一下,这个方法的逻辑是:首先判断Spring Security的上下文环境中是否存在当前用户名对应的认证信息,若是没有或者是有,可是没有认证的,那么就返回true,其次是认证信息是UsernamePasswordAuthenticationToken类型且认证信息的用户名和传入的用户名不一致,那么返回true,最后认证信息是AnonymousAuthenticationToken类型(匿名类型),那么直接返回true,不然其余状况直接返回false,也就是无需再次认证。 if (authenticationIsRequired(username)) { // 将用户名和密码封装成UsernamePasswordAuthenticationToken,并标记为未认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, tokens[1]); authRequest.setDetails( this.authenticationDetailsSource.buildDetails(request)); // 调用认证管理器来进行认证操做,具体的认证步骤在ProviderManager类authenticate方法中,该方法首先是获取Token的类型,这次认证的Token类型是UsernamePasswordAuthenticationToken,而后根据类型找到支持UsernamePasswordAuthenticationToken的Provider对象进行认证 Authentication authResult = this.authenticationManager .authenticate(authRequest); if (debug) { this.logger.debug("Authentication success: " + authResult); } // 将认证成功后结果存储到上下文环境中 SecurityContextHolder.getContext().setAuthentication(authResult); // “记住我”中设置记住当前认证信息,这个功能后期会重点介绍 this.rememberMeServices.loginSuccess(request, response, authResult); // 认证成功后的一些处理,能够自行实现 onSuccessfulAuthentication(request, response, authResult); } } // 若是认证失败,上述的authenticate方法会抛出异常表示认证失败 catch (AuthenticationException failed) { // 清除认证失败的上下文环境 SecurityContextHolder.clearContext(); if (debug) { this.logger.debug("Authentication request for failed: " + failed); } // “记住我”的认证失败后的处理,后面会介绍 this.rememberMeServices.loginFail(request, response); // 认证失败后的处理,能够自行实现 onUnsuccessfulAuthentication(request, response, failed); // 是否忽略认证失败,这里默认为false if (this.ignoreFailure) { chain.doFilter(request, response); } else { // 认证失败后,默认会进入到这里,从而调用到了BasicAuthenticationEntryPoint类中的commence方法,该方法的具体逻辑是在响应体中添加“WWW-Authenticate”的响应头,并设置值为Basic realm="Realm",这也就是用到了HTTP Basic的基本原理,当浏览器接收到响应以后,发现响应头中包含WWW-Authenticate,就会弹出一个要求输入用户名和密码的对话框,输入用户名和密码后,若是正确,那么就会访问到具体的资源,不然会一直会弹出对话框 this.authenticationEntryPoint.commence(request, response, failed); } return; } chain.doFilter(request, response); }
上述的源码中加入了详细的解析,对每个重要步骤都进行了解说,上面提到,具体的认证过程用到了UsernamePasswordAuthenticationToken,这个属于UsernamePasswordAuthenticationFilter的认证范畴,后面的文章将重点介绍(请持续关注个人Spring Security的源码分析哦),这里简单说明一下:使用UsernamePasswordAuthenticationToken封装的用户名和密码将由UsernamePasswordAuthenticationFilter来进行拦截认证,认证管理器拿到这个Token对象后,会从众多的ProviderManager对象中选择合适的manager来处理该Token,会将该用户名和密码与咱们在配置文件中配置的用户名和密码或者默认生成的UUID密码进行匹配,若是匹配成功,那么将返回认证成功的结果,这个结果将由FilterSecurityInterceptor判断,它决定最后是否放行,是否容许当前请求访问到/demo路由。
为了方便交流,本篇文章以及后续的文章中涉及到的案例代码都将托管到码云上,读者能够自行获取。最新代码都将在master分支上,《Spring Security入门到实践》的每一篇文章都有对应的分支,后续文章都会体现每篇文章具体对应于哪个分支。因为本人水平有限,源码分析不免有不妥之处,欢迎批评指正。
代码托管连接:https://gitee.com/itlemon/lea...
了解更多干货,欢迎关注个人微信公众号:爪哇论剑(微信号:itlemon)