spring security用了也有一段時間了,弄過異步和多數據源登陸,也看過一點源碼,最近弄rest,而後順便搭oauth2,前端用json來登陸,沒想到spring security默認竟然不能獲取request中的json數據,谷歌一波後只在stackoverflow找到一個回答比較靠譜,仍是得要重寫filter,因而在這裏填一波坑。前端
基本的spring security配置就不說了,網上一堆例子,只要弄到普通的表單登陸和自定義UserDetailsService就能夠。由於須要重寫Filter,因此須要對spring security的工做流程有必定的瞭解,這裏簡單說一下spring security的原理。java
spring security 是基於javax.servlet.Filter的,所以才能在spring mvc(DispatcherServlet基於Servlet)前起做用。web
Authentication
交給AuthenticationManager
進行認證工做Authentication
認證成功沒;ProviderManager
會持有一個AuthenticationProvider
數組,把認證工做交給這些AuthenticationProvider
,直到有一個AuthenticationProvider
完成了認證工做。DaoAuthenticationProvider
。咱們在配置時通常重寫一個UserDetailsService
來從數據庫獲取正確的用戶名密碼,其實就是配置了DaoAuthenticationProvider
的UserDetailsService
屬性,DaoAuthenticationProvider
會作賬號和密碼的比對,若是正常就返回給AuthenticationManager
一個驗證成功的Authentication
看UsernamePasswordAuthenticationFilter
源碼裏的obtainUsername和obtainPassword方法只是簡單地調用request.getParameter方法,所以若是用json發送用戶名和密碼會致使DaoAuthenticationProvider
檢查密碼時爲空,拋出BadCredentialsException
。spring
/** * 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
的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; }
重寫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