SpringSecurity登录使用JSON格式数据

SpringSecurity登录使用JSON格式数据

作者:zerouwar

出处:https://www.jianshu.com/p/693914564406

1. 前言

最近在自己鼓捣Spring Security,在用 postman 测试登录接口的时候,发现如果 username、password是以 Json 格式传输的话,是没有办法正常登录的。查阅了资料才知道,Spring Security默认居然不能获取request中的Json数据,但在前后端分离的大潮流下,数据交互使用 Json 格式已经成为了一个很普遍的事情,那么就有必要来自己改造一下了。

2. 准备工作

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

2.1 Spring Security 权限认证流程图

Spring Security 权限认证流程图

2.2 分模块进行解析

  • UsernamePasswordAuthenticationFilter :实现Filter接口,负责拦截登录处理的url,帐号和密码会在这里获取,然后封装成Authentication交给AuthenticationManager进行认证工作;
  • Authentication :贯穿整个认证过程,封装了认证的用户名,密码和权限角色等信息,接口有一个boolean isAuthenticated()方法来决定该Authentication认证成功没;
  • AuthenticationManager :认证管理器,但本身并不做认证工作,只是做个管理者的角色。例如默认实现ProviderManager会持有一个AuthenticationProvider数组,把认证工作交给这些AuthenticationProvider,直到有一个AuthenticationProvider完成了认证工作;
  • AuthenticationProvider :认证提供者,默认实现,也是最常使用的是DaoAuthenticationProvider。我们在配置时一般重写一个UserDetailsService来从数据库获取正确的用户名密码,其实就是配置了DaoAuthenticationProviderUserDetailsService属性,DaoAuthenticationProvider会做帐号和密码的比对,如果正常就返回给AuthenticationManager一个验证成功的Authentication

2.3 UsernamePasswordAuthenticationFilter 源代码

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

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
	/**
     * 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);
    }

3. 重写UsernamePasswordAuthenticationFilter

上面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类
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream ideserialize jsons = request.getInputStream()){
                // deserialize json to get loginInfo
                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);
            }
        }
        // If not,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;
}

4. 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();
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值