欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Spring Security如何配置JSON登录

程序员文章站 2023-12-19 18:12:34
spring security用了也有一段时间了,弄过异步和多数据源登录,也看过一点源码,最近弄rest,然后顺便搭oauth2,前端用json来登录,没想到spring...

spring security用了也有一段时间了,弄过异步和多数据源登录,也看过一点源码,最近弄rest,然后顺便搭oauth2,前端用json来登录,没想到spring security默认居然不能获取request中的json数据,谷歌一波后只在*找到一个回答比较靠谱,还是得要重写filter,于是在这里填一波坑。

准备工作

基本的spring security配置就不说了,网上一堆例子,只要弄到普通的表单登录和自定义userdetailsservice就可以。因为需要重写filter,所以需要对spring security的工作流程有一定的了解,这里简单说一下spring security的原理。

详解Spring Security如何配置JSON登录

spring security 是基于javax.servlet.filter的,因此才能在spring mvc(dispatcherservlet基于servlet)前起作用。

  1. usernamepasswordauthenticationfilter:实现filter接口,负责拦截登录处理的url,帐号和密码会在这里获取,然后封装成authentication交给authenticationmanager进行认证工作
  2. authentication:贯穿整个认证过程,封装了认证的用户名,密码和权限角色等信息,接口有一个boolean isauthenticated()方法来决定该authentication认证成功没;
  3. authenticationmanager:认证管理器,但本身并不做认证工作,只是做个管理者的角色。例如默认实现providermanager会持有一个authenticationprovider数组,把认证工作交给这些authenticationprovider,直到有一个authenticationprovider完成了认证工作。
  4. 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登录了。

参考来源


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: