本章是Spring Security理论和概念的东西,没有实际的搭建产出,为下节做个铺垫。只是介绍Spring Security一些核心要用到的东西,Spring Security的功能还是很强大的,有兴趣可以系统的学习和了解
历史遗留TODO:
- 第四章
- 登录日志还未实现。(到登录和权限模块完成)
LogAspect
从缓存获取当前的用户信息使用模拟的数据(到登录和权限模块完成)本章将留下TODO:
- 无
本章将解决TODO:
- 无
Spring Security
:是一个能够为基于Spring
的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
具有以下优点:
UserDetailsService
:该方法很容易理解: 通过用户名来加载用户 。这个方法主要用于从系统数据中查询并加载具体的用户到 Spring Security中。
在开发中我们一般定义一个这个接口的实现类,自定义loadUserByUsername
方法,实现从数据源获取用户,加载用户信息。也可以在其中实现一些校验用户逻辑。
例如:
自己Test中的例子:
若依中的使用:
UserDetails
:从上面 UserDetailsService
可以知道最终交给Spring Security的是UserDetails
。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象 Authentication
中去。
UserDetails
中默认提供:
在我们自己的项目中,我们要定义个用户类实现该接口,在该用户类中我们可以扩展更多的用户信息,比如手机、邮箱等等
UserDetailsServiceAutoConfiguration
:(若依中未使用)
源码:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class},
type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector"}
)
public class UserDetailsServiceAutoConfiguration {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
public UserDetailsServiceAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
)
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
}
private String getOrDeducePassword(User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}
}
我们来简单解读一下该类,从 @Conditional
系列注解我们知道,该类在类路径下存在 AuthenticationManager
或者在Spring 容器中存在Bean ObjectPostProcessor
并且不存在Bean AuthenticationManager
, AuthenticationProvider
, UserDetailsService
的情况下生效。 千万 不要纠结这些类干嘛用的! 该类只初始化了一个 UserDetailsManager
类型的Bean。 UserDetailsManager
类型负责对安全用户实体抽象 UserDetails
的增删查改操作。同时还继承了 UserDetailsService
接口。
明白了上面这些让我们把目光再回到 UserDetailsServiceAutoConfiguration
上来。该类初始化了 一个名为 InMemoryUserDetailsManager
的内存用户管理器。该管理器通过配置注入了一个默认的 UserDetails
存在内存中,在项目使用中就是我们上面自己实现的继承UserDetails
的User类 ,每次启动 user 都是动态生成的。
我们定义自己的 UserDetailsManager
Bean就可以实现我们需要的用户管理逻辑
Spring Secutity提供了JdbcUserDetailsManager
,该类继承 UserDetailsManager
实现基于JDBC的用户管理逻辑。
(但是如果如此使用,就相当于UserDetails
对应一个数据库的表,但它其实不是一个实体类(enyity),只是一个业务对象(bo)。实际使用中我们还是有个User类对应数据库表,通过User类的Service操作用户数据)
PasswordEncoder
接口,并且有多个代表用不同加密算法的实现类默认的加密算法是bcrypt
,对应BCryptPasswordEncoder
。
DelegatingPasswordEncoder
是委托密码编码器。所含内容如图:
idForEncode
:通过id来匹配编码器,该id不能是 {} 包括的
DelegatingPasswordEncoder:
:初始化传入,用来提供默认的密码编码器。
passwordEncoderForEncode
:通过上面 idForEncode
所匹配到的 PasswordEncoder
用来对密码进行编码 。
upgradeEncoding
密码升级。
idToPasswordEncoder
:用来维护多个 idForEncode
与具体 PasswordEncoder
的映射关系。 DelegatingPasswordEncoder
初始化时 装载进去,会在初始化时进行一些规则校验。
PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder()
:默认的密码匹配器,上面的 Map idToPasswordEncoder
中都不存在就用它来执行 matches 方法进行匹配验证。这是一个内部类实现。
encode
编码方法
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
从上面源码可以看出来通过 DelegatingPasswordEncoder
编码后的密码是遵循一定的规则的,遵循 {idForEncode}encodePassword
。也就是前缀 {} 包含了编码的方式再拼接上该方式编码后的密码串。注意:对相同字符串每次加密生成的结果都不同!
例如:(12345678用bcrypt算法编码后的结果:)
12345678 --(bcrypt)--> {bcrypt}$2a$10$XEBcwHqwLYXrbyqN2r9T2..dTmpv23RKi4SxAc4vyyt7ZZh30slAy
```
matches
:密码匹配方法:
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
密码匹配通过传入原始密码和遵循 {idForEncode}encodePassword
规则的密码编码串。通过获取编 码方式id (idForEncode
) 来从 DelegatingPasswordEncoder
中的映射集合 idToPasswordEncode
中获取具体的 PasswordEncoder
进行匹配校验。找不到就使用 UnmappedIdPasswordEncoder
。
PasswordEncoderFactories
密码器静态工厂制造 PasswordEncoder
。而且还是个静态工厂只提供了初始化 DelegatingPasswordEncoder
的方法。该类的主要方法:
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
从上面可以非常具体地看出来 DelegatingPasswordEncoder
提供的密码编码方式。默认采用了 bcrypt
进行编码。
因此,在 DelegatingPasswordEncoder
中也可以实现新旧密码加密校验方式同时存在,并且实现自动更新老密码的编码方式。
例如:旧密码编码方式是SHA-1
,新的方式是bcrypt
。这时候验证时,对于未登录的过的密码用新编码方式编然后存入数据库;老用户在数据库中存储的密码是用老编码方式编码的,这时候校验时,可以用老编码方式校验,成功验证后,把老密码再用新编码方式存入数据库。
示例:
SpringBootWebSecurityConfiguration
:@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
public class SpringBootWebSecurityConfiguration {
public SpringBootWebSecurityConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@Order(2147483642)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
DefaultConfigurerAdapter() {
}
}
}
这个类是Spring Security 对 Spring Boot Servlet Web 应用的默认配置。核心在于 WebSecurityConfigurerAdapter 适配器。从 @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
我们就能看出 WebSecurityConfigurerAdapter
是安全配置的核心。 默认情况下 DefaultConfigurerAdapter
将以 SecurityProperties.BASIC_AUTH_ORDER (-5 )
的顺序注入 Spring IoC 容器,这是个空实现。 如果我们需要实现对Spring Security的配置可以通过继承 WebSecurityConfigurerAdapter
来实现。
在若依中:
SecurityConfig
上面也提到首先要继承WebSecurityConfigurerAdapter
,其次最常用的是实现configure(AuthenticationManagerBuilder auth)
、configure(WebSecurity web)
、configure(HttpSecurity http)
三个方法实现我们对Spring Security的自定义安全配置。
void configure(AuthenticationManagerBuilder auth)
用来配置认证管理器 AuthenticationManager
。
void configure(WebSecurity web)
用来配置 WebSecurity
。而 WebSecurity
是基于Servlet Filter
用来配置 springSecurityFilterChain
。而 springSecurityFilterChain
又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy
。 相关逻辑你可以在 WebSecurityConfiguration
中找到。我们一般不会过多来自定义 WebSecurity
, 使用较多的使其 ignoring()
方法用来忽略 Spring Security 对静态资源的控制。
void configure(HttpSecurity http)
这个是我们使用最多的,用来配置 HttpSecurity
。 HttpSecurity
用于构建一个安全过滤器链 SecurityFilterChain
。 SecurityFilterChain
最终 被注入核心过滤器 。HttpSecurity
有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略。
(看mk网教程的一张图)
UsernamePasswordAuthenticationFilter
:它的作用是拦截登录请求并获取账号和 密码,然后把账号密码封装到认证凭据 UsernamePasswordAuthenticationToken
中,然后把凭据交 给特定配置的 AuthenticationManager
去作认证。 (图片来源《Spring Security实战干货》)
理解了 UsernamePasswordAuthenticationFilter
工作流程后可以做这些事情:
定制我们的登录请求URI和请求方式。
登录请求参数的格式定制化,比如可以使用 JSON格式提交甚至几种并存。
将用户名和密码封装入凭据 UsernamePasswordAuthenticationToken
,定制业务场景需要的特殊凭据。
AuthenticationManager
:这个接口方法非常奇特,入参和返回值的类型都是 Authentication
。该接口的作用是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常AuthenticationException
。认证过程:
AuthenticationManager
的实现 ProviderManager
管理了众多的 AuthenticationProvider
。每 一个 AuthenticationProvider
都只支持特定类型的 Authentication
,然后是对适配到的 Authentication
进行认证,只要有一个 AuthenticationProvider
认证成功,那么就认为认证成功,所有的都没有通过才认为是认证失败。认证成功后的 Authentication
就变成授信凭据,并触发认证成功的事件。认证失败的就抛出异常触发认证失败的事件。
认证管理器 AuthenticationManager
针对特定的 Authentication
提供了特定的 认证功能,我们可以借此来实现多种认证并存(多因子登录)。
Spring Security 以一个单 Filter(FilterChainProxy)
存在于整个过滤器链中,而 这个 FilterChainProxy
实际内部代理着众多的 Spring Security Filter 。
Spring Security 内置过滤器:
ChannelProcessingFilter
:通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http 协议,哪些请求随便用哪个协议都行。ConcurrentSessionFilter
:主要用来判断 session 是否过期以及更新最新的访问时间。 WebAsyncManagerIntegrationFilter
:用于集成SecurityContext到Spring异步执行机制中的 WebAsyncManager。用来处理异步请求的安全上下文。SecurityContextPersistenceFilter
:主要控制 SecurityContext 的在一次请求中的生命周期 。 请求来临时,创建 SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder 。 HeaderWriterFilter
: HeaderWriterFilter
用来给 http 响应添加一些 Header ,比如 X-Frame-Options , X-XSS- Protection ,X-Content-Type-Options 。CorsFilter
:跨域相关的过滤器。这是 Spring MVC Java 配置和 XML 命名空间 CORS 配置的替代方法, 仅对依赖 于 spring-web 的应用程序有用(不适用于 spring-webmvc )或 要求在 javax.servlet.Filter 级别 进行CORS检查的安全约束链接。CsrfFilter
:用于防止 csrf 攻击,前后端使用json交互需要注意的一个问题。LogoutFilter
:处理注销的过滤器。OAuth2AuthorizationRequestRedirectFilter
:这个需要依赖 spring-scurity-oauth2
相关的模块。该过滤器是处理 OAuth2 请求首选重定向相关逻辑的。Saml2WebSsoAuthenticationRequestFilter
:这个需要用到 Spring Security SAML
模块,这是一个基于SMAL
的SSO
单点登录请求认证过滤器。 X509AuthenticationFilter
: X509
认证过滤器。AbstractPreAuthenticatedProcessingFilter
: 处理经过预先认证的身份验证请求的过滤器的 基类其中认证主体已经由外部系统进行了身份验证。目的只是从传入请求中提取主体上的必要信息, 而不是对它们进行身份验证。可以继承该类来具体实现并通过HttpSecurity.addFilter
方法来添加自定义 AbstractPreAuthenticatedProcessingFilter
。CasAuthenticationFilter
: CAS
单点登录认证过滤器 。依赖 Spring Security CAS
模块。OAuth2LoginAuthenticationFilter
:这个需要依赖 spring-scurity-oauth2
相关的模块。 OAuth2 登录认证过滤器。处理通过 OAuth2 进行认证登录的逻辑。Saml2WebSsoAuthenticationFilter
:这个需要用到 Spring Security SAML
模块,这是一个基于 SMAL
的SSO 单点登录认证过滤器。UsernamePasswordAuthenticationFilter
:处理用户以及密码认证的核心过滤器。认证请求提交的 username 和 password ,被封装成 token 进行一系列的认证,便是主要通过这个过滤器完成的,在 表单认证的方法中,这是最最关键的过滤器。OpenIDAuthenticationFilter
:基于 OpenID
认证协议的认证过滤器。 你需要在依赖中依赖额外的相关模块才能启用它。DefaultLoginPageGeneratingFilter
:生成默认的登录页。默认 /login
。 DefaultLogoutPageGeneratingFilter
:生成默认的退出页。 默认/logout
。DigestAuthenticationFilter
: Digest
身份验证是 Web 应用程序中流行的可选的身份验证机制 。 DigestAuthenticationFilter
能够处理 HTTP 头中显示的摘要式身份验证凭据。BasicAuthenticationFilter
: Digest
身份验证是 Web 应用程序中流行的可选的身份验证机制 。验证处理Basic Auth HTTP头中的数据。RequestCacheAwareFilter
:用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到 认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请 求。SecurityContextHolderAwareRequestFilter
:用来 实现 j2ee
中 Servlet Api
一些接口方法, 比如 getRemoteUser
方法、isUserInRole
方法, 在使用 Spring Security 时其实就是通过这个过滤器来实现的。JaasApiIntegrationFilter
:适用于 JAAS
(Java 认证授权服务)。 如果 SecurityContextHolder
中拥有的 Authentication
是一个JaasAuthenticationToken
,那么该 JaasApiIntegrationFilter
将使用包含在 JaasAuthenticationToken
中的 Subject
继续执行 FilterChain
。RememberMeAuthenticationFilter
: 处理 记住我 功能的过滤器。AnonymousAuthenticationFilter
:匿名认证过滤器。 对于 Spring Security 来说,所有对资源的访问都是有 Authentication
的。对于无需登录(UsernamePasswordAuthenticationFilter
)直接可以访问的资源,会授予其匿名用 户身份 。SessionManagementFilter
: Session 管理器过滤器,内部维护了一个SessionAuthenticationStrategy
用于管理 Session 。ExceptionTranslationFilter
:主要来传输异常事件。FilterSecurityInterceptor
:这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什 么样的角色和权限?这些判断和处理都是由该类进行的。SwitchUserFilter
: 用来做账户切换的。默认的切换账号的 url 为 /login/impersonate ,默认注 销切换账号的 url 为 /logout/impersonate ,默认的账号参数为 username 。向项目中添加过滤器:
在配置文件的configure(HttpSecurity httpSecurity)
方法中:
4个方法分别是:addFilter–添加过滤器;addFilterAfter–把过滤器添加到某过滤器之后;addFilterAt–替代某过滤器;addFilterBefore–把过滤器添加到某过滤器之前
一、基于配置表达式控制 URL 路径
在继承WebSecurityConfigurerAdapter
的配置类中的configure(HttpSecurity http)
中进行配置。
例如:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasAnyRole("admin", "user")
.anyRequest().authenticated()
.and()
...
}
常用的配置可选项:
表达式 | 备注 |
---|---|
hasRole | 用户具备某个角色即可访问资源 |
hasAnyRole | 用户具备多个角色中的任意一个即可访问资源 |
hasAuthority | 类似于 hasRole |
hasAnyAuthority | 类似于 hasAnyRole |
permitAll | 统统允许访问 |
denyAll | 统统拒绝访问 |
isAnonymous | 判断是否匿名用户 |
isAuthenticated | 判断是否认证成功 |
isRememberMe | 判断是否通过记住我登录的 |
isFullyAuthenticated | 判断是否用户名/密码登录的 |
principle | 当前用户 |
authentication | 从 SecurityContext 中提取出来的用户对象 |
二、基于注解的接口权限控制
我们可以在任何 @Configuration
实例上使用 @EnableGlobalMethodSecurity
注解来启用全局方 法安全注解功能
例如:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
...
}
prePostEnabled
为 true ,则开启了基于表达式 的方法安全控制。这个配置开启了四个注解,分别是:
@PreAuthorize
:在标记的方法调用之前,通过表达式来计算是否可以授权访问。示例:
@Service
public class TestService {
// 只有当前登录用户名为 Gangbb 的用户才可以访问该方法。
@PreAuthorize("principal.username.equals('Gangbb')")
public String hello() {
return "hello";
}
//用户名开头为 Gangbb 的用户才能访问。
@PreAuthorize("principal.username.startsWith('Gangbb')")
public String hello() {
return "hello";
}
// 访问该方法的用户必须具备 ROLE_ADMIN 角色。
@PreAuthorize("hasRole('ADMIN')")
public String admin() {
return "admin";
}
//*******************以下是基于SpEL 表达式****************
// 表示访问该方法的 age 参数必须大于 98,否则请求不予通过。
@PreAuthorize("#age>98")
public String getAge(Integer age) {
return String.valueOf(age);
}
// 入参id 必须同当前的用户名相同。
@PreAuthorize("#id.equals(principal.username)")
public String getId(Integer age) {
return String.valueOf(age);
}
//......更多关于SpEL 表达式可参考官方文档
}
@PostAuthorize
:在标记的方法调用之后,通过表达式来计算是否可以授权访问。该注解是针对 @PreAuthorize
。区别 在于先执行方法。而后进行表达式判断。如果方法没有返回值实际上等于开放权限控制;如果有返回值 实际的结果是用户操作成功但是得不到响应。
@PreFilter
:基于方法入参相关的表达式,对入参进行过滤。
@PostFilter
:和 @PreFilter
不同的是, 基于返回值相关的表达式,对返回值进行过滤。分页慎用! 该过程发生接口进行数据返回之前。
示例:
测试数据: [“Gangbb”, “GangAA”, “Hangbb”] 都有ROLE_ADMIN 角色
// filterObject表示要过滤的元素对象。 如下会过滤掉 Gangbb GangAA,只通过 Hangbb
@PreFilter(value = "filterObject.startsWith('G')",filterTarget = "ids")
// 如下配置都会通过。如果都没有ROLE_ADMIN 角色则过滤掉Hangbb
@PreFilter("hasRole('ADMIN') or filterObject.startsWith('H')")
// 对集合进行过滤,只返回后缀为 2 的元素
@PostFilter("filterObject.lastIndexOf('2')!=-1")
public List<String> getAllUser() {
List<String> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add("javaboy:" + i);
}
return users;
}
// 由于有两个集合,因此使用 filterTarget 指定过滤对象。
@PreFilter(filterTarget = "ages",value = "filterObject%2==0")
public void getAllAge(List<Integer> ages,List<String> users) {
System.out.println("ages = " + ages);
System.out.println("users = " + users);
}
设置 securedEnabled
为 true ,就开启了角色注解 @Secured
,该注解功能要简单的多,默认情况下只能基于角色(默认需要带前缀 ROLE_ )集合来进行访问控制决策。
该注解的机制是只要其声明的角色集合(value )中包含当前用户持有的任一角色就可以访问。也就是 用户的角色集合和@Secured
注解的角色集合要存在非空的交集。 不支持使用 SpEL
表达式进行决策。
设置 jsr250Enabled
为 true ,就开启了 JavaEE 安全 注解中的以下三个:
@DenyAll
拒绝所有的访问@PermitAll
同意所有的访问@RolesAllowed
用法和@Secured
一样。三、动态权限控制
这个若依中未有使用,也挺复杂,后续有时间再详细分析。