详解Spring Security如何配置JSON登录

作者:zerouwar 时间:2023-02-08 17:39:07 

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

准备工作

基本的spring security配置就不说了,网上一堆例子,只要弄到普通的表单登录和自定义UserDetailsService就可以。因为需要重写Filter,所以需要对spring security的工作流程有一定的了解,这里简单说一下spring security的原理。

详解Spring Security如何配置JSON登录

spring security 是基于javax.servlet.Filter的,因此才能在spring mvc(DispatcherServlet基于Servlet)前起作用。

  1. UsernamePasswordAuthenticationFilter:实现Filter接口,负责拦截登录处理的url,帐号和密码会在这里获取,然后封装成Authentication交给AuthenticationManager进行认证工作

  2. Authentication:贯穿整个认证过程,封装了认证的用户名,密码和权限角色等信息,接口有一个boolean isAuthenticated()方法来决定该Authentication认证成功没;

  3. AuthenticationManager:认证管理器,但本身并不做认证工作,只是做个管理者的角色。例如默认实现ProviderManager会持有一个AuthenticationProvider数组,把认证工作交给这些AuthenticationProvider,直到有一个AuthenticationProvider完成了认证工作。

  4. AuthenticationProvider:认证提供者,默认实现,也是最常使用的是DaoAuthenticationProvider。我们在配置时一般重写一个UserDetailsService来从数据库获取正确的用户名密码,其实就是配置了DaoAuthenticationProvider的UserDetailsService属性,DaoAuthenticationProvider会做帐号和密码的比对,如果正常就返回给AuthenticationManager一个验证成功的Authentication

看UsernamePasswordAuthenticationFilter源码里的obtainUsername和obtainPassword方法只是简单地调用request.getParameter方法,因此如果用json发送用户名和密码会导致DaoAuthenticationProvider检查密码时为空,抛出BadCredentialsException。


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


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


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

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

参考来源

stackoverflow的问答

来源:http://www.jianshu.com/p/693914564406?utm_source=tuicool&utm_medium=referral

标签:Spring,Security,JSON
0
投稿

猜你喜欢

  • Android屏蔽软键盘并且显示光标的实例详解

    2023-01-16 16:54:20
  • Android开发获取重力加速度和磁场强度的方法

    2022-05-27 23:07:09
  • 解读在C#中winform程序响应键盘事件的详解

    2023-10-08 09:17:00
  • C#实现的SQL备份与还原功能示例

    2021-09-03 17:53:17
  • 一篇文章带你搞定JAVA反射

    2023-09-18 02:27:51
  • Java枚举类用法实例

    2023-09-25 01:47:34
  • C/C++中typedef的用法大全

    2023-02-11 08:57:47
  • Java Scanner 类的使用小结

    2023-11-29 00:50:39
  • Spring 代理 Bean 获取不到原始 Bean 对象注解解决方法

    2022-10-31 17:06:08
  • c#代码自动修改解决方案下任意文件实例

    2023-01-31 07:27:10
  • C#基于简单工厂模式实现的计算器功能示例

    2023-06-19 12:40:10
  • Java多线程状态及方法实例解析

    2021-09-10 22:49:53
  • Java下利用Jackson进行JSON解析和序列化示例

    2023-09-27 14:37:38
  • Android自定义TitleView标题开发实例

    2023-09-05 18:21:41
  • Kotlin 匿名类实现接口和抽象类的区别详解

    2021-09-04 07:14:58
  • SpringBoot整合ShardingSphere的示例代码

    2022-08-17 22:52:28
  • Android下拉列表spinner的实例代码

    2023-07-31 20:39:47
  • 基于Java8 函数式接口理解及测试

    2021-06-19 21:54:35
  • Android webveiw 出现栈错误解决办法

    2023-11-29 01:54:26
  • Eclipse安装Aptana插件(注意对应版本问题)

    2022-05-26 17:17:55
  • asp之家 软件编程 m.aspxhome.com