配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
权限管理
SpringSecurity 简介
整体架构
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制
,按照安全规则
或者安全策略
控制用户可以访问而且只能访问自己被授权的资源
。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限,方可访问。
身份认证
,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式就是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
授权
,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限,方可访问系统的资源,对于某些资源没有权限是无法访问的。
和其他领域不同,在 Java 企业级开发中,安全管理框架非常少,目前比较常见的就是:
Shiro
Shiro 本身就是一个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、可以在JavaSE环境中使用等。不过,在微服务时代,Shiro 就显得力不从心了,在微服务面前和扩展方面,无法充分展示自己的优势。
开发者自定义
也有很多公司选择自定义权限,即自己开发权限管理。但是一个系统的安全,不仅仅是登录和权限控制这么简单,我们还要考虑各种各样可能存在的网络政击以及防彻策略,从这个角度来说,开发者自己实现安全管理也并非是一件容易的事情,只有大公司才有足够的人力物力去支持这件事情。
Spring Security
Spring Security作为spring家族的一员,在和 Spring 家族的其他成员如 Spring Boot Spring Clond等进行整合时,具有其他框架无可比拟的优势,同时对 OAuth2 有着良好的支持,再加上Spring Cloud对 Spring Security的不断加持(如推出 Spring Cloud Security ),让 Spring Securiy 不知不觉中成为微服务项目的首选安全管理方案。但Spring Security使用相对复杂,学习成本较高!
总结:目前主流是Shiro和Spring Security,各占50%,自定义权限管理较少,但随着Spring的流行,Spring Security将得到更进一步的发展。
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
Spring Security
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
Spring Security是一个功能强大、可高度定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
Spring Security是一个面向Java应用程序、提供身份验证和安全性的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以轻松地扩展以满足定制需求。
总结:Spring Security是一个功能强大、可高度定制的身份验证
和访问控制
的框架,或者说是用来实现系统中权限管理的框架。
Spring Security 最早叫 Acegi Security, 这个名称并不是说它和 Spring 就没有关系,它依然是为Spring 框架提供安全支持的。Acegi Security 基于 Spring,可以帮助我们为项目建立丰富的角色与权限管理系统。Acegi security 虽然好用,但是最为人诟病的则是它臃肿烦琐的配置,这一问题最终也遗传给了 Spring Security。
Acegi Security 最终被并入 Spring Security 项目中,并于 2008 年4月发布了改名后的第一个版本 Spring Security 2.0.0,到目前为止,Spring Security 的最新版本己经到了 5.6.1。和 Shiro 相比,Spring Security重量级并且配置烦琐,直至今天,依然有人以此为理由而拒绝了解 Spring Security。其实,自从 Spring Boot 推出后,就彻底颠覆了传统了 JavaEE 开发,自动化配置让许多事情变得非常容易,包括 Spring Security 的配置。在一个 Spring Boot 项目中,我们甚至只需要引入一个依赖,不需要任何额外配置,项目的所有接口就会被自动保护起来了。在 Spring Cloud中,很多涉及安全管理的问题,也是一个 Spring Security 依赖两行配置就能搞定,在和 Spring 家族的产品一起使用时,Spring Security 的优势就非常明显了。
因此,在微服务时代,我们不需要纠结要不要学习 Spring Security,我们要考虑的是如何快速掌握Spring Security, 并且能够使用 Spring Security 实现我们微服务的安全管理。
总结:SpringBoot之前主要使用Shiro做权限管理,SpringBoot出现之后才广泛使用Spring Security作为权限管理,Spring Security只针对Spring框架使用起来方便,在其他框架中可使用Shiro作为权限管理。
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
在认证
授权
接口:AuthenticationManager
在Spring Security中,认证是由AuthenticationManager
接口来负责的,接口定义为:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
返回 Authentication 表示认证成功
返回 AuthenticationException 异常,表示认证失败。
AuthenticationManager的主要实现类为 ProviderManager,在 ProviderManager 中管理了众多 AuthenticationProvider 实例。在一次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider ,用来实现多种认证方式,这些 AuthenticationProvider 都是由 ProviderManager 进行统一管理的。
接口:Authentication
认证以及认证成功的信息主要是由 Authentication 的实现类进行保存的,其接口定义为:
public interface Authentication extends Principal, Serializable { Collection extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; } /* 其中: - getAuthorities 获取用户权限信息 - getCredentials 获取用户凭证信息,一般指密码 - getDetails 获取用户详细信息 - getPrincipal 获取用户身份信息,用户名、用户对象等 - isAuthenticated 用户是否认证成功 */
SecurityContextHolder
SecurityContextHolder 用来获取登录之后用户信息。
Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中,SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。
当完成认证后,接下来就是授权了。在 Spring Security 的授权体系中,有2个关键接口:
AccessDecisionManager
AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter
AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
总结:AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
接口:ConfigAttribute
ConfigAttribute,用来保存授权时的角色信息
在 Spring Security 中,当用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色时,会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_
前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具有的角色和请求某个资源所需的 ConfigAtuibute 之间的关系。
环境搭建
自动配置细节
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
spring boot
spring security
认证: 判断用户是否是系统合法用户过程
授权: 判断系统内用户可以访问或具有访问那些资源权限过程
新建一个名为SpringSecurity的空项目,在里面新建module,创建名为spring-security-01的Spring Initializr、Spring Web的SpringBoot项目。
IDEA添加自动导包、格式化配置
新建HelloController用于测试
package com.study.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName HelloController * @Description TODO * @Author Jiangnan Cui * @Date 2022/7/4 17:51 * @Version 1.0 */ @RestController @RequestMapping("/hello") public class HelloController { @RequestMapping("/hello") public String hello() { System.out.println("Hello Spring Security"); return "hello spring security"; } }
启动项目进行测试,访问路径:http://localhost:8080/hello/hello
同时,IDEA控制台输出:Hello Spring Security,表示项目创建成功!
1.引入spring security相关依赖,刷新maven
org.springframework.boot spring-boot-starter-security
2.再次启动项目:控制台输出Using generated security password: 5f37fc93-1ad0-4333-a84b-cec8dc25c16c,其中,5f37fc93-1ad0-4333-a84b-cec8dc25c16c为引入spring security后生成的登录密码
3.重新访问:http://localhost:8080/hello/hello
发现会跳出登录页,此时需要输入用户名和密码,默认用户名为user,密码为启动项目后IDEA控制台输出的密码(Using generated security password),即生成的UUID
能实现上述结果,表示SpringBoot整合SpringSecurity环境搭建成功!
注意:以后每次重新启动项目后都会生成新的登录密码,登录用户名始终默认为user,以后的任何请求都要经过此登录页面后才能正常访问!!!
这就是 Spring Security 的强大之处,只需要引入一个依赖,所有的接口就会自动保护起来!
思考?
为什么引入 Spring Security 之后没有任何配置所有请求就要认证
呢?
在项目中明明没有登录界面,登录界面
怎么来的呢?
为什么使用 user
和 控制台密码
能登陆,登录时验证数据源存在哪里呢?
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
官方原理地址:Spring Security Reference
虽然开发者只引入了一个依赖,但可以让 Spring Security 对应用进行保护。Spring Security 又是如何做到的呢?
在 Spring Security 中 认证、授权
等功能都是基于过滤器(Filter)完成的。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个 FlterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中。FilterChainProxy作为一个顶层的管理者,将统一管理 Security Filter。FilterChainProxy 本身是通过 Spring 框架提供的 DelegatingFilterProxy 整合到原生的过滤器链中。
总结:请求一般都是通过Filter进行拦截的。现有的SpringBoot项目中已经默认集成了Tomcat,Spring Security底层使用了大量的Filter来实现权限管理,但只有JavaWeb原生的Filter能在受限资源之前拿到JavaWeb请求,而Spring Security中的Filter不能直接对请求进行直接拦截,需要通过Spring框架中集成的DelegatingFilterProxy将Spring Security中的Filter整合到JavaWeb原生的Filter上,从而实现对请求的拦截。
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
那么在 Spring Security 中给我们提供那些过滤器? 默认情况下那些过滤器会被加载呢?
过滤器 | 过滤器作用 | 默认是否加载 |
---|---|---|
ChannelProcessingFilter | 过滤请求协议 HTTP 、HTTPS | NO |
WebAsyncManagerIntegrationFilter |
将 WebAsyncManger 与 SpringSecurity 上下文进行集成 | YES |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到 SecurityContextHolder 中 | YES |
HeaderWriterFilter |
处理头信息加入响应中 | YES |
CorsFilter | 处理跨域问题 | NO |
CsrfFilter |
处理 CSRF 攻击 | YES |
LogoutFilter |
处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理 OAuth2 认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理 SAML 认证 | NO |
X509AuthenticationFilter | 处理 X509 认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理 CAS 单点登录 | NO |
OAuth2LoginAuthenticationFilter | 处理 OAuth2 认证 | NO |
Saml2WebSsoAuthenticationFilter | 处理 SAML 认证 | NO |
UsernamePasswordAuthenticationFilter |
处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理 OpenID 认证 | NO |
DefaultLoginPageGeneratingFilter |
配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter |
配置默认注销页面 | YES |
ConcurrentSessionFilter | 处理 Session 有效期 | NO |
DigestAuthenticationFilter | 处理 HTTP 摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理 OAuth2 认证的 Access Token | NO |
BasicAuthenticationFilter |
处理 HttpBasic 登录 | YES |
RequestCacheAwareFilter |
处理请求缓存 | YES |
SecurityContextHolder |
包装原始请求 | YES |
JaasApiIntegrationFilter | 处理 JAAS 认证 | NO |
RememberMeAuthenticationFilter |
处理 RememberMe 登录 | NO |
AnonymousAuthenticationFilter |
配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中授权码 | NO |
SessionManagementFilter |
处理 session 并发问题 | YES |
ExceptionTranslationFilter |
处理认证/授权中的异常 | YES |
FilterSecurityInterceptor |
处理授权相关 | YES |
SwitchUserFilter | 处理账户切换 | NO |
可以看出,Spring Security 提供了 30 多个过滤器。默认情况下Spring Boot 在对 Spring Security 进入自动化配置时,会创建一个名为 SpringSecurityFilerChain 的过滤器,并注入到 Spring 容器中,这个过滤器将负责所有的安全管理,包括用户认证、授权、重定向到登录页面等。具体可以参考WebSecurityConfiguration的源码:
默认加载的15个Filter的执行顺序如下:
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
SpringBootWebSecurityConfiguration这个类是 spring boot自动配置类:
@Configuration(proxyBeanMethods = false) @ConditionalOnDefaultWebSecurity @ConditionalOnWebApplication(type = Type.SERVLET) class SpringBootWebSecurityConfiguration { @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest() .authenticated().and().formLogin().and().httpBasic(); return http.build(); } }
通过这个源码得知,默认情况下对所有请求进行权限控制。
这就是为什么在引入 Spring Security 中没有任何配置情况下,请求会被拦截的原因!
通过上面对自动配置分析,我们也能看出默认生效条件为:
class DefaultWebSecurityCondition extends AllNestedConditions { DefaultWebSecurityCondition() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class }) static class Classes { } @ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class }) static class Beans { } }
条件一:classpath中存在 SecurityFilterChain.class、HttpSecurity.class
条件二:没有自定义的WebSecurityConfigurerAdapter.class(主要用于自定义配置)、SecurityFilterChain.class(主要用于自定义Filter)
默认情况下,条件都是满足的。WebSecurityConfigurerAdapter 这个类极其重要,Spring Security 核心配置都在这个类中:
如果要对 Spring Security 进行自定义配置,就要自定义这个类实例,通过覆盖类中方法达到修改默认配置的目的。其中的config方法被经常使用:
protected void configure(HttpSecurity http) throws Exception { this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http.authorizeRequests((requests) -> { ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated(); }); http.formLogin(); http.httpBasic(); }
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
client请求 /hello 接口,在引入spring security之后会先经过一系列过滤器
在请求到达FilterSecurityInterceptor时,发现请求并未认证,请求被拦截下来,并抛出AccessDeniedException异常。
抛出 AccessDeniedException的异常会被ExceptionTranslationFilter捕获,这个 Filter 中会调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回 302,要求客户端进行重定向到 /login页面。
客户端再次发送 /login 请求。
/login请求会再次被拦截器中DefaultLoginPageGeneratingFilter拦截到,并在拦截器中返回生成登录页面(纯代码拼接而成的页面)。
就是通过这种方式,Spring Security 默认过滤器中生成了登录页面,并返回!
附:DefaultLoginPageGeneratingFilter.java
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.security.web.authentication.ui; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.function.Function; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.util.HtmlUtils; public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { public static final String DEFAULT_LOGIN_PAGE_URL = "/login"; public static final String ERROR_PARAMETER_NAME = "error"; private String loginPageUrl; private String logoutSuccessUrl; private String failureUrl; private boolean formLoginEnabled; private boolean openIdEnabled; private boolean oauth2LoginEnabled; private boolean saml2LoginEnabled; private String authenticationUrl; private String usernameParameter; private String passwordParameter; private String rememberMeParameter; private String openIDauthenticationUrl; private String openIDusernameParameter; private String openIDrememberMeParameter; private Mapoauth2AuthenticationUrlToClientName; private Map saml2AuthenticationUrlToProviderName; private Function > resolveHiddenInputs = (request) -> { return Collections.emptyMap(); }; public DefaultLoginPageGeneratingFilter() { } public DefaultLoginPageGeneratingFilter(AbstractAuthenticationProcessingFilter filter) { if (filter instanceof UsernamePasswordAuthenticationFilter) { this.init((UsernamePasswordAuthenticationFilter)filter, (AbstractAuthenticationProcessingFilter)null); } else { this.init((UsernamePasswordAuthenticationFilter)null, filter); } } public DefaultLoginPageGeneratingFilter(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) { this.init(authFilter, openIDFilter); } private void init(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) { this.loginPageUrl = "/login"; this.logoutSuccessUrl = "/login?logout"; this.failureUrl = "/login?error"; if (authFilter != null) { this.initAuthFilter(authFilter); } if (openIDFilter != null) { this.initOpenIdFilter(openIDFilter); } } private void initAuthFilter(UsernamePasswordAuthenticationFilter authFilter) { this.formLoginEnabled = true; this.usernameParameter = authFilter.getUsernameParameter(); this.passwordParameter = authFilter.getPasswordParameter(); if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) { this.rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter(); } } private void initOpenIdFilter(AbstractAuthenticationProcessingFilter openIDFilter) { this.openIdEnabled = true; this.openIDusernameParameter = "openid_identifier"; if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) { this.openIDrememberMeParameter = ((AbstractRememberMeServices)openIDFilter.getRememberMeServices()).getParameter(); } } public void setResolveHiddenInputs(Function > resolveHiddenInputs) { Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); this.resolveHiddenInputs = resolveHiddenInputs; } public boolean isEnabled() { return this.formLoginEnabled || this.openIdEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled; } public void setLogoutSuccessUrl(String logoutSuccessUrl) { this.logoutSuccessUrl = logoutSuccessUrl; } public String getLoginPageUrl() { return this.loginPageUrl; } public void setLoginPageUrl(String loginPageUrl) { this.loginPageUrl = loginPageUrl; } public void setFailureUrl(String failureUrl) { this.failureUrl = failureUrl; } public void setFormLoginEnabled(boolean formLoginEnabled) { this.formLoginEnabled = formLoginEnabled; } public void setOpenIdEnabled(boolean openIdEnabled) { this.openIdEnabled = openIdEnabled; } public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) { this.oauth2LoginEnabled = oauth2LoginEnabled; } public void setSaml2LoginEnabled(boolean saml2LoginEnabled) { this.saml2LoginEnabled = saml2LoginEnabled; } public void setAuthenticationUrl(String authenticationUrl) { this.authenticationUrl = authenticationUrl; } public void setUsernameParameter(String usernameParameter) { this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { this.passwordParameter = passwordParameter; } public void setRememberMeParameter(String rememberMeParameter) { this.rememberMeParameter = rememberMeParameter; this.openIDrememberMeParameter = rememberMeParameter; } public void setOpenIDauthenticationUrl(String openIDauthenticationUrl) { this.openIDauthenticationUrl = openIDauthenticationUrl; } public void setOpenIDusernameParameter(String openIDusernameParameter) { this.openIDusernameParameter = openIDusernameParameter; } public void setOauth2AuthenticationUrlToClientName(Map oauth2AuthenticationUrlToClientName) { this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName; } public void setSaml2AuthenticationUrlToProviderName(Map saml2AuthenticationUrlToProviderName) { this.saml2AuthenticationUrlToProviderName = saml2AuthenticationUrlToProviderName; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { boolean loginError = this.isErrorPage(request); boolean logoutSuccess = this.isLogoutSuccess(request); if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) { chain.doFilter(request, response); } else { String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml); } } private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "Invalid credentials"; if (loginError) { HttpSession session = request.getSession(false); if (session != null) { AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION"); errorMsg = ex != null ? ex.getMessage() : "Invalid credentials"; } } String contextPath = request.getContextPath(); StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); sb.append(" \n"); sb.append(" \n"); sb.append(" \n"); sb.append(" \n"); sb.append(" \n"); sb.append(" Please sign in \n"); sb.append(" \n"); sb.append(" \n"); sb.append(" \n"); sb.append(" \n"); sb.append("\n"); if (this.formLoginEnabled) { sb.append(" \n"); } if (this.openIdEnabled) { sb.append(" \n"); } Iterator var7; Map.Entry relyingPartyUrlToName; String url; String partyName; if (this.oauth2LoginEnabled) { sb.append("\n"); sb.append(""); return sb.toString(); } private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); Iterator var3 = ((Map)this.resolveHiddenInputs.apply(request)).entrySet().iterator(); while(var3.hasNext()) { Map.EntryLogin with OAuth 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("\n"); var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator(); while(var7.hasNext()) { relyingPartyUrlToName = (Map.Entry)var7.next(); sb.append("
\n"); } if (this.saml2LoginEnabled) { sb.append("\n"); } sb.append(" "); url = (String)relyingPartyUrlToName.getKey(); sb.append(""); partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue()); sb.append(partyName); sb.append(""); sb.append(" Login with SAML 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append("\n"); var7 = this.saml2AuthenticationUrlToProviderName.entrySet().iterator(); while(var7.hasNext()) { relyingPartyUrlToName = (Map.Entry)var7.next(); sb.append("
\n"); } sb.append("\n"); } sb.append(" "); url = (String)relyingPartyUrlToName.getKey(); sb.append(""); partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue()); sb.append(partyName); sb.append(""); sb.append(" input = (Map.Entry)var3.next(); sb.append("\n"); } return sb.toString(); } private String createRememberMe(String paramName) { return paramName == null ? "" : " Remember me on this computer.
\n"; } private boolean isLogoutSuccess(HttpServletRequest request) { return this.logoutSuccessUrl != null && this.matches(request, this.logoutSuccessUrl); } private boolean isLoginUrlRequest(HttpServletRequest request) { return this.matches(request, this.loginPageUrl); } private boolean isErrorPage(HttpServletRequest request) { return this.matches(request, this.failureUrl); } private static String createError(boolean isError, String message) { return !isError ? "" : "" + HtmlUtils.htmlEscape(message) + ""; } private static String createLogoutSuccess(boolean isLogoutSuccess) { return !isLogoutSuccess ? "" : "You have been signed out"; } private boolean matches(HttpServletRequest request, String url) { if ("GET".equals(request.getMethod()) && url != null) { String uri = request.getRequestURI(); int pathParamIndex = uri.indexOf(59); if (pathParamIndex > 0) { uri = uri.substring(0, pathParamIndex); } if (request.getQueryString() != null) { uri = uri + "?" + request.getQueryString(); } return "".equals(request.getContextPath()) ? uri.equals(url) : uri.equals(request.getContextPath() + url); } else { return false; } } }
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
1. 查看 SpringBootWebSecurityConfiguration#defaultSecurityFilterChain 方法表单登录
2. 处理登录为 FormLoginConfigurer 类中 调用 UsernamePasswordAuthenticationFilter这个类实例
3.查看类中 UsernamePasswordAuthenticationFilter#attempAuthentication 方法得知实际调用 AuthenticationManager 中 authenticate 方法
4. 调用 ProviderManager 类中方法 authenticate
5.调用了 ProviderManager 实现类中 AbstractUserDetailsAuthenticationProvider类中方法
6.最终调用实现类 DaoAuthenticationProvider 类中方法比较
看到这里就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!
通过刚才源码分析也能得知 UserDetailService 是顶层父接口,接口中 loadUserByUserName 方法是用来在认证时进行用户名认证方法,默认实现使用是内存实现,如果想要修改数据库实现我们只需要自定义 UserDetailService 实现,最终返回 UserDetails 实例即可。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
这个源码非常多,这里梳理了关键部分:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(AuthenticationManager.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean( value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = { "org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" }) public class UserDetailsServiceAutoConfiguration { //.... @Bean @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProviderpasswordEncoder) { SecurityProperties.User user = properties.getUser(); List roles = user.getRoles(); return new InMemoryUserDetailsManager( User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())) .roles(StringUtils.toStringArray(roles)).build()); } //... }
结论
从自动配置源码中得知当 classpath 下存在 AuthenticationManager 类;
当前项目中,系统没有提供 AuthenticationManager.class、 AuthenticationProvider.class、UserDetailsService.class、AuthenticationManagerResolver.class实例。
默认情况下都会满足,此时Spring Security会提供一个 InMemoryUserDetailManager 实例:
@ConfigurationProperties(prefix = "spring.security") public class SecurityProperties { private final User user = new User(); public User getUser() { return this.user; } //.... public static class User { private String name = "user"; private String password = UUID.randomUUID().toString(); private Listroles = new ArrayList<>(); private boolean passwordGenerated = true; //get set ... } }
这就是默认生成 user 以及 uuid 密码过程!
另外看明白源码之后,就知道只要在配置文件中加入如下配置可以对内存中用户和密码进行覆盖。
spring.security.user.name=root spring.security.user.password=root spring.security.user.roles=admin,users
重新启动项目后输入配置文件中的用户名和密码即可正常访问,原有的登录方式失效!
配套视频:【编程不良人】SpringSecurity 最新实战教程,更新中..._哔哩哔哩_bilibili
AuthenticationManager、ProviderManger、以及 AuthenticationProvider 关系:
WebSecurityConfigurerAdapter 扩展 Spring Security 所有默认配置:
UserDetailService 用来修改默认认证的数据源信息