结合实际场景分析Spring Boot Security(2.x.x)认证过程

相信任何一个平台都绕不开用户认证鉴权这两个功能,最近我恰好负责调研Spring Security接第三方认证中心的技术方案。所以借此机会,认真调研学习一番它。
Spring Boot Security分为两部分讲解——认证授权,这一篇分析认证过程,下一篇分析授权过程。
顺便说一下这次调研过程中的教训:面对这种比较复杂的技术/功能,不能像对待一般功能,copy百度上的代码就行了。了解它的原理之后,再动手效率会高很多。


一、需求介绍

我们有一个公共的认证中心(Auth Server),前端页面在Auth Server登录后,会得到一个token。页面访问业务应用的API时,需要在header里携带“sessionId: token”。业务应用的Spring Security就负责调用Auth Server API验证得到的token是否合法,以及获取对应的用户信息、角色信息。
我们主要讲业务应用里的Spring Security是如何运作的。

二、Security处理过程

认证是通过一系列的filter来实现的,通过debug来梳理Spring Filter运作流程:


image.png
  1. 首先进入 ApplicationFilterChain 类
    它负责管理针对request的一系列filter的执行,当所有的filter执行完成后,它最终会调用servlet的service():
/**
 * Implementation of javax.servlet.FilterChain used to manage
 * the execution of a set of filters for a particular request.  When the
 * set of defined filters has all been executed, the next call to
 * doFilter() will execute the servlet's service()
 * method itself.
 *
 * @author Craig R. McClanahan
 */
public final class ApplicationFilterChain implements FilterChain {

如下图所示,我们可以发现它共用6个filter需要执行,其中第5个则是security相关的filter:


ApplicationFilterChain所管理的默认6个Filter
  1. 进入security相关的filter bean:springSecurityFilterChain
    它实则类为DelegatingFilterProxy.class
public class DelegatingFilterProxy extends GenericFilterBean {
...
...
@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 本类会委派给真正的filter(默认是FilterChainProxy.class)
        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 filter去真正执行任务
        invokeDelegate(delegateToUse, request, response, filterChain);
    }
  1. 进入真正负责处理security相关事宜的filter:FilterChainProxy.class
public class FilterChainProxy extends GenericFilterBean {
//该对象里有默认的security相关的11个filter
//针对我们的定制化需求,我们需要往这里面增加一个我们自己编写的filter。
private List filterChains;
...
...
private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);
        //得到chain里面的filters
        List filters = getFilters(fwRequest);

        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }

加上下文我们写的filter,chain里共有12个filter:


12filters.png
  1. 这些filter都执行完后,会执行servlet.service(request, response);
三、Demo编写(请认真阅读代码中的注释信息,涉及到细节)

现在我们结合上述原理,来编写代码...
大致思路就是首先配置好spring security,然后写自己的filter,并加入到默认的11个filter中。

  1. 搭建Spring Boot程序,maven pom.xml主要信息如下:

        org.springframework.boot
        spring-boot-starter-parent
        2.0.6.RELEASE



        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-security
        

  1. 写一个简单的Controller
@RestController
//意味着该用户的角色必须包含"ROLE_admin"
@PreAuthorize("hasRole('admin')")
public class TestController {
    @RequestMapping({ "/api/test" })
    public String user() {
        return "success";
    }
}
  1. 配置security
@Configuration
@EnableWebSecurity
//配合Controller层的注解 @PreAuthorize("hasRole('admin')") 使用
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * filter(生成Authentication对象) -> provider manager -> provider(校验Authentication)
     *也可以把全部的校验任务放在filter里实现,provider可以理解为供filter的使用的另一个类
     */
    @Autowired
    private MyAuthenticationProvider myAuthenticationProvider;

    /**
     * 添加自己的provider,给providerManager管理
     * @param auth
     */
    @Override
    protected void configure(
            AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(myAuthenticationProvider);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //下面这一行很重要,添加一些基本的配置
        super.configure(http);
        MyAuthenticationFilter filter = new MyAuthenticationFilter();
        //给自己的filter添加provider manager
        filter.setAuthenticationManager(super.authenticationManagerBean());

        //把自己的Filter添加到FilterChain合适的位置
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }
}
  1. 自定义包含用户信息的Authentication对象:MyAuthenticationToken
    它是用于认证、鉴权的核心对象
public class MyAuthenticationToken extends AbstractAuthenticationToken {
    private Object principal;

    public MyAuthenticationToken(Collection authorities) {
        super(authorities);
        principal = "my principal";
    }


    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }
}
  1. 实现自己的Filter(可以在这里面写认证的相关代码,也可以放到provider里写)
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * 对以/api/开头的请求进行过滤处理
     */
    protected MyAuthenticationFilter() {
        super(new RegexRequestMatcher("^(/api/).*", null));
    }

    /**
     * 开始认证的核心代码
     * 返回null则代表还需要继续认证(其他filter)
     * 抛出AuthenticationException的子类,则代表认证失败 --> 401
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        //SimpleGrantedAuthority是代表用户的角色
        List roleList = new ArrayList<>();
        //角色如果是admin,则设置为"ROLE_admin"
        roleList.add(new SimpleGrantedAuthority("ROLE_" + request.getHeader("sessionId")));

        //MyAuthenticationToken是Authentication的实现类,它是用于security认证处理的对象!!!
        MyAuthenticationToken authenticationToken = new MyAuthenticationToken(roleList);
        //标志为已认证,但并不代表不会被再次认证,取决于AbstractSecurityInterceptor.class(它的子类就是第十二个filter:FilterSecurityInterceptor)里的alwaysReauthenticate字段
        authenticationToken.setAuthenticated(true);
        authenticationToken.setDetails(new Object());
        return authenticationToken;
        //!!!如果需要自定义的AuthenticationProvider来进行后续认证操作,则可以用下一行代码
        //由于我打算直接在本Filter里完整认证,则不需要provider
        //getAuthenticationManager()是得到ProviderManager,该Manager会调用相关的provider
        //return getAuthenticationManager().authenticate(authenticationToken);

        //认证失败 --> 401
        //throw new FailureAuthenticationException("error");
    }

    /**
     * 认证成功后,默认是重定向到一个系统默认地址
     * 所以重载:当认证成功后,继续后续操作,访问Controller层
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        try {
            chain.doFilter(request, response);
        } finally {
            SecurityContextHolder.clearContext();
        }
    }
}
  1. 实现自己的provider
    不是必须的,它是负责处理 filter里生成的authentication对象,我们可以直接在filter里硬嵌相关代码
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
    /**
     * 认证filter产生的Authentication对象
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        MyAuthenticationToken token = (MyAuthenticationToken) authentication;
        return token;
    }

    /**
     * 指定支持的Authentication类型
     */
    @Override
    public boolean supports(Class authentication) {
        return MyAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

我们的认证和鉴权的Demo就此完成

四、测试
可以把sessionId值换为其他的试试,这时就与鉴权相关了,会在下篇博客里讲解其原理

Demo git 地址:https://gitee.com/cherron/spring-security-demo
Security的底层原理实在太复杂,我所写的内容只是冰山一角。如果有什么疑问,可以在评论区一起探讨。
鉴权的原理会在下篇博客里讲解!

你可能感兴趣的:(结合实际场景分析Spring Boot Security(2.x.x)认证过程)