基本原理
本质:一个过滤器链
1、底层核心过滤器
(1)FilterSecurityInterceptor:一个方法级的权限过滤器,基本位于过滤链的最底部
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
//过滤器已经应用于这个请求,而用户希望我们观察到的是已经处理一次请求,所以不需要重新进行安全检查
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
//第一次调用这个请求,所以要进行安全检查
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//查看之前的过滤器是否放行
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
//执行本过滤器的doFilter,表示真正的调用后台的服务
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
(2)ExceptionTranslationFilter:一个异常过滤器,处理在认证授权过程中抛出的异常
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
(3)UsernamePasswordAuthenticationFilter:拦截 /login 的 POST 请求,校验表单中用户名和密码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//判断是否为POST请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取用户名
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
//获取密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
//校验
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
//允许子类设置details属性
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
2、加载过滤器流程
(1)DelegatingFilterProxy 类
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//如果有必要,可以懒加载Filter代理
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
//获取将要使用的代理
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
//让delegateToUse执行实际doFilter操作
invokeDelegate(delegateToUse, request, response, filterChain);
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//内置过滤器:FilterChainProxy
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
//从容器中获取代理
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
(2)FilterChinaProxy 类
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//内部过滤
doFilterInternal(request, response, chain);
}
catch (RequestRejectedException ex) {
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
//收集Spring Security所有Filter到过滤链中
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
UserDetailsService 接口
1、没有配置时,Spring Security 定义生成账号和密码;实际项目中,由数据库查询账号和密码,需要自定义逻辑控制、逻辑认证
2、用户认证时,继承 UsernamePasswordAuthenticationFilter 类,重写 attemptAuthentication(验证)、successfulAuthentication(验证成功)、unsuccessfulAuthentication(验证失败)
3、查询数据库时,实现 UserDetailsService 接口
public interface UserDetailsService {
/*
根据用户名找到用户。在实际执行中,搜索可能是区分大小写的,也可能是不区分大小写的,这取决于执行实例是如何配置的。在这种情况下,返回的UserDetails对象可能有一个与实际请求的不同大小写的用户名
形参:
username - 识别需要数据的用户的用户名
返回值:
一个完整的用户记录(不能为空)
抛出:
UsernameNotFoundException - 如果找不到用户或者用户没有GrantedAuthority的话
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
(1)返回 User 对象,实现 UserDetails 接口,该 User 对象由 Spring Security 提供
public class User implements UserDetails, CredentialsContainer
(2)UserDetails 接口
public interface UserDetails extends Serializable {
/*
返回授予该用户的权限,不能返回空值
返回值:
按自然键排序的权限(不能为空)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/*
返回用于验证用户身份的密码
返回值:
密码
*/
String getPassword();
/*
返回用于验证用户的用户名,不能返回空值
返回值:
用户名(不能为空)
*/
String getUsername();
/*
表示用户的账户是否已经过期,一个过期的账户不能被认证
返回值:
如果用户的账户是有效的(即未过期),则为true;如果不再有效(即过期),则为false
*/
boolean isAccountNonExpired();
/*
表示用户是被锁定还是未被锁定,一个被锁定的用户不能被认证
返回值:
如果用户没有被锁定,则为true,否则为false
*/
boolean isAccountNonLocked();
/*
表示用户的凭证(密码)是否过期,过期的凭证会阻止认证
返回值:
如果用户的凭证有效(即未过期),则为true;如果不再有效(即过期),则为false
*/
boolean isCredentialsNonExpired();
/*
表示该用户是启用还是禁用,一个禁用的用户不能被认证
返回值:
如果用户被激活,则为true,否则为false
*/
boolean isEnabled();
}
PasswordEncoder 接口
1、密码加密接口,加密返回 User 对象的密码
public interface PasswordEncoder {
/*
对原始密码进行编码。一般来说,一个好的编码算法适用于SHA-1或更大的哈希值,并结合一个8字节或更大的随机生成的盐值
*/
String encode(CharSequence rawPassword);
/*
验证从存储区获得的编码密码,与提交的原始密码在编码后是否一致。如果密码匹配则返回true,如果不匹配则返回false。存储的密码本身不会被解码
形参:
rawPassword - 要编码和匹配的原始密码
encodedPassword - 与存储的编码密码进行比较
返回值:
如果编码后的原始密码与存储的编码密码匹配,则为true
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/*
如果编码后的密码应该再次编码以提高安全性,则返回true,否则返回false。默认实现总是返回false。
形参;
encodedPassword - 要检查的编码密码
返回值:
如果为了更好的安全,应该重新编码密码,则返回true,否则返回false
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战