自行实现项目授权控制实战

in 工作记录 with 0 comment

一直以来, 接触到的项目授权都是利用安全框架居多。

SpringSecurity、 Oauth2、 JWT等等...

大多进行一番配置,后期只需关注放行和拦截和角色之类的配置就可。

SpringSecurity使用中我曾为了实现短信登录,也写过Filter添加到 SpringSecurity的过滤器链中,大概对SpringSecurity的原理略知一二。

参考了(2020/6) https://github.com/halo-dev/halo.git security 部分实现。

为何要自行实现授权?

起因是接手的一个项目中,授权控制部分一团糟,接口控制基本没有实现。

但是已经有了登录,注销之类的功能,这部分是用JWT做的。

所以主要想用Filter重构授权, 完成接口控制

我选择不使用框架,主要是因为授权部分的需求比较简单,而且获取登录的用户部分可以不用修改,判断用户的Token可以利用已经实现好了的JWT。

分析下主要要做的事

首先是用户的登录状态,这里用户登录后,会返回前端一的请求头增加Authorization 值为用户的JWT信息。

之后前端的每次请求都会携带此信息在Request中。

image.png

我需要用Filter在SpringMVC执行方法前(也就是@Mapping注解标注的方法)执行这些Filter,判断当前有没有用户,或者用户有没有权限去请求这个接口,完成拦截。

然后需要校验JWT后,获取到当前的用户,用户信息要保存在当前请求线程中,后续的业务有时会需要拿到当前登录用户的信息。

当前线程中存放信息工具类

public class SecurityContextHolder {

    private final static ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();

    private SecurityContextHolder() {
    }

    /**
     * Gets context.
     *
     * @return security context NonNull
     */
    public static SecurityContext getContext() {
        // Get from thread local
        SecurityContext context = CONTEXT_HOLDER.get();
        if (context == null) {
            // If no context is available now then create an empty context
            context = createEmptyContext();
            // Set to thread local
            CONTEXT_HOLDER.set(context);
        }

        return context;
    }

    /**
     * Sets security context.
     *
     * @param context security context Nullable
     */
    public static void setContext(SecurityContext context) {
        CONTEXT_HOLDER.set(context);
    }

    /**
     * Clears context.
     */
    public static void clearContext() {
        CONTEXT_HOLDER.remove();
    }

    /**
     * Creates an empty security context.
     *
     * @return an empty security context
     */
    private static SecurityContext createEmptyContext() {
        return new SecurityContextImpl(null);
    }
}

CONTEXT_HOLDER这个ThreadLocal类的泛型SecurityContext 为自定义储存用户信息的对象,用来保存和取出用户信息 如下:

@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class SecurityContextImpl implements SecurityContext {

    private Authentication authentication;

    @Override
    public Authentication getAuthentication() {
        return authentication;
    }

    @Override
    public void setAuthentication(Authentication authentication) {
        this.authentication = authentication;
    }

}

复习下javax.servlet.Filter类

image.png

init, destroy 实在没什么好说的,doFilter 方法的3个参数,可以看到servlet规范下的ServletRequest,ServletResponse和一个FilterChain。

image.png

FilterChain 就是做过滤器链用到的对象,FilterChain 的实现类一般会有一个集合,放多个Filter,用来套娃执行,这个我觉得还是自己去看源码体会比较深。

OncePerRequestFilter

org.springframework.web.filter.OncePerRequestFilter
image.png

这个Filter是Spring全家桶的一部分,大概就是对javax.servlet.Filter的封装,选择使用这个来作为Filter。

来看看重写的doFilter 方法封装了什么。

image.png

ServletResponse 帮我们封装为 HttpServletRequest。

// Do invoke this filter... 主要就是调用了doFilterInternal()方法
还有两个需要自行实现的方法skipDispatch(httpRequest),shouldNotFilter(httpRequest) 这两个就是放行路径和拦截路径的逻辑。

来看看doFilterInternal

	/**
	 * Same contract as for {@code doFilter}, but guaranteed to be
	 * just invoked once per request within a single request thread.
	 * See {@link #shouldNotFilterAsyncDispatch()} for details.
	 * <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
	 * default ServletRequest and ServletResponse ones.
	 */
	protected abstract void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException;

还是Spring懂我,留着给我实现呢.. 也就是说doFilterInternal 就相当与 Filter中的 doFilter方法。

定义自己的授权Filter类

AbstractAuthenticationFilter extends OncePerRequestFilter

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Do authenticate
        try {
            doAuthenticate(request, response, filterChain);
        } catch (Exception e) {
            getFailureHandler().onFailure(request, response, e);
        } finally {
            SecurityContextHolder.clearContext();
        }
    }
  1. SecurityContextHolder.clearContext() 清理掉保存线程中的内容
  2. getFailureHandler()是失败时执行的方法
  3. doAuthenticate()为实际的逻辑

doAuthenticate应该做的事。

   @Override
    protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
     
        //get token
        String accessToken = getTokenFromRequest(request);

        if (StringUtils.isBlank(accessToken)) {
            throw  new BusinessException(401 , "未登录,请登录后访问! token is null ");
        }

        // Get the user
        User user = oauthServiceFeign.getUser(accessToken);

        if (user == null) {
            throw  new BusinessException(401, "token认证失败");
        }

        // Build user detail
        UserDetail userDetail = new UserDetail(user);

        // Set security
        SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail)));

        // Do filter
        filterChain.doFilter(request, response);

    }

这里放的伪代码.. 大概获取到用户就放到线程中保存并且 filterChain.doFilter(request, response),不然就拦截。

拦截和放行

重写之前提到的OncePerRequestFilter中的 shouldNotFilter 自行实现拦截逻辑就可。

    protected final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final UrlPathHelper urlPathHelper = new UrlPathHelper();

    /**
     * Exclude url patterns.
     */
    private final Set<String> excludeUrlPatterns = new HashSet<>(16);

    private final Set<String> urlPatterns = new LinkedHashSet<>();
   @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        Assert.notNull(request, "Http servlet request must not be null");

        // check white list
        boolean result = excludeUrlPatterns.stream().anyMatch(p -> antPathMatcher.match(p, urlPathHelper.getRequestUri(request)));

        return result || urlPatterns.stream().noneMatch(p -> antPathMatcher.match(p, urlPathHelper.getRequestUri(request)));

    }

SpringSecurity 本质上不过是一个Servlet规范下的过滤器链,实现一次后感觉很有收获,实际的业务中还增加了相当多的逻辑...

记录下我实现的核心逻辑,写了一次后,对当前线程中储存信息,还有过滤器链条的执行原理等等.. 有 '顿悟' 的感觉。