每个人迟早都需要为他的项目增加安全性,在Spring生态系统中,您可以借助Spring Security库来做到这一点。
因此,当您继续将Spring Security添加到您的Spring Boot(或普通Spring)项目中,然后突然...
…您有自动生成的登录页面。
…您无法再执行POST请求。
…您的整个应用程序处于锁定状态,并提示您输入用户名和密码。
在随后的精神崩溃中幸存下来之后,您可能会对所有这些工作原理感兴趣。
简短的答案:
从本质上讲,Spring Security实际上只是一堆servlet过滤器,可帮助您向Web应用程序添加身份验证和授权。
它还与Spring Web MVC(或Spring Boot)之类的框架以及OAuth2或SAML之类的标准很好地集成。并且它会自动生成登录/注销页面,并防御CSRF等常见漏洞。
现在,那真的没有帮助,是吗?
幸运的是,答案很长:
本文的其余部分。
在成为Spring Security Guru之前,您需要了解三个重要概念:
认证方式
授权书
Servlet过滤器
家长建议:不要跳过本节,因为它是Spring Security所做的一切的基础。另外,我将使其尽可能有趣。
首先,如果您正在运行典型的(网络)应用程序,则需要您的用户进行身份验证。这意味着您的应用需求,以验证用户是否是谁,他声称自己是,通常与一个用户名和密码检查完成。
用户:“我是美国总统。我username
是:potus!”
您的web应用程序:“当然password
,总统先生,您那是什么?”
用户:“我的密码是:th3don4ld”。
您的网络应用程序:“正确。欢迎您,先生!”
在更简单的应用程序中,身份验证可能就足够了:用户进行身份验证后,便可以访问应用程序的每个部分。
但是大多数应用程序都有权限(或角色)的概念。想象一下:有权访问您的Webshop面向公众的前端的客户,以及有权访问单独的管理区域的管理员。
两种类型的用户都需要登录,但是仅凭身份验证并不能说明他们在系统中可以执行的操作。因此,您还需要检查经过身份验证的用户的权限,即需要授权用户。
用户:“让我玩那个核足球……”。
您的web应用程序:permissions
``请稍等一会,我需要检查一下....是的,总统先生,您的权限级别正确。请尽情享受。''
用户:“那个红色按钮又是什么……?”
最后但并非最不重要的一点,让我们看一下Servlet过滤器。他们与身份验证和授权有什么关系?(如果您是Java Servlet或过滤器的新手,建议您阅读旧的但仍然非常有效的Head First Servlets书。)
为什么要使用Servlet过滤器?
回想一下我的另一篇文章,我们发现基本上任何Spring Web应用程序都只是一个servlet:Spring的老版本DispatcherServlet,它将传入的HTTP请求(例如,来自浏览器)重定向到您的@Controllers或@RestControllers。
关键是:该DispatcherServlet中没有安全编码,您也很可能不想在@Controllers中摸索原始的HTTP Basic Auth标头。最佳情况下,应该在请求到达您的@Controllers 之前完成身份验证和授权。
幸运的是,在Java网络世界中,有一种方法可以做到这一点:您可以将过滤器 放在 servlet的前面,这意味着您可以考虑编写SecurityFilter并在Tomcat(servlet容器/应用程序服务器)中对其进行配置以过滤所有传入的信息命中servlet之前的HTTP请求。
+-------------------------------+ +-----------------------------------+
| Browser | | SecurityFilter (Tomcat) |
|-------------------------------| |-----------------------------------|
| | | |
| https://my.bank/account | -------> | Check if user is authenticated/ |
| | | 1. authenticated |
| | | 2. authorized |
| | | |
| | | -- if false: HTTP 401/403 | ---------> +-----------------------------------+
| | | -- if true: continue to servlet | | DispatcherServlet (Tomcat) |
| | | | | @RestController/@Controller |
| | +-----------------------------------+ +-----------------------------------+
+-------------------------------+
天真的SecurityFilter
一个SecurityFilter大约有4个任务,一个过分简单的实现可能看起来像这样:
import javax.servlet.*;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SecurityServletFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
UsernamePasswordToken token = extractUsernameAndPasswordFrom(request);
if (notAuthenticated(token)) {
// either no or wrong username/password
// unfortunately the HTTP status code is called "unauthorized", instead of "unauthenticated"
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401.
return;
}
if (notAuthorized(token, request)) {
// you are logged in, but don't have the proper rights
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // HTTP 403
return;
}
// allow the HttpRequest to go to Spring's DispatcherServlet
// and @RestControllers/@Controllers.
chain.doFilter(request, response);
}
private UsernamePasswordToken extractUsernameAndPasswordFrom(HttpServletRequest request) {
// Either try and read in a Basic Auth HTTP Header, which comes in the form of user:password
// Or try and find form login request parameters or POST bodies, i.e. "username=me" & "password="myPass"
return checkVariousLoginOptions(request);
}
private boolean notAuthenticated(UsernamePasswordToken token) {
// compare the token with what you have in your database...or in-memory...or in LDAP...
return false;
}
private boolean notAuthorized(UsernamePasswordToken token, HttpServletRequest request) {
// check if currently authenticated user has the permission/role to access this request's /URI
// e.g. /admin needs a ROLE_ADMIN , /callcenter needs ROLE_CALLCENTER, etc.
return false;
}
}
首先,过滤器需要从请求中提取用户名/密码。它可以通过Basic Auth HTTP Header,表单字段或Cookie等进行。
然后,筛选器需要针对诸如数据库之类的东西来验证该用户名/密码组合。
过滤器需要在成功认证后检查用户是否有权访问所请求的URI。
如果该请求在所有这些检查中仍然存在,则过滤器可以使该请求通过您的DispatcherServlet,即@Controllers。
过滤链
现实检查:上面的代码可以编译时,迟早会导致一个带有大量用于各种身份验证和授权机制的代码的怪兽过滤器。
但是,在现实世界中,您可以将此过滤器拆分为多个过滤器,然后将它们链接在一起。
例如,传入的HTTP请求将...
首先,通过LoginMethodFilter ...
然后,通过AuthenticationFilter ...
然后,通过AuthorizationFilter ...
最后,点击您的servlet。
这个概念称为FilterChain,上面过滤器中的最后一个方法调用实际上委托给了那个链:
chain.doFilter(request, response);
使用这样的过滤器(链),您基本上可以处理应用程序中存在的每个身份验证或授权问题,而无需更改实际的应用程序实现(请考虑:@RestControllers / @Controllers)。
有了这些知识,让我们了解一下Spring Security如何利用这种过滤器魔术。
我们将以与上一章相反的方向开始,以非常规的方式介绍Spring Security,从Spring Security的FilterChain开始。
假设您正确设置了Spring Security,然后启动了Web应用程序。您将看到以下日志消息:
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
如果将这一行扩展到列表中,Spring Security似乎不仅会安装一个过滤器,还会安装由15个(!)不同过滤器组成的整个过滤器链。
因此,当HTTPRequest传入时,它将通过所有这15个过滤器,直到您的请求最终到达@RestControllers为止。顺序也很重要,从该列表的顶部开始,一直到底部。
+----------------------------------+ +----------------------------------------+ +---------------------------------------+
| Browser HTTP Request |---------> | SecurityContextPersistenceFilter | -------> | HeaderWriterFilter | ----->
+----------------------------------+ +----------------------------------------+ +---------------------------------------+
+----------------------------------+ +----------------------------------------+ +---------------------------------------+
| CsrfFilter |---------> | LogoutFilter | -------> | UsernamePasswordAuthenticationFilter | ----->
+----------------------------------+ +----------------------------------------+ +---------------------------------------+
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
| DefaultLoginPageGeneratingFilter |---------> | DefaultLogoutPageGeneratingFilter | -------> | BasicAuthenticationFilter | ----->
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
| RequestCacheAwareFilter |---------> | SecurityContextHolderAwareRequestFilter| -------> | AnonymousAuthenticationFilter | ----->
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
| SessionManagementFilter |---------> | ExceptionTranslationFilter | -------> | FilterSecurityInterceptor | ----->
+----------------------------------+ +----------------------------------------+ +--------------------------------------+
+----------------------------------+
| your @RestController/@Controller |
+----------------------------------+
详细了解该链中的每个过滤器都太过分了,但是这里是其中一些过滤器的解释。随意查看Spring Security的源代码以了解其他过滤器。
BasicAuthenticationFilter:尝试在请求中查找基本身份验证HTTP标头,如果找到,则尝试使用标头的用户名和密码对用户进行身份验证。
UsernamePasswordAuthenticationFilter:尝试查找用户名/密码请求参数/ POST正文,如果找到,则尝试使用这些值对用户进行身份验证。
DefaultLoginPageGeneratingFilter:如果您没有明确禁用该功能,则为您生成一个登录页面。这是启用Spring Security时获得默认登录页面的原因。
DefaultLogoutPageGeneratingFilter:如果您未明确禁用该功能,则为您生成一个注销页面。
FilterSecurityInterceptor:执行您的授权。
因此,通过这两个过滤器,Spring Security为您提供了一个登录/注销页面,并提供了使用基本身份验证或表单登录进行登录的功能,以及一些其他功能,例如CsrfFilter,我们将拥有一个以后再看。
Half-Time Break:这些过滤器在很大程度上是 Spring Security。不多不少。他们完成所有工作。剩下要做的就是配置它们的工作方式,即要保护的URL,要忽略的URL以及要用于身份验证的数据库表。
因此,接下来我们需要看看如何配置Spring Security。
使用最新的Spring Security和/或Spring Boot版本,配置Spring Security的方法是通过具有以下类:
用@EnableWebSecurity注释。
扩展了WebSecurityConfigurer,它基本上为您提供了配置DSL /方法。使用这些方法,您可以指定应用程序中要保护的URI或要启用/禁用的漏洞利用保护。
这是典型的WebSecurityConfigurerAdapter的外观:
@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()
.and()
.httpBasic();
}
}
带有@EnableWebSecurity批注的常规Spring @Configuration,从WebSecurityConfigurerAdapter扩展。
通过重写适配器的configure(HttpSecurity)方法,您将获得一个不错的小DSL,可用来配置FilterChain。
所有请求去/
和/home
被允许(允许) -用户就不能有进行身份验证。您正在使用antMatcher,这意味着您还可以在字符串中使用通配符(*,\ * \ * ,?)。
任何其他请求需要被认证的用户的第一,即,用户需要登录。
您正在允许使用自定义的loginPage(/login
即不是Spring Security的自动生成的)登录表单(表单中的用户名/密码)。任何人都应该能够访问登录页面,而无需先登录(permitAll;否则我们将获得Catch-22!)。
注销页面也一样
最重要的是,您还允许基本身份验证,即发送HTTP基本身份验证标头进行身份验证。
如何使用Spring Security的配置DSL
习惯了该DSL需要花费一些时间,但是您可以在FAQ部分中找到更多示例:AntMatchers:常见示例。
现在重要的是,此 configure
方法是您指定的位置:
要保护的URL(authenticated())和允许的URL(permitAll())。
允许哪些身份验证方法(formLogin(),httpBasic())及其配置方式。
简而言之:您的应用程序的完整安全性配置。
注意:您不需要立即重写适配器的configure方法,因为它带有相当合理的实现-默认情况下。看起来是这样的:
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
要访问应用程序上的任何 URI(anyRequest()
),您需要进行身份验证(authenticated())。
formLogin()
使用默认设置的表单登录()已启用。
和HTTP基本身份验证(httpBasic()
)一样。
这种默认配置是您在向其添加Spring Security之后立即将其锁定的原因。很简单,不是吗?
摘要:WebSecurityConfigurerAdapter的DSL配置
我们了解到,Spring Security由使用WebSecurityConfigurerAdapter @Configuration类配置的几个过滤器组成。
但是缺少一个关键的部分。让我们以Spring的BasicAuthFilter为例。它可以从HTTP Basic Auth标头提取用户名/密码,但是对这些凭证进行身份验证又是基于什么呢?
这自然使我们想到了身份验证如何与Spring Security一起工作的问题。
在身份验证和Spring Security方面,您大致有以下三种情况:
该默认:您可以访问用户的(加密)密码,因为你有他保存在例如数据库表的详细信息(用户名,密码)。
不太常见:您无法访问用户的(加密)密码。如果您的用户和密码存储在其他地方,例如在提供身份验证的REST服务的第三方身份管理产品中,就属于这种情况。认为:Atlassian人群。
也很受欢迎:您想使用OAuth2或“使用Google / Twitter / etc登录”。(OpenID),可能与JWT结合使用。然后,以下任何一项都不适用,您应该直接进入OAuth2一章。
注意:根据您的情况,您需要指定不同的@Beans来使Spring Security正常工作,否则最终将得到非常令人困惑的异常(例如,如果您忘记指定PasswordEncoder,则为NullPointerException)。请记在脑子里。
让我们看一下前两种情况。
假设您有一个存储用户的数据库表。它有几列,但最重要的是,它有一个用户名和密码列,您可以在其中存储用户的已加密(!)密码。
create table users (id int auto_increment primary key, username varchar(255), password varchar(255));
在这种情况下,Spring Security需要您定义两个bean以启动并运行身份验证。
一个UserDetailsService。
密码编码器。
指定UserDetailsService就是这样简单:
@Bean
public UserDetailsService userDetailsService() {
return new MyDatabaseUserDetailsService();
}
public class MyDatabaseUserDetailsService implements UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. Load the user from the users table by username. If not found, throw UsernameNotFoundException.
// 2. Convert/wrap the user to a UserDetails object and return it.
return someUserDetails;
}
}
public interface UserDetails extends Serializable {
String getUsername();
String getPassword();
// <3> more methods:
// isAccountNonExpired,isAccountNonLocked,
// isCredentialsNonExpired,isEnabled
}
UserDetailsService通过用户的用户名加载UserDetails。请注意,该方法仅采用一个参数:用户名(而不是密码)。
UserDetails界面具有获取(加密!)密码的方法和获取用户名的方法。
UserDetails拥有更多方法,例如帐户处于活动状态或被阻止,凭据已过期或用户具有什么权限-但是我们不在此介绍。
因此,您可以像上面一样自行实现这些接口,也可以使用Spring Security提供的现有接口。
现成的实现
简要说明一下:您始终可以自己实现UserDetailsService和UserDetails接口。
但是,您还会发现Spring Security提供的现成的实现,您可以改用/ configure / extend / override。
JdbcUserDetailsManager,这是一个基于JDBC(数据库)的UserDetailsService。您可以配置它以匹配您的用户表/列结构。
InMemoryUserDetailsManager,它将所有用户详细信息保留在内存中,非常适合测试。
org.springframework.security.core.userdetail.User,这是您可以使用的明智的默认UserDetails实现。这意味着您可能在实体/数据库表与该用户类之间进行映射/复制。或者,您可以简单地使您的实体实现UserDetails接口。
完整的UserDetails工作流程:HTTP基本身份验证
现在回想一下您的HTTP基本身份验证,这意味着您正在使用Spring Security和Basic Auth保护您的应用程序的安全。当您指定UserDetailsService并尝试登录时,将发生以下情况:
从过滤器中的HTTP Basic Auth标头中提取用户名/密码组合。您无需为此做任何事情,它会在后台进行。
呼叫你的 MyDatabaseUserDetailsService加载从数据库中相应的用户,包装成一个UserDetails对象,它暴露了用户的加密密码。
从HTTP Basic Auth标头中提取提取的密码,对其进行自动加密,并将其与UserDetails对象中的加密密码进行比较。如果两者都匹配,则说明用户已成功通过身份验证。
这里的所有都是它的。但是等等,Spring Security 如何加密来自客户端的密码(步骤3)?用什么算法?
密码编码器
Spring Security无法神奇地猜测您首选的密码加密算法。这就是为什么您需要指定另一个@Bean,一个PasswordEncoder的原因。
例如,如果要对所有密码使用BCrypt加密(Spring Security的默认设置),则可以在SecurityConfig中指定此@Bean。
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
如果您有多个密码加密算法,因为您有一些旧用户的密码是用MD5存储的(不这样做),而较新的用户使用Bcrypt甚至是第三个算法(如SHA-256),该怎么办?然后,您将使用以下编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
该委托编码器如何工作?它将查看UserDetail的加密密码(例如来自数据库表),该密码现在必须以开头{prefix}
。该前缀是您的加密方法!您的数据库表将如下所示:
用户名 |
密码 |
|
{bcrypt} $ 2y $ 12 $ 6t86Rpr3llMANhCUt26oUen2WhvXr / A89Xo9zJion8W7gWgZ / zA0C |
||
{sha256} 5ffa39f5757a0dad5dfada519d02c6b71b61ab1df51b4ed1f3beed6abe0ff5f6 |
Spring Security将:
读入这些密码并删除前缀({bcrypt}或{sha256})。
根据前缀值,使用正确的PasswordEncoder(即BCryptEncoder或SHA256Encoder)
使用该PasswordEncoder加密传入的未加密密码,并将其与存储的密码进行比较。
这就是PasswordEncoders的全部内容。
摘要:有权访问用户的密码
本部分的要点是:如果您正在使用Spring Security并有权访问用户的密码,则:
指定一个UserDetailsService。定制实现或使用并配置Spring Security提供的实现。
指定一个PasswordEncoder。
简而言之,就是Spring Security认证。
现在,假设您正在使用Atlassian Crowd进行集中身份管理。这意味着所有应用程序的所有用户和密码都存储在Atlassian Crowd中,而不再存储在数据库表中。
这有两个含义:
您的应用程序中不再有用户密码,因为您不能要求Crowd仅给您这些密码。
但是,您确实拥有可以使用您的用户名和密码登录的REST API。(对/rest/usermanagement/1/authentication
REST端点的POST请求)。
在这种情况下,您不能再使用UserDetailsService,而需要实现并提供AuthenticationProvider @Bean。
@Bean
public AuthenticationProvider authenticationProvider() {
return new AtlassianCrowdAuthenticationProvider();
}
AuthenticationProvider主要由一种方法组成,并且一个简单的实现可能看起来像这样:
public class AtlassianCrowdAuthenticationProvider implements AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
User user = callAtlassianCrowdRestService(username, password);
if (user == null) {
throw new AuthenticationException("could not login");
}
return new UserNamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
}
// other method ignored
}
与只能访问用户名的UserDetails load()方法相比,现在可以访问完整的身份验证尝试,通常包含用户名和密码。
您可以执行任何想要验证用户身份的操作,例如,调用REST服务。
如果身份验证失败,则需要引发异常。
如果身份验证成功,则需要返回一个完全初始化的UsernamePasswordAuthenticationToken。它是Authentication接口的一种实现,需要将authenticated字段设置为true(上面使用的构造函数将自动设置该字段)。我们将在下一章介绍有关权限。
完整的AuthenticationProvider工作流程:HTTP基本身份验证
现在回想一下您的HTTP基本身份验证,这意味着您正在使用Spring Security和Basic Auth保护您的应用程序的安全。当您指定AuthenticationProvider并尝试登录时,将发生以下情况:
从过滤器中的HTTP Basic Auth标头中提取用户名/密码组合。您无需为此做任何事情,它会在后台进行。
使用该用户名和密码调用您的 AuthenticationProvider(例如AtlassianCrowdAuthenticationProvider),以便您自己进行身份验证(例如REST调用)。
没有进行密码加密或类似操作,因为您实际上是委派第三方进行实际的用户名/密码检查。简而言之,这就是AuthenticationProvider身份验证!
摘要:AuthenticationProvider
本部分的要点是:如果您使用的是Spring Security,但没有访问用户密码的权限,则请实现并提供AuthenticationProvider @Bean。
到目前为止,我们仅讨论了身份验证,例如用户名和密码检查。
现在让我们看一下权限,或者说Spring Security中的角色和权限。
以典型的电子商务网上商店为例。它可能由以下部分组成:
网上商店本身。假设其网址为www.youramazinshop.com
。
也许是呼叫中心代理的区域,他们可以在其中登录并查看客户最近购买的商品或包裹的位置。其网址可能是www.youramazinshop.com/callcenter
。
一个单独的管理区域,管理员可以在其中登录和管理呼叫中心代理或网上商店的其他技术方面(例如主题,性能等)。其网址可能是www.youramazinshop.com/admin
。
这具有以下含义,因为仅对用户进行身份验证已不再足够:
客户显然不应该能够访问呼叫中心或管理区域。只允许他在网站上购物。
呼叫中心代理应无法访问管理区域。
管理员可以访问网上商店,呼叫中心区域和管理员区域。
简而言之,您要允许不同的用户根据其权限或角色进行不同的访问。
简单:
权限(以最简单的形式)只是一个字符串,它可以是:user,ADMIN,ROLE_ADMIN或53cr37_r0l3。
角色是具有ROLE_
前缀的授权。因此,称为的角色ADMIN
与称为的授权相同ROLE_ADMIN
。
角色和权限之间的区别纯粹是概念上的,这常常使Spring Security的新手感到困惑。
老实说,我已经阅读了Spring Security文档以及有关此问题的几个相关StackOverflow线程,我无法给您一个明确的令人满意的答案。
当然,Spring Security并不能让您仅使用String 来摆脱困境。有一个Java类代表您的权限String,一种流行的类型是SimpleGrantedAuthority。
public final class SimpleGrantedAuthority implements GrantedAuthority {
private final String role;
@Override
public String getAuthority() {
return role;
}
}
(注意:还有其他授权类,可以让您在字符串旁边存储其他对象(例如,主体),这里我不会覆盖它们。现在,我们仅使用SimpleGrantedAuthority。)
假设您将用户存储在自己的应用程序中(认为:UserDetailsService),您将拥有一个Users表。
现在,您只需在其中添加一个名为“ authorities”的列即可。对于本文,我在这里选择了一个简单的字符串列,尽管它可以包含多个逗号分隔的值。另外,我也可以有一个完全独立的表AUTHORITIES,但是在本文的范围内,可以这样做。
注意:再次参考section_title:您将权限(即字符串)保存到数据库。这些权限恰巧以ROLE_前缀开头,因此,就Spring Security而言,这些权限也是角色。
用户名 |
密码 |
当局 |
|
{bcrypt} ... |
ROLE_ADMIN |
||
{sha256} ... |
ROLE_CALLCENTER |
剩下要做的唯一一件事情就是调整UserDetailsService以在该权限列中读取。
public class MyDatabaseUserDetailsService implements UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
List<SimpleGrantedAuthority> grantedAuthorities = user.getAuthorities().map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}
您只需将数据库列内的任何内容映射到SimpleGrantedAuthorities列表。做完了
同样,我们在这里使用Spring Security的UserDetails的基本实现。您也可以使用自己的类在此处实现UserDetails,甚至不必进行映射。
当用户来自第三方应用程序(例如Atlassian Cloud)时,您需要找出他们用于支持权限的概念。Atlassian Crowd具有“角色”的概念,但不赞成使用“组”。
因此,根据您使用的实际产品,您需要在AuthenticationProvider中将此映射到Spring Security授权。
public class AtlassianCrowdAuthenticationProvider implements AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
atlassian.crowd.User user = callAtlassianCrowdRestService(username, password);
if (user == null) {
throw new AuthenticationException("could not login");
}
return new UserNamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), mapToAuthorities(user.getGroups()));
}
// other method ignored
}
注意:这不是实际的 Atlassian Crowd代码,但可以达到目的。您针对REST服务进行身份验证,并获取一个JSON User对象,然后将其转换为atlassian.crowd.User对象。
该用户可以是一个或多个组的成员,这里假定只是字符串。然后,您可以简单地将这些组映射到Spring的“ SimpleGrantedAuthority”。
到目前为止,我们已经讨论了很多有关在Spring Security中为经过身份验证的用户存储和检索授权的信息。但是,如何通过Spring Security的DSL 保护具有不同权限的URL?简单:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin").hasAuthority("ROLE_ADMIN")
.antMatchers("/callcenter").hasAnyAuthority("ROLE_ADMIN", "ROLE_CALLCENTER")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
要访问该/admin
区域,您(即用户)需要经过认证并且具有权限(简单字符串)ROLE_ADMIN。
要访问该/callcenter
区域,您需要进行身份验证并具有权限ROLE_ADMIN 或 ROLE_CALLCENTER。
对于任何其他请求,您不需要特定角色,但仍需要进行身份验证。
请注意,上面的代码(1,2)等效于以下代码:
http
.authorizeRequests()
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/callcenter").hasAnyRole("ADMIN", "CALLCENTER")
现在,您无需调用“ hasAuthority”,而是调用“ hasRole”。注意:Spring Security将寻找ROLE_ADMIN
对经过身份验证的用户调用的权限。
现在,您无需调用“ hasAnyAuthority”,而是调用“ hasAnyRole”。注意:Spring Security将在经过身份验证的用户上查找称为ROLE_ADMIN
或ROLE_CALLCENTER
的授权。
最后但并非最不重要的是,配置授权最强大的方法是使用访问方法。它使您几乎可以指定任何有效的SpEL表达式。
http
.authorizeRequests()
.antMatchers("/admin").access("hasRole('admin') and hasIpAddress('192.168.1.0/24') and @myCustomBean.checkAccess(authentication,request)")
要全面了解Spring的基于表达式的访问控制的功能,请查看官方文档。
Spring Security可帮助您防御多种常见攻击。它从定时攻击开始(即,即使用户不存在,Spring Security也会始终在登录时对提供的密码进行加密),最后是针对高速缓存控制攻击,内容嗅探,点击劫持,跨站点脚本编写等的防护。
在本指南的范围内,不可能详细介绍每种攻击。因此,我们将只关注使大多数Spring Security新手失去最大支持的一种保护:跨站点请求伪造。
如果您不熟悉CSRF,则可能需要观看此YouTube视频,以快速掌握它。但是,快速的收获是,默认情况下, Spring Security使用有效的CSRF令牌保护所有传入的POST(或PUT / DELETE / PATCH)请求。
这意味着什么?
CSRF和服务器端呈现的HTML
想象一下与此有关的银行转帐表格或任何表格(例如登录表格),这些表格将由您的@Controllers在模板技术(如Thymeleaf或Freemarker)的帮助下呈现。
启用Spring Security后,您将无法再提交该表单。因为Spring Security的CSRFFilter会在任何POST(PUT / DELETE)请求中寻找其他隐藏参数:所谓的CSRF令牌。
默认情况下,它会在每个HTTP会话中生成此类令牌并将其存储在该令牌中。而且,您需要确保将其注入到任何HTML表单中。
CSRF令牌和Thymeleaf
由于Thymeleaf与Spring Security(与Spring Boot结合使用)具有良好的集成,因此您只需将以下代码段添加到任何表单中,即可从会话中自动将令牌注入到表单中。更好的是,如果您在表单中使用“ th:action”,则Thymeleaf会自动为您注入该隐藏字段,而无需手动进行。
在这里,我们正在手动添加CSRF参数。
在这里,我们使用Thymeleaf的表单支持。
注意:有关Thymeleaf的CSRF支持的更多信息,请参阅官方文档。
CSRF和其他模板库
我不能在本节中介绍所有模板库,但作为最后一招,您始终可以将CSRFToken注入任何@Controller方法中,并将其简单地添加到模型中以在视图中呈现它,或直接将其作为HttpServletRequest请求属性访问。
@Controller
public class MyController {
@GetMaping("/login")
public String login(Model model, CsrfToken token) {
// the token will be injected automatically
return "/templates/login";
}
}
CSRF&React或Angular
对于Javascript应用(例如React或Angular单页应用),情况有所不同。这是您需要做的:
配置Spring Security以使用CookieCsrfTokenRepository,它将把CSRFToken放入cookie“ XSRF-TOKEN”(并将其发送到浏览器)。
使您的Javascript应用采用该Cookie值,并在每个POST(/ PUT / PATCH / DELETE)请求中将其作为“ X-XSRF-TOKEN” 标头发送。
有关完整的复制粘贴React示例,请查看以下出色的博客文章:https : //developer.okta.com/blog/2018/07/19/simple-crud-react-and-spring-boot。
禁用CSRF
如果仅提供CSRF保护没有任何意义的无状态REST API,则将完全禁用CSRF保护。这是您的操作方式:
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
}
坏蛋!本部分只是下一篇文章的预告片:Spring Security&OAuth2。为什么?
因为Spring Security的OAuth2集成实际上是一个复杂的主题,并且足以容纳另外7,000-10,000个字,这不属于本文的范围。
敬请关注。
对于本文的大部分内容,您仅在应用程序的Web层上指定了安全配置。您通过WebSecurityConfigurerAdapter的DSL使用antMatcher或regexMatchers保护了某些URL。这是完美的安全标准方法。
除了保护您的Web层,还有“深度防御”的想法。这意味着除了保护URL外,您可能还希望保护业务逻辑本身。想想:您的@ Controllers,@ Components,@ Services甚至@Repositories。简而言之,就是您的Spring bean。
该方法将被调用method security
并通过注释工作,您基本上可以将其放置在Spring bean的任何公共方法上。您还需要通过将@EnableGlobalMethodSecurity批注放在ApplicationContextConfiguration上来显式启用方法安全性。
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class YourSecurityConfig extends WebSecurityConfigurerAdapter{
}
prePostEnabled属性启用对Spring @PreAuthorize
和@PostAuthorize
批注的支持。支持意味着,除非将标志设置为true,否则Spring将忽略此注释。
secureEnabled属性启用对@Secured
注释的支持。支持意味着,除非将标志设置为true,否则Spring将忽略此注释。
jsr250Enabled属性启用对@RolesAllowed
注释的支持。支持意味着,除非将标志设置为true,否则Spring将忽略此注释。
@Secured和@RolesAllowed基本相同,尽管@Secured是Spring特定的批注,带有spring-security-core依赖项,而@RolesAllowed是标准化的批注,位于javax.annotation-api依赖项中。这两个注释都将一个授权/角色字符串作为值。
@ PreAuthorize / @ PostAuthorize也是(较新的)Spring特定注释,并且比上述注释更强大,因为它们不仅可以包含授权/角色,还可以包含任何有效的SpEL表达式。
最后,AccessDeniedException
如果您尝试访问权限/角色不足的受保护方法,则所有这些注释都将引发。
因此,让我们最后来看一下这些注解。
@Service
public class SomeService {
@Secured("ROLE_CALLCENTER")
// == @RolesAllowed("ADMIN")
public BankAccountInfo get(...) {
}
@PreAuthorize("isAnonymous()")
// @PreAuthorize("#contact.name == principal.name")
// @PreAuthorize("ROLE_ADMIN")
public void trackVisit(Long id);
}
}
如前所述,@ Secured将权限/角色作为参数。@RolesAllowed,同样。注意:请记住,这@RolesAllowed("ADMIN")
将检查授予的权限ROLE_ADMIN
。
如前所述,@ PreAuthorize接受权限,但也接受任何有效的SpEL表达式。有关像isAnonymous()
上面这样的常见内置安全表达式的列表,而不是编写自己的SpEL表达式,请查看官方文档。
这主要是一个同质性问题,而不是过多地将自己与Spring特定的API捆绑在一起(通常会提出这种观点)。
如果使用@Secured,请坚持使用它,不要在28%的其他bean中跳到@RolesAllowed注释,以实现标准化,但切勿完全通过。
首先,您可以始终使用@Secured并在需要时立即切换到@PreAuthorize。
至于与Spring WebMVC的集成,Spring Security允许您做几件事:
除了antMatchers和regexMatchers,您还可以使用mvcMatchers。区别在于,尽管antMatchers和regexMatchers基本上将URI字符串与通配符匹配,但mvcMatchers的行为与@RequestMappings 完全相同。
将您当前已验证的主体注入@ Controller / @ RestController方法。
将当前会话CSRFToken注入@ Controller / @ RestController方法。
正确处理异步请求处理的安全性。
@Controller
public class MyController {
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser, CsrfToken token) {
// .. find messages for this user and return them ...
}
}
如果对用户进行了身份验证,则@AuthenticationPrincipal将注入主体;如果没有对用户进行身份验证,则将注入null。该主体是来自UserDetailsService / AuthenticationManager的对象!
或者,您可以将当前会话CSRFToken注入每个方法中。
如果不使用@AuthenticationPrincipal批注,则必须通过SecurityContextHolder自己获取主体。在旧版Spring Security应用程序中经常看到的一种技术。
@Controller
public class MyController {
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(CsrfToken token) {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
CustomUser customUser = (CustomUser) authentication.getPrincipal();
// .. find messages for this user and return them ...
}
// todo
}
}
每当您将spring-boot-starter-security依赖项添加到Spring Boot项目中时,Spring Boot确实只会为您预先配置Spring Security。
除此之外,所有安全性配置都是通过普通的Spring Security概念(例如WebSecurityConfigurerAdapter,身份验证和授权规则)完成的,而这些概念本身与Spring Boot无关。
因此,您在本指南中阅读的所有内容都将1:1应用于将Spring Security与Spring Boot结合使用。而且,如果您不了解一般的安全性,请不要期望正确地了解这两种技术如何协同工作。
Spring Security与Thymeleaf集成良好。它提供了特殊的Spring Security Thymeleaf方言,可让您将安全性表达式直接放入Thymeleaf HTML模板中。
sec:authorize="isAuthenticated()">
This content is only shown to authenticated users.
sec:authorize="hasRole('ROLE_ADMIN')">
This content is only shown to administrators.
sec:authorize="hasRole('ROLE_USER')">
This content is only shown to users.
有关这两种技术如何协同工作的完整且更详细的概述,请参阅官方文档。
截至2020年4月,即{springsecurityversion}。
请注意,如果您使用的是Spring Boot定义的Spring Security依赖关系,则可能使用的是较旧的Spring Security版本,如5.2.1。
Spring Security最近已经发生了相当大的变化。因此,您需要找到目标版本的迁移指南并逐步进行操作:
Spring Security 3.x至4.x→ https://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-jc.html
Spring Security 4.x到5.x(<5.3)→ https://docs.spring.io/spring-security/site/docs/5.0.15.RELEASE/reference/htmlsingle/#new(不是真正的指南,但有新消息)
Spring Security 5.x至5.3→ https://docs.spring.io/spring-security/site/docs/5.3.1.RELEASE/reference/html5/#new(不是真正的指南,而是新功能)
平原Spring项目
如果您使用的是普通的Spring项目(不是 Spring Boot),则需要向项目中添加以下两个Maven / Gradle依赖项:
org.springframework.security
spring-security-web
5.2.2.RELEASE
org.springframework.security
spring-security-config
5.2.2.RELEASE
您还需要在web.xml或Java配置中配置SecurityFilterChain。在这里查看如何进行。
Spring Boot启动项目
如果您正在使用Spring Boot项目,则需要在项目中添加以下Maven / Gradle依赖项:
org.springframework.boot
spring-boot-starter-security
其他所有内容将自动为您配置,您可以立即开始编写WebSecurityConfigurerAdapter。
如本文所述,Spring Security将当前已通过身份验证的用户(或更确切地说是SecurityContext)存储在SecurityContextHolder内部的线程局部变量中。您可以这样访问它:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
请注意,如果您未登录,Spring Security 默认情况下会AnonymousAuthenticationToken
在SecurityContextHolder上设置一个身份验证。这会引起一些混乱,因为人们自然会期望在那里有一个null值。
一个无意义的示例,显示了最有用的antMatcher(和regexMatcher / mvcMatcher)可能性:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/user/**", "/api/ticket/**", "/index").hasAuthority("ROLE_USER")
.antMatchers(HttpMethod.POST, "/forms/**").hasAnyRole("ADMIN", "CALLCENTER")
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().ignoringAntMatchers("/api/**");
}
如果到目前为止,您已经了解了Spring Security生态系统的复杂性,即使没有OAuth2也是如此。总结一下:
如果您对Spring Security的FilterChain的工作原理及其默认的漏洞利用保护有什么基本的了解(请考虑:CSRF),它会有所帮助。
确保了解身份验证和授权之间的区别。另外,还需要为特定的身份验证工作流程指定@Beans。
确保您了解Spring Security的WebSecurityConfigurerAdapter的DSL以及基于注释的方法安全性。
最后但并非最不重要的一点是,它有助于仔细检查Spring Security与其他框架和库(例如Spring MVC或Thymeleaf)的集成。
今天足够了,因为那是一个很大的旅程,不是吗?谢谢阅读!
原文链接:https://dev.to//marcobehler/spring-security-authentication-and-authorization-in-depth-m9g