SpringSecurity极简入门


SpringSecurity入门

SpringSecurity极简入门


Spring我们是再熟悉不过的后端开发框架,只要是java体系的开发人员应该都至少接触过Spring,但是对于今天我们要讲的一个框架,可能对于部分同学来说比较陌生——Spring Security

之所以要整理这一套资料,一方面是将自己之前研究的记录下来,另一方面是因为在我刚开始接触的时候真的没有一份说全的文档,要么是结构支离破碎,要么是只有一部分

希望这一系列的文档对于学习、应用这个框架的同学有用。

介绍

简单一句话:认证+授权+重量级。重点:是一款重量级的认证授权框架。这也是很多刚上手的同学半路放弃它的原因

认证:

通俗点说就是系统认为用户是否能登录。

授权:

通俗点讲就是系统判断用户是否有权限去做某些事情。

极简入门

创建springboot

引入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

编写controller测试

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("add")
    public String add() {
        return "hello";
    }
}

启动测试:

浏览器输入:http://localhost:7000/test/add,发现并没有返回我们预期的内容而是到了一个登陆页面,这个就是spring-security-web的默认登陆页面,也就是我们的鉴权已经生效了。

SpringSecurity极简入门
image-20210421220827253

spring-security默认的用户名为user,密码在控制台输出:

SpringSecurity极简入门
image-20210421221010264

输入之后,显示我们的hello返回值:

SpringSecurity极简入门
image-20210421221045927

原理

过滤器链

SpringSecurity 本质是一个过滤器链:从启动是可以获取到过滤器链,常见的过滤器如下:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

代码底层流程:重点看三个过滤器:FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。

    //FilterSecurityInterceptor
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.invoke(new FilterInvocation(request, response, chain));
    }
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } else {
            if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
                filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

            try {
                filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }
    }

super.beforeInvocation(fi) 表示查看之前的filter 是否通过。fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

//ExceptionTranslationFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (IOException var7) {
            throw var7;
        } catch (Exception var8) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
            RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.classcauseChain);
            if (securityException == null) {
                securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.classcauseChain);
            }

            if (securityException == null) {
                this.rethrow(var8);
            }

            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);
            }

            this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
        }

    }

UsernamePasswordAuthenticationFilter :对/login的POST请求做拦截,校验表单中用户名,密码。

//UsernamePasswordAuthenticationFilter 
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

另外,它的父类AbstractAuthenticationProcessingFilter中有两个方法,successfulAuthentication和unsuccessfulAuthentication,用于处理认证结果:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }

        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

过滤器加载过程

首先:

DelegatingFilterProxy(配置过滤器),springboot自动配置已经帮忙配置了

//DelegatingFilterProxy
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized(this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = this.findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
                    }

                    delegateToUse = this.initDelegate(wac);//初始化
                }

                this.delegate = delegateToUse;
            }
        }

        this.invokeDelegate(delegateToUse, request, response, filterChain);
    }
 //上下文拿到FilterChainProxy
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = this.getTargetBeanName();//FilterChainProxy
        Assert.state(targetBeanName != null"No target bean name set");
        Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
        if (this.isTargetFilterLifecycle()) {
            delegate.init(this.getFilterConfig());
        }

        return delegate;
    }

继续查看FilterChainProxy源码,通过它加载过滤器链

//FilterChainProxy
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);
  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);
 }
/**
  * Returns the first filter chain matching the supplied URL.
  * @param request the request to match
  * @return an ordered array of Filters defining the filter chain
  */

 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

当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。接口定义如下:

public interface UserDetailsService {

 /**
  * 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;

}

其中返回UserDetails对象:这个类是系统默认的用户“主体”

public interface UserDetails extends Serializable {

 // 表示获取登录用户所有权限 
 Collection<? extends GrantedAuthority> getAuthorities(); 
 // 表示获取密码 
 String getPassword()
 // 表示获取用户名 
 String getUsername()
 // 表示判断账户是否过期 
 boolean isAccountNonExpired()
 // 表示判断账户是否被锁定 
 boolean isAccountNonLocked()
 // 表示凭证{密码}是否过期
    boolean isCredentialsNonExpired()
    // 表示当前用户是否可用 
    boolean isEnabled();

}

在实际中实现该接口的类就是用户主体对象(用户名、密码、权限等)

SpringSecurity极简入门
image-20210422083514255
 public User(String username, String password, boolean enabled, boolean accountNonExpired,
   boolean credentialsNonExpired, boolean accountNonLocked,
   Collection<? extends GrantedAuthority> authorities)
 
{
  Assert.isTrue(username != null && !"".equals(username) && password != null,
    "Cannot pass null or empty values to constructor");
  this.username = username;
  this.password = password;
  this.enabled = enabled;
  this.accountNonExpired = accountNonExpired;
  this.credentialsNonExpired = credentialsNonExpired;
  this.accountNonLocked = accountNonLocked;
  this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
 }
PasswordEncoder

用于将密码按照一定方式加密的接口,在spring-security中有很多的加密方式,可以直接实现该接口,定义自己的实现类即可

// 表示把参数按照特定的解析规则进行解析 
String encode(CharSequence rawPassword)
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。 
boolean matches(CharSequence rawPassword, String encodedPassword)
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则返回false。默认返回false。 
default boolean upgradeEncoding(String encodedPassword) return false; }

需要深入去研究。

接口实现类

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.

@Test public void test01()
    // 创建密码解析器 
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); 
    // 对密码进行加密
    String atguigu = bCryptPasswordEncoder.encode("atguigu"); 
    // 打印加密之后的数据 
    System.out.println("加密之后数据:t"+atguigu); 
    //判断原字符加密后和加密之前是否匹配 
    boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu); 
    // 打印比较结果 
    System.out.println("比较结果:t"+result); 
}


原文始发于微信公众号(云户):SpringSecurity极简入门

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25760.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!