在 Spring Security 中,默认的登陆方式是以表单形式进行提交参数的。可以参考前面的几篇文章,但是在前后端分离的项目,前后端都是以 JSON 形式交互的。一般不会使用表单形式提交参数。所以,在 Spring Security 中如果要使用 JSON 格式登录,需要自己来实现。那本文介绍两种方式使用 JSON 登录。
UsernamePasswordAuthenticationFilter 过滤器通过前面几篇文章的分析,我们已经知道了登录参数的提取在 UsernamePasswordAuthenticationFilter 过滤器中提取的,因此我们只需要模仿UsernamePasswordAuthenticationFilter过滤器重写一个过滤器,替代原有的UsernamePasswordAuthenticationFilter过滤器即可。
UsernamePasswordAuthenticationFilter 的源代码如下:

重写的逻辑如下:
- 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
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("验证码错误!");
- }
- }
- }
- 复制代码
上述代码逻辑如下:
super.attemptAuthentication 方法,进入父类原本的处理逻辑中;当然也可以抛出异常。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,否则启动时会报错。
- 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();
- }
- }
- 复制代码
- 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();
- }
-
- }
- 复制代码
- 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 接口。

接下来先调用验证码接口,然后再访问登录接口,如下:

再次访问业务接口!

- @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());
-
- }
- }
- 复制代码
简简单单的配置一下内存用户,接口放行。
- 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();
- }
- }
- 复制代码
还是先来访问一下业务接口,如下:

再访问登录接口,如下:

登录成功之后,访问业务接口,如下:

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