SpringSecurity最主要的就是过滤器链。
一、过滤器链的原理分析
首先分析web.xml中的如下配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
找到DelegatingFilterProxy这个类,其实这个类还是过滤器。
查看doFilter方法。
debug查看上面的方法。所以web.xml中Filter的名字必须是springSecurityFilterChain也就不奇怪了。
继续查看上述方法返回的FilterChainProxy这个类。
还是过滤器,继续看doFilter方法。
继续debug上述方法。
继续进入getFilters方法。
SecurityFilterChain其实是个接口,实现关系如下图:
二、CSRF过滤器分析
CSRF跨站点请求伪造(Cross—Site Request Forgery),存在巨大的危害性。
CSRF攻击攻击原理及过程如下:
1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
现在想明白了,为什么我之前的QQ号被盗了。。。裂开啊。。。那肯定是小杂碎通过诱惑的手段,让我去点击某个网址,并在该网址上造了一个URL(群发骚扰消息的请求,也是之前的诱惑网址,发给几个群之后,修改自己密码为杂碎自己定义的密码)
,OK接下来我密码直接没了。联想CSRF攻击,我在点那个网址的时候,相当于我把手机上的缓存信息也带过去了,就相当于黑客伪造我在发请求,emmm,信息安全很重要呐。
接下来看一下SpringSecurity提供的CsrfFilter过滤器,
点到最后会发现,如下图。
非GET等如下请求都会经过Csrf过滤器,而GET请求等直接放行。要想通过CSRF过滤器,可以手动添加csrf的token信息,可通过SpringSecurity提供的动态标签直接添加。
三、认证的过滤器分析
UsernamePasswordAuthenticationToken类中其实就包括了用户名和密码两部分。
继续进入认证方法。
SpringSecurity自带的用户对象为UserDetails 。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
//UserDetails就是SpringSecurity自己的用户对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//用自己数据库中的数据实现认证操作啊
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
通过retrieveUser方法查看认证业务。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
所以我们自己通过数据库实现认证是需要实现UserDetailsService 接口,并返回SpringSecurity提供的用户对象UserDetails即可。
再看之前源码中的最后一句createSuccessAuthentication方法。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
这里又封装了UsernamePasswordAuthenticationToken 对象,但是和之前不同的是,此时多了角色信息。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
顺便看一下authorities参数的来源,来到实现了GrantedAuthoritiesMapper接口到的父类中。
然而一顿操作猛如虎,一看战绩0-5的感觉油然而生。
接着看super(authorities)方法。
找到最开始的UsernamePasswordAuthenticationFilter的父类,并查看父类实现的doFilter方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
//失败执行此方法
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//成功走此方法
this.successfulAuthentication(request, response, chain, authResult);
}
}
四、查看记住我源码分析
找到接口的实现类。
判断表单中记住我标记的具体逻辑。
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
String paramValue = request.getParameter(parameter);
//忽略大小写的on、true、yes以及1都可以通过
if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
}
return false;
}
}
}
继续查看父类PersistentTokenBasedRememberMeServices实现的onLoginSuccess方法。
SpringSecurity需要手动开启其开启remember me的过滤器,并且持久化到数据库表时,表名以及各个字段名都是固定的,无法改变,表结构如下:
CREATE TABLE persistent_logins(
username varchar(64) NOT NULL,
series varchar(64) NOT NULL,
token varchar(64) NOT NULL,
last_used timestamp NOT NULL,
PRIMARY KEY(series)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;