本文正在参与「金石方案 . 瓜分6万现金大奖」

日积月累,积习沉舟

前言

Spring Security 中,默许的登陆办法是以表单办法进行提交参数的。能够参阅前面的几篇文章,可是在前后端别离的项目,前后端都是以 JSON 办法交互的。一般不会运用表单办法提交参数。所以,在 Spring Security 中假如要运用 JSON 格局登录,需求自己来完成。那本文介绍两种办法运用 JSON 登录。

  • 办法一:重写 UsernamePasswordAuthenticationFilter 过滤器
  • 办法二:自定义登录接口

办法一

经过前面几篇文章的分析,咱们现已知道了登录参数的提取在 UsernamePasswordAuthenticationFilter 过滤器中提取的,因此咱们只需求仿照UsernamePasswordAuthenticationFilter过滤器重写一个过滤器,替代原有的UsernamePasswordAuthenticationFilter过滤器即可。

UsernamePasswordAuthenticationFilter 的源代码如下:

Spring Security 使用JSON格式参数登录的两种方式

重写的逻辑如下:

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 需求是 POST 恳求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        HttpSession session = request.getSession();
        // 取得 session 中的 验证码值
        String sessionVerifyCode = (String) session.getAttribute("verify_code");
        // 判别恳求格局是否是 JSON
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
            }finally {
                String code = loginData.get("code");
                checkVerifyCode(sessionVerifyCode, code);
            }
            String username = loginData.get(getUsernameParameter());
            String password = loginData.get(getPasswordParameter());
            if(StringUtils.isEmpty(username)){
                throw new AuthenticationServiceException("用户名不能为空");
            }
            if(StringUtils.isEmpty(password)){
                throw new AuthenticationServiceException("暗码不能为空");
            }
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }else {
            checkVerifyCode(sessionVerifyCode, request.getParameter("code"));
            return super.attemptAuthentication(request, response);
        }
    }
    private void checkVerifyCode(String sessionVerifyCode, String code) {
        if (StringUtils.isEmpty(code)){
            throw new AuthenticationServiceException("验证码不能为空!");
        }
        if(StringUtils.isEmpty(sessionVerifyCode)){
            throw new AuthenticationServiceException("请重新恳求验证码!");
        }
        if (!sessionVerifyCode.equalsIgnoreCase(code)) {
            throw new AuthenticationServiceException("验证码过错!");
        }
    }
}

上述代码逻辑如下:

  • 1、当时登录恳求是否是 POST 恳求,假如不是,则抛出异常。
  • 2、判别恳求格局是否是 JSON,假如是则走咱们自定义的逻辑,假如不是则调用 super.attemptAuthentication 办法,进入父类原本的处理逻辑中;当然也能够抛出异常。
  • 3、假如是 JSON 恳求格局的数据,经过 ObjectMapper 读取 request 中的 I/O 流,将 JSON 映射到Map 上。
  • 4、从 Map 中取出 code key的值,判别验证码是否正确,假如验证码有错,则直接抛出异常。假如对验证码相关逻辑感到疑惑,请前往:Spring Security 在登录时如何增加图形验证码验证
  • 5、依据用户名、暗码构建 UsernamePasswordAuthenticationToken 对象,然后调用官方的办法进行验证,验证用户名、暗码是否真实有效。

接下来便是将咱们自定义的 LoginFilter 过滤器替代默许的UsernamePasswordAuthenticationFilter

import cn.cxyxj.study05.filter.config.MyAuthenticationEntryPoint;
import cn.cxyxj.study05.filter.config.MyAuthenticationFailureHandler;
import cn.cxyxj.study05.filter.config.MyAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 用自定义的 LoginFilter 实例替代 UsernamePasswordAuthenticationFilter
        http.addFilterBefore(loginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()  //敞开装备
                // 验证码、登录接口放行
                .antMatchers("/verify-code","/auth/login").permitAll()
                .anyRequest() //其他恳求
                .authenticated().and()//验证   表明其他恳求需求登录才干拜访
                .csrf().disable();  // 禁用 csrf 维护
                http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
    }
    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/auth/login");
        loginFilter.setUsernameParameter("account");
        loginFilter.setPasswordParameter("pwd");
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        loginFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        return loginFilter;
    }
}

