上一节写到,使用 Form-data 表单提交,登陆请求。这一节,我们改造成 json 格式的请求,并且添加登陆验证码参数。
需求描述
登陆时传递三个字段:username
、password
、commonLoginVerifyCode
。
验证码错误,直接返回登陆失败。
解决方案查询
有问题,先找官方文档看看。查阅文档,其针对常见的问题,给出了一些建议。
这打眼一瞅,第一条貌似有点符合啊。我们也是需要添加个自定义参数(验证码)。
提交的登录信息由UsernamePasswordAuthenticationFilter
的实例处理,所以我们可以在这里处理登陆请求的参数。
一种方法是使用自定义身份验证令牌类(不使用默认的UsernamePasswordAuthenticationToken
)。使用自定义身份令牌类,还需要自定义身份验证的过程。需要编写AuthenticationProvider
或扩展DaoAuthenticationProvider
来处理令牌类。
还有一种简单的方法是将多余的字段与用户名连接起来(例如,使用“:”作为分隔符),并将它们传递到UsernamePasswordAuthenticationToken
的username
属性中。这种方式需要实现自己的UserDetailsService
,在其中把username
进行拆分。
解决方案抉择
我们的需求是Json格式。所以拼接方法 pass 掉。
又因为,我们只需要添加验证码字段,比较简单,所以我们也暂时不自定义身份验证自定义令牌类,后面我们需要支持多种登陆方式的时候,我们再使用这种方法。
根据文档的说法,提交的登录信息由UsernamePasswordAuthenticationFilter
的实例处理。所以我们可以自定义一个 filter
继承这个类,然后定义自己的处理方式。
所以最后的解决方案定为:自定义 MyAuthenticationProcessingFilter
extend UsernamePasswordAuthenticationFilter
,在这其中拿到 request 请求,把请求体( json )反序列化成实体类,然后校验验证码,通过后再封装成UsernamePasswordAuthenticationToken
,向后传递。
实现
此章代码实现均在前章代码的基础上进行。
-
定义接收参数的实体
@Data public class LoginData { /** 用户名 */ private String username; /** 密码 */ private String password; /** 普通登陆验证码 */ private String commonLoginVerifyCode; }
-
自定义认证入口过滤器
这个我们可以参考
UsernamePasswordAuthenticationToken
的实现来编写。UsernamePasswordAuthenticationToken#attemptAuthentication
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); }
MyAuthenticationProcessingFilter#attemptAuthentication
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{ // 校验请求方法、请求体格式 if (!request.getMethod().equals(HttpMethod.POST.name())) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { throw new AuthenticationServiceException( "Authentication contentType not supported: " + request.getContentType()); } // 序列化请求体 LoginData loginData = new LoginData(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); try { loginData = objectMapper.readValue(request.getInputStream(), LoginData.class); } catch (IOException e) { throw new AuthenticationServiceException( "loginDataJson to LoginData failed"); } // 模拟校验验证码 String loginVerifyCode = loginData.getCommonLoginVerifyCode(); if (!"111111".equals(loginVerifyCode)) { throw new AuthenticationServiceException("commonLoginVerifyCode is wrong"); } // 传递令牌类 UsernamePasswordAuthenticationToken String username = loginData.getUsername(); String password = loginData.getPassword(); username = username == null ? "":username; password = password == null ? "":password; username = username.trim(); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); setDetails(request,usernamePasswordAuthenticationToken); return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken); }
-
Security 配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MyUserDetailServiceImpl userDetailService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService); } // 配置自定 Filter 的 Bean @Bean MyAuthenticationProcessingFilter myAuthenticationProcessingFilter() throws Exception { MyAuthenticationProcessingFilter filter = new MyAuthenticationProcessingFilter(); // 认证成功的处理办法 filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal(); userDetails.setPassword(null); HashMap<String, Object> result = new HashMap<>(); result.put("code","000000"); result.put("msg","登陆成功"); result.put("data",userDetails); String s = new ObjectMapper().writeValueAsString(result); out.write(s); out.flush(); out.close(); }); // 认证错误的处理办法 filter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); HashMap<String, Object> result = new HashMap<>(); result.put("code","111111"); if (exception instanceof LockedException) { result.put("msg","账户已锁定!"); } else if (exception instanceof DisabledException) { result.put("msg","账户已禁用!"); } else if (exception instanceof BadCredentialsException) { result.put("msg","用户名或者密码输入错误,请重新输入!"); } else if (exception instanceof AuthenticationServiceException) { result.put("msg",exception.getMessage()); } out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); }); filter.setAuthenticationManager(authenticationManagerBean()); filter.setFilterProcessesUrl("/toLogin"); return filter; } // 这个方法可以算是 Security 配置类的配置中心 @Override protected void configure(HttpSecurity http) throws Exception { // 拦截所有请求 http.authorizeRequests() .anyRequest() .authenticated() .and() .csrf() .disable(); // 替换 filter http.addFilterAt(myAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); }
登陆
-
尝试使用表单登陆
{ "msg":"Authentication contentType not supported: multipart/form-data; boundary=--------------------------730516048605803229023191", "code":"111111" }
-
使用 json 登陆,携带错误验证码
{ "msg":"commonLoginVerifyCode is wrong", "code":"111111" }
-
使用 json 登陆,密码错误
{ "msg": "用户名或者密码输入错误,请重新输入!", "code": "111111" }
-
正确
这里还返回了一些没用的信息,实际项目中可以按需去掉。
{ "msg": "登陆成功", "code": "000000", "data": { "id": 1, "phone": null, "username": "张三", "password": null, "accountStatus": 1, "roleList": [ { "id": 1, "name": "admin", "desc": null }, { "id": 2, "name": "user", "desc": null } ], "enabled": true, "authorities": [ { "authority": "admin" }, { "authority": "user" } ], "accountNonLocked": true, "accountNonExpired": true, "credentialsNonExpired": true } }