序
一直以来, 接触到的项目授权都是利用安全框架居多。
SpringSecurity、 Oauth2、 JWT等等...
大多进行一番配置,后期只需关注放行和拦截和角色之类的配置就可。
SpringSecurity使用中我曾为了实现短信登录,也写过Filter添加到 SpringSecurity的过滤器链中,大概对SpringSecurity的原理略知一二。
参考了(2020/6) https://github.com/halo-dev/halo.git security 部分实现。
为何要自行实现授权?
起因是接手的一个项目中,授权控制部分一团糟,接口控制基本没有实现。
但是已经有了登录,注销之类的功能,这部分是用JWT做的。
所以主要想用Filter重构授权, 完成接口控制
我选择不使用框架,主要是因为授权部分的需求比较简单,而且获取登录的用户部分可以不用修改,判断用户的Token可以利用已经实现好了的JWT。
分析下主要要做的事
首先是用户的登录状态,这里用户登录后,会返回前端一的请求头增加Authorization 值为用户的JWT信息。
之后前端的每次请求都会携带此信息在Request中。
我需要用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类
init, destroy 实在没什么好说的,doFilter 方法的3个参数,可以看到servlet规范下的ServletRequest,ServletResponse和一个FilterChain。
FilterChain 就是做过滤器链用到的对象,FilterChain 的实现类一般会有一个集合,放多个Filter,用来套娃执行,这个我觉得还是自己去看源码体会比较深。
OncePerRequestFilter
org.springframework.web.filter.OncePerRequestFilter
这个Filter是Spring全家桶的一部分,大概就是对javax.servlet.Filter的封装,选择使用这个来作为Filter。
来看看重写的doFilter 方法封装了什么。
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();
}
}
- SecurityContextHolder.clearContext() 清理掉保存线程中的内容
- getFailureHandler()是失败时执行的方法
- 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规范下的过滤器链,实现一次后感觉很有收获,实际的业务中还增加了相当多的逻辑...
记录下我实现的核心逻辑,写了一次后,对当前线程中储存信息,还有过滤器链条的执行原理等等.. 有 '顿悟' 的感觉。
本文由 考拉 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Mar 11,2022