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

其它連接

個人簡書

相關文章
相關標籤/搜索