在使用 SpringSecurity 中,大伙都知道默认的登录数据是通过 key/value 的形式来传递的,默认情况下不支持 JSON格式的登录数据,如果有这种需求,就需要自己来解决,本文主要和小伙伴来聊聊这个话题。
基本登录方案
在说如何使用 JSON 登录之前,我们还是先来看看基本的登录吧,本文为了简单,SpringSecurity 在使用中就不连接数据库了,直接在内存中配置用户名和密码,具体操作步骤如下:
首先创建 SpringBoot 工程,添加 SpringSecurity 依赖,如下:
1 2 3 4 5 6 7 8
| <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>
|
创建 SecurityConfig,完成 SpringSecurity 的配置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| @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 中提供该接口,如下:
1 2 3 4 5 6 7 8 9 10 11 12
| @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
接口,结果如下:
此时先调用登录接口进行登录,如下:
登录成功后,再去访问 /hello
接口就可以成功访问了。
使用JSON登录
上面演示的是一种原始的登录方案,如果想将用户名密码通过 JSON 的方式进行传递,则需要自定义相关过滤器,通过分析源码我们发现,默认的用户名密码提取在 UsernamePasswordAuthenticationFilter 过滤器中,部分源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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);
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 传递用户名密码,只需要将这个过滤器替换掉即可,自定义过滤器如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 中作出如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @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 进行登录了,如下:
好了,本文就先介绍到这里,有问题欢迎留言讨论。