查看: 733|回复: 0

[Java学习] Spring Security配置JSON登录

发表于 2017-8-8 08:00:03
句号论坛

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. * Enables subclasses to override the composition of the password, such as by
  3. * including additional values and a separator.
  4. * <p>
  5. * This might be used for example if a postcode/zipcode was required in addition to
  6. * the password. A delimiter such as a pipe (|) should be used to separate the
  7. * password and extended value(s). The <code>AuthenticationDao</code> will need to
  8. * generate the expected password in a corresponding manner.
  9. * </p>
  10. *
  11. * @param request so that request attributes can be retrieved
  12. *
  13. * @return the password that will be presented in the <code>Authentication</code>
  14. * request token to the <code>AuthenticationManager</code>
  15. */
  16. protected String obtainPassword(HttpServletRequest request) {
  17. return request.getParameter(passwordParameter);
  18. }
  19. /**
  20. * Enables subclasses to override the composition of the username, such as by
  21. * including additional values and a separator.
  22. *
  23. * @param request so that request attributes can be retrieved
  24. *
  25. * @return the username that will be presented in the <code>Authentication</code>
  26. * request token to the <code>AuthenticationManager</code>
  27. */
  28. protected String obtainUsername(HttpServletRequest request) {
  29. return request.getParameter(usernameParameter);
  30. }
复制代码
重写UsernamePasswordAnthenticationFilter

上面UsernamePasswordAnthenticationFilter的obtainUsername和obtainPassword方法的注释已经说了,可以让子类来自定义用户名和密码的获取工作。但是我们不打算重写这两个方法,而是重写它们的调用者attemptAuthentication方法,因为json反序列化毕竟有一定消耗,不会反序列化两次,只需要在重写的attemptAuthentication方法中检查是否json登录,然后直接反序列化返回Authentication对象即可。这样我们没有破坏原有的获取流程,还是可以重用父类原有的attemptAuthentication方法来处理表单登录。

  1. /**
  2. * AuthenticationFilter that supports rest login(json login) and form login.
  3. * @author chenhuanming
  4. */
  5. public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
  6. @Override
  7. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  8. //attempt Authentication when Content-Type is json
  9. if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
  10. ||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
  11. //use jackson to deserialize json
  12. ObjectMapper mapper = new ObjectMapper();
  13. UsernamePasswordAuthenticationToken authRequest = null;
  14. try (InputStream is = request.getInputStream()){
  15. AuthenticationBean authenticationBean = mapper.readValue(is,AuthenticationBean.class);
  16. authRequest = new UsernamePasswordAuthenticationToken(
  17. authenticationBean.getUsername(), authenticationBean.getPassword());
  18. }catch (IOException e) {
  19. e.printStackTrace();
  20. authRequest = new UsernamePasswordAuthenticationToken(
  21. "", "");
  22. }finally {
  23. setDetails(request, authRequest);
  24. return this.getAuthenticationManager().authenticate(authRequest);
  25. }
  26. }
  27. //transmit it to UsernamePasswordAuthenticationFilter
  28. else {
  29. return super.attemptAuthentication(request, response);
  30. }
  31. }
  32. }
复制代码

封装的AuthenticationBean类,用了lombok简化代码(lombok帮我们写getter和setter方法而已)

  1. @Getter
  2. @Setter
  3. public class AuthenticationBean {
  4. private String username;
  5. private String password;
  6. }
复制代码
WebSecurityConfigurerAdapter配置

重写Filter不是问题,主要是怎么把这个Filter加到spring security的众多filter里面。

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http
  4. .cors().and()
  5. .antMatcher("/**").authorizeRequests()
  6. .antMatchers("/", "/login**").permitAll()
  7. .anyRequest().authenticated()
  8. //这里必须要写formLogin(),不然原有的UsernamePasswordAuthenticationFilter不会出现,也就无法配置我们重新的UsernamePasswordAuthenticationFilter
  9. .and().formLogin().loginPage("/")
  10. .and().csrf().disable();
  11. //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
  12. http.addFilterAt(customAuthenticationFilter(),
  13. UsernamePasswordAuthenticationFilter.class);
  14. }
  15. //注册自定义的UsernamePasswordAuthenticationFilter
  16. @Bean
  17. CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
  18. CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
  19. filter.setAuthenticationSuccessHandler(new SuccessHandler());
  20. filter.setAuthenticationFailureHandler(new FailureHandler());
  21. filter.setFilterProcessesUrl("/login/self");
  22. //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
  23. filter.setAuthenticationManager(authenticationManagerBean());
  24. return filter;
  25. }
复制代码

题外话,如果搭自己的oauth2的server,需要让spring security oauth2共享同一个AuthenticationManager(源码的解释是这样写可以暴露出这个AuthenticationManager,也就是注册到spring ioc)

  1. @Override
  2. [url=home.php?mod=space&uid=4377]@Bean[/url] // share AuthenticationManager for web and oauth
  3. public AuthenticationManager authenticationManagerBean() throws Exception {
  4. return super.authenticationManagerBean();
  5. }
复制代码

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

参考来源

stackoverflow的问答

其它链接

我的简书



太阳http代理AD
回复

使用道具 举报

关闭

站长推荐上一条 /1 下一条