当咱们替换了UsernamePasswordAuthenticationFilter之后,原本在 SecurityConfig#configure 办法中关于 form 表单的装备就会失效,那些失效的属性,都能够在装备 LoginFilter 实例的时候装备;还需求记得装备AuthenticationManager,不然发动时会报错。

  • MyAuthenticationFailureHandler
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 登录失利回调
 */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        String msg = "";
        if (e instanceof LockedException) {
            msg = "账户被锁定,请联系管理员!";
        }
       else if (e instanceof BadCredentialsException) {
            msg = "用户名或许暗码输入过错,请重新输入!";
        }
        out.write(e.getMessage());
        out.flush();
        out.close();
    }
}
  • MyAuthenticationSuccessHandler
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 登录成功回调
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Object principal = authentication.getPrincipal();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(principal));
        out.flush();
        out.close();
    }
}
  • MyAuthenticationEntryPoint
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * 未登录但拜访需求登录的接口异常回调
 */
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("您未登录,请先登录!");
        out.flush();
        out.close();
    }
}

测试

供给一个事务接口,该接口需求登录才干拜访

@GetMapping("/hello")
public String hello(){
    return "登录成功拜访事务接口";
}

OK,发动项目,先拜访一下 hello 接口。

Spring Security 使用JSON格式参数登录的两种方式
接下来先调用验证码接口,然后再拜访登录接口,如下:

Spring Security 使用JSON格式参数登录的两种方式

再次拜访事务接口!

Spring Security 使用JSON格式参数登录的两种方式

办法二

@PostMapping("/doLogin")
public Object login(@RequestBody LoginReq req) {
    String account = req.getAccount();
    String pwd = req.getPwd();
    String code = req.getCode();
    UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(account, pwd);
    Authentication authentication = authenticationManager.authenticate(authenticationToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication.getPrincipal();
}
public class LoginReq {
    private String account;
    private String pwd;
    private String code;
}

办法二便是在咱们自己的 Controller 层中,编写一个登录接口,接纳用户名、暗码、验证码参数。依据用户名、暗码构建UsernamePasswordAuthenticationToken对象,然后调用官方的办法进行验证,验证用户名、暗码是否真实有效;最终将认证对象放入到 Security 的上下文中。就三行代码就完成了简单的登录功能。

import cn.cxyxj.study05.custom.config.MyAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
        manager.createUser(User.withUsername("security").password("security").roles("user").build());
        return manager;
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()  //敞开装备
                // 验证码、登录接口放行
                .antMatchers("/verify-code","/doLogin").permitAll()
                .anyRequest() //其他恳求
                .authenticated().and()//验证   表明其他恳求需求登录才干拜访
                .csrf().disable();  // 禁用 csrf 维护
       http.exceptionHandling().authenticationEntryPoint(new MyAuthenticationEntryPoint());
    }
}

简简单单的装备一下内存用户,接口放行。

测试

仍是先来拜访一下事务接口,如下:

Spring Security 使用JSON格式参数登录的两种方式
再拜访登录接口,如下:

Spring Security 使用JSON格式参数登录的两种方式

登录成功之后,拜访事务接口,如下:

Spring Security 使用JSON格式参数登录的两种方式


  • 自定义官方过滤器办法,要重写各种接口,比方失利回调、登录成功回调,由于官方现已将这些逻辑单独抽离出来了。需求对认证流程有必定的了解,不然你都不知道为什么需求完成这个接口。
  • 自定义接口办法,只要写好那几行代码,你就能够在后面自定义自己的逻辑,比方:暗码输入过错次数限制,这种办法代码编写起来更流畅一点,不需求这个类写一点代码,那个类写一点代码。

两者之间没有哪种办法更好,看公司、个人的开发习惯吧!但自定义接口办法应该用的会比较多一点,笔者公司用的便是该办法。


  • 如你对本文有疑问或本文有过错之处,欢迎谈论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 保藏。