自实现oauth2验证与spring-security的结合
前言
前面写了一篇关于spring-security-oauth2适配的文章,但是种种原因,项目中正在使用的spring-security版本暂时不能更换,没法直接使用spring-security-oauth2,无奈只能自己实现验证过程。
本文主要总结一下自实现oauth2验证遇到的一些问题及解决办法。
一、自实现oauth2验证v1.0
自己实现一个简单oauth2验证过程,很简单,不需要像开源软件那样考虑各种模块的配合和弹性,也不需要各种模式都支持。
我的第一个版本就是把所有过程放在一个函数中处理,回调函数传回code之后,使用code去换取AT,然后用AT去获取用户信息,这样就验证过程就结束了,是不是很简单?从功能上来看,这么做是没问题的,但是会有两个问题:
1、没有与spring-security结合起来
也就是验证虽然结束了,但是后续的权限控制之类的跟安全相关的功能其实是用不起来的。这个最终是使用设置安全上下文的方式来解决的:
//auth就是认证成功后自己生成的UsernamePasswordAuthenticationToken
SecurityContextHolder.getContext().setAuthentication(auth);
2、session id没有变更
对安全比较了解的同学都知道,验证成功后,需要改变session id,防止之前的session id被利用。而我这个版本的验证过程,因为没有与任何已有的security组件配合,session id肯定是不会变更的。这个问题最终被我用一个比较暴力的方法解决掉:
//request就是请求的HttpServletRequest
request.changeSessionId()
上面就是我之前遇到的两个问题,虽然都解决了,但是心里肯定比较虚,是不是还有其它问题没有发现?毕竟spring-security做了那么多工作,而我搞的这么简单,没出问题,不代表没有啊。。。
二、 自实现oauth2验证v2.0
由于有了上面的顾虑,我就考虑利用spring-security自身的机制来完成这个验证过程,最终,被我找到了一个不算优雅,但是比较取巧的方法–利用spring-security自身的用户名密码验证机制来帮助实现这一过程。
1、 用户名密码验证机制
用户名密码的配置一般在WebSecurityConfigurerAdapter中配置,类似:
//实现安全的一些配置
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
//设置验证接口
http.formLogin().loginProcessingUrl("/login/auth")
//设置用户名、密码的参数名称
.usernameParameter("account")
.passwordParameter("password");
...
}
//将验证过程交给自定义验证工具
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new MyAuthenticationProvider());
}
}
实现自定义验证过程:
public class MyAuthenticationProvider implements AuthenticationProvider {
public Authentication authenticate(Authentication authentication) {
//获取用户名、密码
String username = authentication.getName();
String password = (String) authentication.getCredentials();
}
}
2、利用用户名密码验证机制
从上面的用户名密码验证机制可以看出,只要我们能够利用getName()来获取code,那剩下的验证过程通过重写AuthenticationProvider肯定能够实现,也就是类似:
http.formLogin().loginProcessingUrl("/login/auth")
//设置用户名、密码的参数名称
.usernameParameter("code")
too naive,试了一下,不行。。。老办法,debug,过程不赘述,简单讲一下涉及用户名密码验证的几个类及遇到的问题。
1) AbstractAuthenticationProcessingFilter
这个是实现安全验证的一个核心filter,安全验证的过滤器都是继承这个类来完成整个验证过程。该类的doFilter函数:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
//成功验证后的session处理
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
...
}
//成功验证后处理
this.successfulAuthentication(request, response, chain, authResult);
}
}
其中,this.sessionStrategy.onAuthentication就是处理上文中提到的session id变更的问题,而this.successfulAuthentication就是处理上文中提到的安全上下文设置的问题。虽然从源码中看,主要涉及的也就是这两项,但是源码中名显然做了更多的工作,利用spring-security自身机制始终是更好的选择。。。
继续,我的请求是在requiresAuthentication这个函数中被拒掉的,原因是http method不是post。。。直接引出了第二个类。
2)FormLoginConfigurer
该类就是http.formLogin()函数返回的设置类,而设置监听接口的函数:
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
看到这段代码,心里立马有一万只CNM飘过。。。为毛POST是写死的,为毛访问控制是protected。。。
没有办法,翻来覆去,确认了确实没有办法在原有代码的基础上设置http method。。。
咱也不是菜鸟,重写,仿照FormLoginConfigurer重写了configer,当然这个过程很简单,最主要的其实只要重写这个函数就行,其它的函数基本都可以删除,然后,需要的是将自定义configer应用到HttpSecurity上:
MyLoginConfigure<HttpSecurity> loginConfigure = new MyLoginConfigure<>();
loginConfigure.setBuilder(http);
loginConfigure.loginProcessingUrl("/login/auth")
.usernameParameter("code");
http.apply(loginConfigure);
跟默认设置差不多,只不过需要apply一下。
继续,又不行,吐血,直接引出第三个类。
3)UsernamePasswordAuthenticationFilter
该类继承AbstractAuthenticationProcessingFilter,实现attemptAuthentication函数来执行验证工作:
//attemptAuthentication函数节选
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
这次是挂在this.postOnly,默认是true,简单,这个在自定义configer构造函数中直接设置一下就ok:
getAuthenticationFilter().setPostOnly(false);
总结
最终结果,只需要重新实现一个configer就能达到目的,其他的功能都是按照spring-security自身的机制在运作。不算优雅,总之还算是个不错的方法。
遗留一个问题,oauth2模式是支持携带state来防止CSRF的(个人觉得意义不大,code本身应该能解决这个问题,更多的我认为还是防暴力攻击),通常做法是state存在session中,然后跟回调的state相比较,如果不一致,则请求可以丢弃。 但是在调试过程中,发现存在请求的session与回调的session(全新session)不一致的情况,这尼玛就没法搞了啊,state肯定不一样啊。。。不是必现,但是频率比较高,没定位出来,尚不清楚是三方服务器的原因还是代码的原因,spring-security-oauth2也有这个问题。虽然不影响功能使用,但是有时间还是需要研究一下的。