Spring Security配置JSON登陆

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

准备工做

基本的spring security配置就不说了,网上一堆例子,只要弄到普通的表单登陆和自定义UserDetailsService就能够。由于须要重写Filter,因此须要对spring security的工做流程有必定的了解,这里简单说一下spring security的原理。java

image.png

spring security 是基于javax.servlet.Filter的,所以才能在spring mvc(DispatcherServlet基于Servlet)前起做用。web

  • UsernamePasswordAuthenticationFilter:实现Filter接口,负责拦截登陆处理的url,账号和密码会在这里获取,而后封装成Authentication交给AuthenticationManager进行认证工做
  • Authentication:贯穿整个认证过程,封装了认证的用户名,密码和权限角色等信息,接口有一个boolean isAuthenticated()方法来决定该Authentication认证成功没;
  • AuthenticationManager:认证管理器,但自己并不作认证工做,只是作个管理者的角色。例如默认实现ProviderManager会持有一个AuthenticationProvider数组,把认证工做交给这些AuthenticationProvider,直到有一个AuthenticationProvider完成了认证工做。
  • AuthenticationProvider:认证提供者,默认实现,也是最常使用的是DaoAuthenticationProvider。咱们在配置时通常重写一个UserDetailsService来从数据库获取正确的用户名密码,其实就是配置了DaoAuthenticationProviderUserDetailsService属性,DaoAuthenticationProvider会作账号和密码的比对,若是正常就返回给AuthenticationManager一个验证成功的Authentication

UsernamePasswordAuthenticationFilter源码里的obtainUsername和obtainPassword方法只是简单地调用request.getParameter方法,所以若是用json发送用户名和密码会致使DaoAuthenticationProvider检查密码时为空,抛出BadCredentialsExceptionspring

/**
     * 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();
                authRequest = 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方法而已)json

@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)mvc

@Override
@Bean // share AuthenticationManager for web and oauth
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

至此,spring security就支持表单登陆和异步json登陆了。app

参考来源

stackoverflow的问答cors

其它连接

个人简书