详解Spring Security如何配置JSON登录
spring security用了也有一段时间了,弄过异步和多数据源登录,也看过一点源码,最近弄rest,然后顺便搭oauth2,前端用json来登录,没想到spring security默认居然不能获取request中的json数据,谷歌一波后只在*找到一个回答比较靠谱,还是得要重写filter,于是在这里填一波坑。
准备工作
基本的spring security配置就不说了,网上一堆例子,只要弄到普通的表单登录和自定义userdetailsservice就可以。因为需要重写filter,所以需要对spring security的工作流程有一定的了解,这里简单说一下spring security的原理。
spring security 是基于javax.servlet.filter的,因此才能在spring mvc(dispatcherservlet基于servlet)前起作用。
- usernamepasswordauthenticationfilter:实现filter接口,负责拦截登录处理的url,帐号和密码会在这里获取,然后封装成authentication交给authenticationmanager进行认证工作
- authentication:贯穿整个认证过程,封装了认证的用户名,密码和权限角色等信息,接口有一个boolean isauthenticated()方法来决定该authentication认证成功没;
- authenticationmanager:认证管理器,但本身并不做认证工作,只是做个管理者的角色。例如默认实现providermanager会持有一个authenticationprovider数组,把认证工作交给这些authenticationprovider,直到有一个authenticationprovider完成了认证工作。
- authenticationprovider:认证提供者,默认实现,也是最常使用的是daoauthenticationprovider。我们在配置时一般重写一个userdetailsservice来从数据库获取正确的用户名密码,其实就是配置了daoauthenticationprovider的userdetailsservice属性,daoauthenticationprovider会做帐号和密码的比对,如果正常就返回给authenticationmanager一个验证成功的authentication
看usernamepasswordauthenticationfilter源码里的obtainusername和obtainpassword方法只是简单地调用request.getparameter方法,因此如果用json发送用户名和密码会导致daoauthenticationprovider检查密码时为空,抛出badcredentialsexception。
/** * enables subclasses to override the composition of the password, such as by * including additional values and a separator. * <p> * this might be used for example if a postcode/zipcode was required in addition to * the password. a delimiter such as a pipe (|) should be used to separate the * password and extended value(s). the <code>authenticationdao</code> will need to * generate the expected password in a corresponding manner. * </p> * * @param request so that request attributes can be retrieved * * @return the password that will be presented in the <code>authentication</code> * request token to the <code>authenticationmanager</code> */ protected string obtainpassword(httpservletrequest request) { return request.getparameter(passwordparameter); } /** * enables subclasses to override the composition of the username, such as by * including additional values and a separator. * * @param request so that request attributes can be retrieved * * @return the username that will be presented in the <code>authentication</code> * request token to the <code>authenticationmanager</code> */ protected string obtainusername(httpservletrequest request) { return request.getparameter(usernameparameter); }
重写usernamepasswordanthenticationfilter
上面usernamepasswordanthenticationfilter的obtainusername和obtainpassword方法的注释已经说了,可以让子类来自定义用户名和密码的获取工作。但是我们不打算重写这两个方法,而是重写它们的调用者attemptauthentication方法,因为json反序列化毕竟有一定消耗,不会反序列化两次,只需要在重写的attemptauthentication方法中检查是否json登录,然后直接反序列化返回authentication对象即可。这样我们没有破坏原有的获取流程,还是可以重用父类原有的attemptauthentication方法来处理表单登录。
/** * authenticationfilter that supports rest login(json login) and form login. * @author chenhuanming */ public class customauthenticationfilter extends usernamepasswordauthenticationfilter { @override public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception { //attempt authentication when content-type is json if(request.getcontenttype().equals(mediatype.application_json_utf8_value) ||request.getcontenttype().equals(mediatype.application_json_value)){ //use jackson to deserialize json objectmapper mapper = new objectmapper(); usernamepasswordauthenticationtoken authrequest = null; try (inputstream is = request.getinputstream()){ authenticationbean authenticationbean = mapper.readvalue(is,authenticationbean.class); authrequest = new usernamepasswordauthenticationtoken( authenticationbean.getusername(), authenticationbean.getpassword()); }catch (ioexception e) { e.printstacktrace(); new usernamepasswordauthenticationtoken( "", ""); }finally { setdetails(request, authrequest); return this.getauthenticationmanager().authenticate(authrequest); } } //transmit it to usernamepasswordauthenticationfilter else { return super.attemptauthentication(request, response); } } }
封装的authenticationbean类,用了lombok简化代码(lombok帮我们写getter和setter方法而已)
@getter @setter public class authenticationbean { private string username; private string password; }
websecurityconfigureradapter配置
重写filter不是问题,主要是怎么把这个filter加到spring security的众多filter里面。
@override protected void configure(httpsecurity http) throws exception { http .cors().and() .antmatcher("/**").authorizerequests() .antmatchers("/", "/login**").permitall() .anyrequest().authenticated() //这里必须要写formlogin(),不然原有的usernamepasswordauthenticationfilter不会出现,也就无法配置我们重新的usernamepasswordauthenticationfilter .and().formlogin().loginpage("/") .and().csrf().disable(); //用重写的filter替换掉原有的usernamepasswordauthenticationfilter http.addfilterat(customauthenticationfilter(), usernamepasswordauthenticationfilter.class); } //注册自定义的usernamepasswordauthenticationfilter @bean customauthenticationfilter customauthenticationfilter() throws exception { customauthenticationfilter filter = new customauthenticationfilter(); filter.setauthenticationsuccesshandler(new successhandler()); filter.setauthenticationfailurehandler(new failurehandler()); filter.setfilterprocessesurl("/login/self"); //这句很关键,重用websecurityconfigureradapter配置的authenticationmanager,不然要自己组装authenticationmanager filter.setauthenticationmanager(authenticationmanagerbean()); return filter; }
题外话,如果搭自己的oauth2的server,需要让spring security oauth2共享同一个authenticationmanager(源码的解释是这样写可以暴露出这个authenticationmanager,也就是注册到spring ioc)
@override @bean // share authenticationmanager for web and oauth public authenticationmanager authenticationmanagerbean() throws exception { return super.authenticationmanagerbean(); }
至此,spring security就支持表单登录和异步json登录了。
参考来源
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
详解Spring Security如何配置JSON登录
-
SpringBoot + Spring Security 基本使用及个性化登录配置详解
-
如何在Spring中使用编码方式动态配置Bean详解
-
详解如何在spring boot中使用spring security防止CSRF攻击
-
SpringBoot + Spring Security 基本使用及个性化登录配置详解
-
如何在Spring中使用编码方式动态配置Bean详解
-
详解如何在spring boot中使用spring security防止CSRF攻击
-
详解如何在低版本的Spring中快速实现类似自动配置的功能
-
详解基于Spring Cloud几行配置完成单点登录开发
-
详解基于Spring Cloud几行配置完成单点登录开发