前言
Spring Security是一个功能强大且高度可定制的Java安全框架,主要负责为Java程序提供声明式的身份验证和访问控制。Spring Security具备足够的灵活性,可以选择不同的身份验证方式、授权方式、密码编码器等。并且集成了一系列安全措施,包括 XSS(Cross-Site Scripting)攻击防范、CSRF 攻击防范、点击劫持攻击防范。
原理
Spring Security 虽然听说很复杂,但本质上就是使用多个Filter组成FilterChain为基础实现认证与授权,是一种“责任链”设计模式
基本概念
- 认证(Authentication)的过程就是一个确定用户身份的过程。认证就是确定 你是谁?。
- 授权(Authorization):确定用户是否有权进行某个操作的过程。授权是确定 你有资格做什么?。
- 过滤器链(Filter Chain):在 Web 应用程序中,请求经过一系列过滤器处理后才能到达servlet。Spring Security 提供了一系列过滤器来处理认证、授权、防止 CSRF(Cross-Site Request Forgery)攻击等方面的问题。
- 安全上下文(Security Context):Spring Security 将安全相关的信息存储在一个安全上下文中,这个上下文包括当前用户的身份信息、所拥有的权限、会话信息等。
- UserDetails 和 UserDetailsService:UserDetails 是 Spring Security 中用于表示用户信息的接口,它包含了用户的用户名、密码和角色等信息。UserDetailsService 是用于加载 UserDetails 对象的接口,它通常从数据库中获取用户信息。
- AccessDecisionManager:AccessDecisionManager 是 Spring Security 中用于判断用户是否有权访问资源的接口,它通常使用 Access Control List(ACL)或 Role-Based Access Control(RBAC)等技术来进行权限管理。
认证
SecurityContextHolder 与 SecurityContext
SecurityContextHolder 是用于存放当前用户信息的地方;其内部包含一个值,该值通常被认为是当前登录用户。
SecurityContextHolder并不直接持有SecurityContext,而是通过策略模式,由SecurityContextHolderStrategy代为持有
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
由以下代码可知,SecurityContextHolder本质上就是我们平时使用的ThreadLocal
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
SecurityContext 中存储着当前认证用户的Principal、Credentials、Authorities。
默认的SecurityContext如下所示
public class SecurityContextImpl implements SecurityContext {
// ......
private Authentication authentication;
// ......
}
其中Authentication即为当前登录对象,UsernamePasswordAuthenticationToken就是最常用的实现类
package org.springframework.security.authentication;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} implementation that is
* designed for simple presentation of a username and password.
* <p>
* The <code>principal</code> and <code>credentials</code> should be set with an
* <code>Object</code> that provides the respective property via its
* <code>Object.toString()</code> method. The simplest such <code>Object</code> to use is
* <code>String</code>.
*
* @author Ben Alex
*/
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param principal
* @param credentials
* @param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
AuthenticationManager
AuthenticationManager 是定义 Spring Security 的 Filter 如何执行认证的API。返回的认证是由调用 AuthenticationManager 的控制器(即 Spring Security的 Filter 实例)在SecurityContextHolder 上设置的。如果你不与 Spring Security 的 Filter 实例集成,你可以直接设置 SecurityContextHolder,不需要使用 AuthenticationManager。
虽然 AuthenticationManager 的实现可以是任何东西,但最常见的实现是ProviderManager。
ProviderManager
ProviderManager内包含多个Provider用于认证,ProviderManager是最常用的AuthenticationManager的实现。ProviderManager 委托给一个 List[AuthenticationProvider]实例。每个 AuthenticationProvider 都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的 AuthenticationProvider 来决定。如果配置的 AuthenticationProvider 实例中没有一个能进行认证,那么认证就会以 ProviderNotFoundException 而失败,这是一个特殊的 AuthenticationException,表明 ProviderManager 没有被配置为支持被传入它的 Authentication 类型
同时,ProviderManager还可以为自己配置父级,当当前无法处理认证时,可以委托父级来处理
DaoAuthenticationProvider
DaoAuthenticationProvider是一个基于用户名密码的认证类,主要通过UserDetailsService来工作
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
AuthenticationEntryPoint
此类用于异常处理,会通过ExceptionHandlingConfigurer来配置进入ExceptionTranslationFilter从而进入过滤器链来处理异常
public void configure(H http) {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint,
getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
UserDetails 与 UserDetailsService
UserDetailsService主要用于发现用户,或者说,去数据库或内存中查询用户与认证的具体的工作就是它来做的
UserDetails 则是 UserDetailsService 的发现结果;UserDetailsService在找到用户后,需要将信息包装为UserDetails
小结
由login方法调用 AuthenticationManager 的 authentication() 方法, 而在方法中,实际认证过程交由 AuthenticationProvider 进行;在 AuthenticationProvider中(涉及到 authenticate() -> retrieveUser()
) 在retrieveUser()中使用 UserDetailsService 去 loadUserByUsername,之后,要么发生异常后交由AuthenticationEntryPoint处理;要么返回UserDetails,然后将其包装为authentication返回给Controller
所以,我们在自己使用时最基础需要定义3个类,AuthenticationManager、AuthenticationProvider与 UserDetailsService
授权
HttpServletRequest授权
在Spring Security中,我们可以根据请求的路径来判断这个request是否需要鉴权以及是否通过校验
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}
上述配置的意思是,在 /api/ 路径下,所有对 /user/ 下的请求都需要具有USER角色,所有对 /admin/ 下的请求都需要具有ADMIN角色; 所有的请求都需要通过 认证。
Method授权
需要在配置类上使用@EnableMethodSecurity, 然后,你就可以立即用 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter 注解任何 Spring 管理的类或方法,以授权方法调用,包括参数和返回值。以实现更加细粒度的鉴权
在访问对应方法时触发
以下所有注解中的Spring EL表达式针对的对象都为SecurityContext中存放的对象
@PreAuthorize
此注解要求在访问readAccount前,需要当前的认证用户满足条件hasRole('ADMIN'), 否则返回403
@Component
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}
@PostAuthorize
此注解要求在访问readAccount前,需要当前的认证用户满足条件returnObject.owner == authentication.name, 否则返回403
@Component
public class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
同理,@PreFilter与@PostFilter使用方法类似
自定义校验
通过编程授权方法的第一种方法是一个两步过程。
首先,声明一个 Bean,该 Bean 的方法需要一个 MethodSecurityExpressionOperations 或者 String 实例,如下所示:
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations /*String permi*/) {
// ... authorization logic
}
}
然后,在注解中以如下方式引用该 Bean:
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root) /*@authz.decide('ADMIN')*/")
@GetMapping("/endpoint")
public String endpoint() {
// ...
}
}
Spring Security 将在每次方法调用时调用该Bean上的给定方法。
这样做的好处是所有的授权逻辑都在一个单独的类中,可以独立进行单元测试并验证其正确性。它还可以访问完整的Java语言。
小结
| 请求级 | 方法级 | |
|---|---|---|
| 授权类型 | 粗粒度 | 细粒度 |
| 配置位置 | 在配置类中声明 | 局部到方法声明 |
| 配置方式 | DSL | 注解 |
| 授权的定义 | 编程式 | SpEL |
其他
LogoutFilter
HttpSecurity的配置
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
上面的代码会触发LogoutConfigurer去配置LogoutFilter
private LogoutFilter createLogoutFilter(H http) {
// ......
private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
// ......
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
this.logoutHandlers.add(this.contextLogoutHandler);
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
// 下面会配置logouthandlers
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
result = postProcess(result);
return result;
}
上面的配置会触发下面的LogoutFilter的构造函数
public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
this.handler = new CompositeLogoutHandler(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
LogoutFilter的doFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 默认是logout
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
// 按顺序执行handler
this.handler.logout(request, response, auth);
// 执行logout成功的handler
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
最好还是去自己实现Handler来自定义退出成功处理器
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler