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

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规范下的过滤器链, 实现一次后感觉很有收获, 实际的业务中还增加了相当多的逻辑...
记录下我实现的核心逻辑 写了一次后, 对当前线程中储存信息, 还有过滤器链条的执行原理等等.. 有 '顿悟' 的感觉..