SpringSecurity中的过滤器
以上图片来自官网,Security过滤器通过SecurityFilterChain API插入FilterChainProxy。过滤器的顺序很重要。通常不需要知道Spring Security过滤器的顺序。然而,有时了解顺序是有益的。以下是Spring Security Filter顺序的详细列表:
ForceEagerSessionCreationFilter
ChannelProcessingFilter//过滤请求协议http、https。默认不加载
WebAsyncManagerIntegrationFilter//将WebAsyncManager与SpringSecurity上下文进行集成,默认加载。
SecurityContextPersistenceFilter//在处理请求前将安全信息加载到SpringContextHolder中。默认加载
HeaderWriterFilter//处理头信息加入响应中,默认加载
CorsFilter//处理跨域问题,默认不加载
CsrfFilter//处理CSRF攻击,默认加载
LogoutFilter//处理注销登录,默认加载
OAuth2AuthorizationRequestRedirectFilter//处理OAuth2认证重定向,NO
Saml2WebSsoAuthenticationRequestFilter//处理SAML认证,NO
X509AuthenticationFilter//处理X509认证,NO
AbstractPreAuthenticatedProcessingFilter//处理预认证问题,NO
CasAuthenticationFilter//处理CAS单点登录
OAuth2LoginAuthenticationFilter//处理OAuth2认证
Saml2WebSsoAuthenticationFilter//处理SAML认证
UsernamePasswordAuthenticationFilter//处理表单登录
OpenIDAuthenticationFilter//处理OpenId认证
DefaultLoginPageGeneratingFilter//配置默认登录页面
DefaultLogoutPageGeneratingFilter//配置默认注销页面
ConcurrentSessionFilter//处理session有效期
DigestAuthenticationFilter//处理HTTP摘要认证
BearerTokenAuthenticationFilter//处理OAuth2认证的AccessToken
BasicAuthenticationFilter//处理HTTP basic登录
RequestCacheAwareFilter//处理请求缓存
SecurityContextHolderAwareRequestFilter//包装原始请求
JaasApiIntegrationFilter//处理JAAS认证
RememberMeAuthenticationFilter//处理RememberMe登录
AnonymousAuthenticationFilter//配置匿名认证
OAuth2AuthorizationCodeGrantFilter//处理OAuth2认证中的授权码
SessionManagementFilter//处理session并发问题
ExceptionTranslationFilter//处理认证授权中的异常
FilterSecurityInterceptor//处理授权相关
SwitchUserFilter//处理账户切换
默认情况下SpringBoot
对SpringSecurity
进行自动化配置时,会创建一个名字为SpringSecurityFilterChain
的过滤器,并且注入到Spring容器中,这个过滤器将负责所有的安全管理,包括用户的授权、认证、重定向等等,这里可以参考WebSecurityConfiguraction
的源码。
里面有这么一段代码
@Bean(
name = {"springSecurityFilterChain"}
)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
这里就会加载默认的filter
(这里如果没有源码就使用maven命令: mvn dependency:sources
下载源码!)
默认对所有的请求做验证
我们在WebSecurityConfigAdapter类中可以发现下面这个方法,这也就是为什么我们加入了启动jar之后原来的请求就都会被过滤了。
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();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)//类路径下存在这个类的时候才会生效
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)//只要我们自定义了bean就可以覆盖上面默认的配置,
@ConditionalOnWebApplication(type = Type.SERVLET)//在servlet容器下才会生效
public class SpringBootWebSecurityConfiguration {
@Configuration(proxyBeanMethods = false)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
}
}
默认的登录界面
在源码中存在这样一个类DefaultLoginPageGeneratingFilter
,这个类就生成了登录界面
默认登录用户的生成
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();
}
我们点进去formLogin()
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
看一下这个类FormLoginConfigurer<>()
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
里面有这么一个类UsernamePasswordAuthenticationFilter
,这个类里面有这么一个方法attemptAuthentication
,这里就对登录进行了一些验证。
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);
}
点进最后的authenticate
方法,里面有一个循环判断的逻辑
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 | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
我们进入这一行代码result = provider.authenticate(authentication);
这个子类AbstractUserDetailsAuthenticationProvider
里面的实现
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);
}
我们点进代码retrieveUser
的实现
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//下面就是验证的实现
//这里根据username去查询user用户
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);
}
}
我们仔细阅读这个方法的实现this.getUserDetailsService().loadUserByUsername(username);
public interface UserDetailsService {
// ~ Methods
// ========================================================================================================
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
这个UserDetailsService
这个接口有四个实现类
这里是基于内存实现的,默认使用实现类是InMemoryUserDetailsManager
,我们可以看到这里面有一个HashMap,这里存储了对应的用户名和密码。
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//这里直接使用users获取,就是获取hashmap里面的key对应的value。
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
关于这里为什么默认是这个类InMemoryUserDetailsManager
,需要看以下这个自动配置类UserDetailsServiceAutoConfiguration
如果ConditionalOnMissingBean
里面的类都没有就会使用基于内存的InMemoryUserDetailsManager
类。
@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);
@Bean
@ConditionalOnMissingBean(
type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}
}
这段代码里面有这个SecurityProperties.User user = properties.getUser();
,
我们看一下这个类,这里面可以配置用户名和密码,如果没有配置就使用默认的。我们还可以在配置文件里面进行配置然后读取
spring:
security:
user:
name: root
password: root
roles:
-
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
/**
* Order applied to the WebSecurityConfigurerAdapter that is used to configure basic
* authentication for application endpoints. If you want to add your own
* authentication for all or some of those endpoints the best thing to do is to add
* your own WebSecurityConfigurerAdapter with lower order.
*/
public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5;
/**
* Order applied to the WebSecurityConfigurer that ignores standard static resource
* paths.
*/
public static final int IGNORED_ORDER = Ordered.HIGHEST_PRECEDENCE;
/**
* Default order of Spring Security's Filter in the servlet container (i.e. amongst
* other filters registered with the container). There is no connection between this
* and the {@code @Order} on a WebSecurityConfigurer.
*/
public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;
private final Filter filter = new Filter();
private User user = new User();
public User getUser() {
return this.user;
}
这个类里面有一个静态内部类User
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
/**
* Granted roles for the default user name.
*/
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
this.passwordGenerated = false;
this.password = password;
}
public List<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
从上面的分析可以知道,如果我们集成了数据库可以直接实现UserDetailsService
接口然后重写验证逻辑即可。