spring security用了也有一段時間了,弄過異步和多數據源登錄,也看過一點源碼,最近弄rest,然后順便搭oauth2,前端用json來登錄,沒想到spring security默認居然不能獲取request中的json數據,谷歌一波后只在stackoverflow找到一個回答比較靠譜,還是得要重寫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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/** * 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方法來處理表單登錄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/** * 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 ||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方法而已)
1
2
3
4
5
6
|
@Getter @Setter public class AuthenticationBean { private String username; private String password; } |
WebSecurityConfigurerAdapter配置
重寫Filter不是問題,主要是怎么把這個Filter加到spring security的眾多filter里面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@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)
1
2
3
4
5
|
@Override @Bean // share AuthenticationManager for web and oauth public AuthenticationManager authenticationManagerBean() throws Exception { return super .authenticationManagerBean(); } |
至此,spring security就支持表單登錄和異步json登錄了。
參考來源
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.jianshu.com/p/693914564406?utm_source=tuicool&utm_medium=referral