在使用 SpringSecurity 中,大伙都知道默认的登录数据是通过 key/value 的形式来传递的,默认情况下不支持 JSON格式的登录数据,如果有这种需求,就需要自己来解决,本文主要和小伙伴来聊聊这个话题。

基本登录方案

在说如何使用 JSON 登录之前,我们还是先来看看基本的登录吧,本文为了简单,SpringSecurity 在使用中就不连接数据库了,直接在内存中配置用户名和密码,具体操作步骤如下:

  • 创建 Spring Boot 工程

首先创建 SpringBoot 工程,添加 SpringSecurity 依赖,如下:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 添加 Security 配置

创建 SecurityConfig,完成 SpringSecurity 的配置,如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Bean
   PasswordEncoder passwordEncoder() {
       return new BCryptPasswordEncoder();
   }
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.inMemoryAuthentication().withUser("zhangsan").password("$2a$10$2O4EwLrrFPEboTfDOtC0F.RpUMk.3q3KvBHRx7XXKUMLBGjOOBs8q").roles("user");
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .anyRequest().authenticated()
               .and()
               .formLogin()
               .loginProcessingUrl("/doLogin")
               .successHandler(new AuthenticationSuccessHandler() {
                   @Override
                   public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                       RespBean ok = RespBean.ok("登录成功!",authentication.getPrincipal());
                       resp.setContentType("application/json;charset=utf-8");
                       PrintWriter out = resp.getWriter();
                       out.write(new ObjectMapper().writeValueAsString(ok));
                       out.flush();
                       out.close();
                   }
               })
               .failureHandler(new AuthenticationFailureHandler() {
                   @Override
                   public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                       RespBean error = RespBean.error("登录失败");
                       resp.setContentType("application/json;charset=utf-8");
                       PrintWriter out = resp.getWriter();
                       out.write(new ObjectMapper().writeValueAsString(error));
                       out.flush();
                       out.close();
                   }
               })
               .loginPage("/login")
               .permitAll()
               .and()
               .logout()
               .logoutUrl("/logout")
               .logoutSuccessHandler(new LogoutSuccessHandler() {
                   @Override
                   public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                       RespBean ok = RespBean.ok("注销成功!");
                       resp.setContentType("application/json;charset=utf-8");
                       PrintWriter out = resp.getWriter();
                       out.write(new ObjectMapper().writeValueAsString(ok));
                       out.flush();
                       out.close();
                   }
               })
               .permitAll()
               .and()
               .csrf()
               .disable()
               .exceptionHandling()
               .accessDeniedHandler(new AccessDeniedHandler() {
                   @Override
                   public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
                       RespBean error = RespBean.error("权限不足,访问失败");
                       resp.setStatus(403);
                       resp.setContentType("application/json;charset=utf-8");
                       PrintWriter out = resp.getWriter();
                       out.write(new ObjectMapper().writeValueAsString(error));
                       out.flush();
                       out.close();
                   }
               });

   }
}

这里的配置虽然有点长,但是很基础,配置含义也比较清晰,首先提供 BCryptPasswordEncoder 作为 PasswordEncoder,可以实现对密码的自动加密加盐,非常方便,然后提供了一个名为 zhangsan 的用户,密码是 123 ,角色是 user ,最后配置登录逻辑,所有的请求都需要登录后才能访问,登录接口是 /doLogin ,用户名的 key 是 username,密码的 key 是 password,同时配置登录成功、登录失败以及注销成功、权限不足时都给用户返回 JSON 提示,另外,这里虽然配置了登录页面为 /login ,实际上这不是一个页面,而是一段 JSON,在 LoginController 中提供该接口,如下:

@RestController
@ResponseBody
public class LoginController {
   @GetMapping("/login")
   public RespBean login() {
       return RespBean.error("尚未登录,请登录");
   }
   @GetMapping("/hello")
   public String hello() {
       return "hello";
   }
}

这里 /login 只是一个 JSON 提示,而不是页面, /hello 则是一个测试接口。

OK,做完上述步骤就可以开始测试了,运行 SpringBoot 项目,访问 /hello 接口,结果如下:

SpringSecurity 使用 JSON 格式登录_SpringSecurity

此时先调用登录接口进行登录,如下:

SpringSecurity 使用 JSON 格式登录_SpringSecurity _02

登录成功后,再去访问 /hello 接口就可以成功访问了。

使用 JSON 登录

上面演示的是一种原始的登录方案,如果想将用户名密码通过 JSON 的方式进行传递,则需要自定义相关过滤器,通过分析源码我们发现,默认的用户名密码提取在 UsernamePasswordAuthenticationFilter 过滤器中,部分源码如下:

public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter
{
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException
{
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

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

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

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}

protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
   //...
   //...
}

从这里可以看到,默认的 用户名/密码 提取就是通过 request 中的 getParameter 来提取的,如果想使用 JSON 传递用户名密码,只需要将这个过滤器替换掉即可,自定义过滤器如下:

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
       if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
               || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
           ObjectMapper mapper = new ObjectMapper();
           UsernamePasswordAuthenticationToken authRequest = null;
           try (InputStream is = request.getInputStream()) {
               Map<String,String> authenticationBean = mapper.readValue(is, Map.class);
               authRequest = new UsernamePasswordAuthenticationToken(
                       authenticationBean.get("username"), authenticationBean.get("password"));
           } catch (IOException e) {
               e.printStackTrace();
               authRequest = new UsernamePasswordAuthenticationToken(
                       "", "");
           } finally {
               setDetails(request, authRequest);
               return this.getAuthenticationManager().authenticate(authRequest);
           }
       }
       else {
           return super.attemptAuthentication(request, response);
       }
   }
}

这里只是将用户名/密码的获取方案重新修正下,改为了从 JSON 中获取用户名密码,然后在 SecurityConfig 中作出如下修改:

@Override
protected void configure(HttpSecurity http) throws Exception {
   http.authorizeRequests().anyRequest().authenticated()
           .and()
           .formLogin()
           .and().csrf().disable();
   http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
   CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
   filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
       @Override
       public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
           resp.setContentType("application/json;charset=utf-8");
           PrintWriter out = resp.getWriter();
           RespBean respBean = RespBean.ok("登录成功!");
           out.write(new ObjectMapper().writeValueAsString(respBean));
           out.flush();
           out.close();
       }
   });
   filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
       @Override
       public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
           resp.setContentType("application/json;charset=utf-8");
           PrintWriter out = resp.getWriter();
           RespBean respBean = RespBean.error("登录失败!");
           out.write(new ObjectMapper().writeValueAsString(respBean));
           out.flush();
           out.close();
       }
   });
   filter.setAuthenticationManager(authenticationManagerBean());
   return filter;
}

将自定义的 CustomAuthenticationFilter 类加入进来即可,接下来就可以使用 JSON 进行登录了,如下:

SpringSecurity 使用 JSON 格式登录_SpringSecurity _03

转自:https://mp.weixin.qq.com/s/QApGe7I94lAMiYy8qbLZ9A