Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
spring security spel_Spring框架介绍及使用,希望能够帮助你!!!。
spring security 的核心功能主要包括:
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
比如,对于username password认证过滤器来说,
会检查是否是一个登录请求;
是否包含username 和 password (也就是该过滤器需要的一些认证信息) ;
如果不满足则放行给下一个。
下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic eHh4Onh4 的信息。中间可能还有更多的认证过滤器。最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。
注意:绿色的过滤器可以配置是否生效,其他的都不能控制。
首先创建spring boot项目HelloSecurity,其pom主要依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </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>
然后在src/main/resources/templates/目录下创建页面:
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
我们可以看到, 在这个简单的视图中包含了一个链接: “/hello”. 链接到了如下的页面,Thymeleaf模板如下:
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
Web应用程序基于Spring MVC。 因此,你需要配置Spring MVC并设置视图控制器来暴露这些模板。 如下是一个典型的Spring MVC配置类。在src/main/java/hello目录下(所以java都在这里):
@Configuration public class MvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/home").setViewName("home"); registry.addViewController("/").setViewName("home"); registry.addViewController("/hello").setViewName("hello"); registry.addViewController("/login").setViewName("login"); } }
addViewControllers()方法(覆盖WebMvcConfigurerAdapter中同名的方法)添加了四个视图控制器。 两个视图控制器引用名称为“home”的视图(在home.html中定义),另一个引用名为“hello”的视图(在hello.html中定义)。 第四个视图控制器引用另一个名为“login”的视图。 将在下一部分中创建该视图。此时,可以跳过来使应用程序可执行并运行应用程序,而无需登录任何内容。然后启动程序如下:
@SpringBootApplication public class Application { public static void main(String[] args) throws Throwable { SpringApplication.run(Application.class, args); } }
假设你希望防止未经授权的用户访问“/ hello”。 此时,如果用户点击主页上的链接,他们会看到问候语,请求被没有被拦截。 你需要添加一个障碍,使得用户在看到该页面之前登录。您可以通过在应用程序中配置Spring Security来实现。 如果Spring Security在类路径上,则Spring Boot会使用“Basic认证”来自动保护所有HTTP端点。 同时,你可以进一步自定义安全设置。首先在pom文件中引入:
<dependencies> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ... </dependencies>
如下是安全配置,使得只有认证过的用户才可以访问到问候页面:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } }
WebSecurityConfig类使用了@EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还扩展了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。
configure(HttpSecurity)方法定义了哪些URL路径应该被保护,哪些不应该。具体来说,“/”和“/ home”路径被配置为不需要任何身份验证。所有其他路径必须经过身份验证。
当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。有一个由 loginPage()指定的自定义“/登录”页面,每个人都可以查看它。
对于configureGlobal(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。该用户的用户名为“user”,密码为“password”,角色为“USER”。
现在我们需要创建登录页面。前面我们已经配置了“login”的视图控制器,因此现在只需要创建登录页面即可:
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
你可以看到,这个Thymeleaf模板只是提供一个表单来获取用户名和密码,并将它们提交到“/ login”。 根据配置,Spring Security提供了一个拦截该请求并验证用户的过滤器。 如果用户未通过认证,该页面将重定向到“/ login?error”,并在页面显示相应的错误消息。 注销成功后,我们的应用程序将发送到“/ login?logout”,我们的页面显示相应的登出成功消息。最后,我们需要向用户提供一个显示当前用户名和登出的方法。 更新hello.html 向当前用户打印一句hello,并包含一个“注销”表单,如下所示:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
在 Spring boot 应用中使用 Spring Security,用到了 @EnableWebSecurity注解,官方说明为,该注解和 @Configuration 注解一起使用, 注解 WebSecurityConfigurer 类型的类,或者利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置。
一般情况,会选择继承 WebSecurityConfigurerAdapter 类,其官方说明为:WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制。
Demo 中重写了 WebSecurityConfigurerAdapter 的两个方法:
/** * 通过 {@link #authenticationManager()} 方法的默认实现尝试获取一个 {@link AuthenticationManager}. * 如果被复写, 应该使用{@link AuthenticationManagerBuilder} 来指定 {@link AuthenticationManager}. * * 例如, 可以使用以下配置在内存中进行注册公开内存的身份验证{@link UserDetailsService}: * * // 在内存中添加 user 和 admin 用户 * @Override * protected void configure(AuthenticationManagerBuilder auth) { * auth * .inMemoryAuthentication().withUser("user").password("password").roles("USER").and() * .withUser("admin").password("password").roles("USER", "ADMIN"); * } * * // 将 UserDetailsService 显示为 Bean * @Bean * @Override * public UserDetailsService userDetailsServiceBean() throws Exception { * return super.userDetailsServiceBean(); * } * */ protected void configure(AuthenticationManagerBuilder auth) throws Exception { this.disableLocalConfigureAuthenticationBldr = true; } /** * 复写这个方法来配置 {@link HttpSecurity}. * 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为: * * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); * */ protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
HttpSecurity 常用方法及说明:
方法 | 说明 |
---|---|
openidLogin() |
用于基于 OpenId 的验证 |
headers() |
将安全标头添加到响应 |
cors() |
配置跨域资源共享( CORS ) |
sessionManagement() |
允许配置会话管理 |
portMapper() |
允许配置一个PortMapper (HttpSecurity#(getSharedObject(class)) ),其他提供SecurityConfigurer 的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl 映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() |
配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() |
配置基于x509的认证 |
rememberMe |
允许配置“记住我”的验证 |
authorizeRequests() |
允许基于使用HttpServletRequest 限制访问 |
requestCache() |
允许配置请求缓存 |
exceptionHandling() |
允许配置错误处理 |
securityContext() |
在HttpServletRequests 之间的SecurityContextHolder 上设置SecurityContext 的管理。 当使用WebSecurityConfigurerAdapter 时,这将自动应用 |
servletApi() |
将HttpServletRequest 方法与在其上找到的值集成到SecurityContext 中。 当使用WebSecurityConfigurerAdapter 时,这将自动应用 |
csrf() |
添加 CSRF 支持,使用WebSecurityConfigurerAdapter 时,默认启用 |
logout() |
添加退出登录支持。当使用WebSecurityConfigurerAdapter 时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe() 身份验证,清除SecurityContextHolder ,然后重定向到”/login?success” |
anonymous() |
允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter 结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken 表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() |
指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String) ,则将生成默认登录页面 |
oauth2Login() |
根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() |
配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
httpBasic() |
配置 Http Basic 验证 |
addFilterAt() |
在指定的Filter类的位置添加过滤器 |
/** * {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for * easily building in memory authentication, LDAP authentication, JDBC based * authentication, adding {@link UserDetailsService}, and adding * {@link AuthenticationProvider}'s. */
意思是,AuthenticationManagerBuilder 用于创建一个 AuthenticationManager,让我能够轻松的实现内存验证、LADP验证、基于JDBC的验证、添加UserDetailsService、添加AuthenticationProvider。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } 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); }
调用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。如果需要验证,则会调用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三种结果:
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); return this.getAuthenticationManager().authenticate(authRequest); }
attemptAuthentication () 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证(即 this.getAuthenticationManager().authenticate(authRequest) )。默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。
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 { 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). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }
尝试验证 Authentication 对象。AuthenticationProvider 列表将被连续尝试,直到 AuthenticationProvider 表示它能够认证传递的过来的Authentication 对象。然后将使用该 AuthenticationProvider 尝试身份验证。如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。从代码中不难看出,由 provider 来验证 authentication, 核心点方法是:
Authentication result = provider.authenticate(authentication);
此处的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的实现,看看它的 authenticate(authentication) 方法:
// 验证 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); }
AbstractUserDetailsAuthenticationProvider 内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用如下方法获取用户信息,然后和 用户传来的信息进行对比来判断是否验证成功。
// 获取用户信息 UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
retrieveUser() 方法在 DaoAuthenticationProvider 中实现,DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子类。具体实现如下:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { 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; }
可以看到此处的返回对象 userDetails 是由 UserDetailsService 的 #loadUserByUsername(username) 来获取的。
对于看本文比较困难的同学可以移步:https://www.lanqiao.cn/courses/3013
邀请码:STyLDQzM
其实内容和博客差不多,只不过更详细(就是一步一步的来,所有步骤有详细过程记录和截图,按照课程步骤最终能完整的操作完整个项目过程,适合小白),版本上也有所升级,还有就是有问题可以直接沟通,谢谢支持!
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章