前後端分離中,使用 JSON 格式登錄原來這麼簡單!

做微人事的小夥伴(https://github.com/lenve/vhr),應該都發現了在微人事中有一個極為特殊的請求,那就是登錄。

登錄請求是一個 POST 請求,但是數據傳輸格式是 key/value 的形式。整個項目裡就只有這一個 POST 請求是這樣,其他 POST 請求都是 JSON 格式的數據。

為什麼做成這個樣子呢?還是懶唄。

因為 Spring Security 中默認的登錄數據格式就是 key/value 的形式,一直以來懶得改。最近剛好在錄 Spring Security,就抽空把這裡調整了下,這樣前後端就能統一起來了。

好了,我們一起來看下怎麼實現。

1.服務端接口調整

首先大家知道,用戶登錄的用戶名/密碼是在 UsernamePasswordAuthenticationFilter 類中處理的,具體的處理代碼如下:

<code>public Authentication attemptAuthentication(HttpServletRequest request,
\t\tHttpServletResponse response) throws AuthenticationException {
\tString username = obtainUsername(request);
\tString password = obtainPassword(request);
//省略
}
protected String obtainPassword(HttpServletRequest request) {
\treturn request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
\treturn request.getParameter(usernameParameter);
}/<code>

從這段代碼中,我們就可以看出來為什麼 Spring Security 默認是通過 key/value 的形式來傳遞登錄參數,因為它處理的方式就是 request.getParameter。

所以我們要定義成 JSON 的,思路很簡單,就是自定義來定義一個過濾器代替 UsernamePasswordAuthenticationFilter ,然後在獲取參數的時候,換一種方式就行了。

「這裡有一個額外的點需要注意,就是我們的微人事現在還有驗證碼的功能,所以如果自定義過濾器,要連同驗證碼一起處理掉。」

2.自定義過濾器

接下來我們來自定義一個過濾器代替 UsernamePasswordAuthenticationFilter ,如下:

<code>public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String verify_code = (String) request.getSession().getAttribute("verify_code");
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<string> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}finally {
String code = loginData.get("code");
checkCode(response, code, verify_code);
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
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);
} else {
checkCode(response, request.getParameter("code"), verify_code);
return super.attemptAuthentication(request, response);
}
}

public void checkCode(HttpServletResponse resp, String code, String verify_code) {
if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
//驗證碼不正確
throw new AuthenticationServiceException("驗證碼不正確");
}
}
}/<string>/<code>

這段邏輯我們基本上是模仿官方提供的 UsernamePasswordAuthenticationFilter 來寫的,我來給大家稍微解釋下:

  1. 首先登錄請求肯定是 POST,如果不是 POST ,直接拋出異常,後面的也不處理了。
  2. 因為要在這裡處理驗證碼,所以第二步從 session 中把已經下發過的驗證碼的值拿出來。
  3. 接下來通過 contentType 來判斷當前請求是否通過 JSON 來傳遞參數,如果是通過 JSON 傳遞參數,則按照 JSON 的方式解析,如果不是,則調用 super.attemptAuthentication 方法,進入父類的處理邏輯中,也就是說,我們自定義的這個類,既支持 JSON 形式傳遞參數,也支持 key/value 形式傳遞參數。
  4. 如果是 JSON 形式的數據,我們就通過讀取 request 中的 I/O 流,將 JSON 映射到一個 Map 上。
  5. 從 Map 中取出 code,先去判斷驗證碼是否正確,如果驗證碼有錯,則直接拋出異常。驗證碼的判斷邏輯,大家可以參考:松哥手把手教你給微人事添加登錄驗證碼。
  6. 接下來從 Map 中取出 username 和 password,構造 UsernamePasswordAuthenticationToken 對象並作校驗。

過濾器定義完成後,接下來用我們自定義的過濾器代替默認的 UsernamePasswordAuthenticationFilter,首先我們需要提供一個 LoginFilter 的實例:

<code>@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = RespBean.ok("登錄成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
});
loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(exception.getMessage());
if (exception instanceof LockedException) {
respBean.setMsg("賬戶被鎖定,請聯繫管理員!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密碼過期,請聯繫管理員!");

} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("賬戶過期,請聯繫管理員!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("賬戶被禁用,請聯繫管理員!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用戶名或者密碼輸入錯誤,請重新輸入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
return loginFilter;
}/<code>

當我們代替了 UsernamePasswordAuthenticationFilter 之後,原本在 SecurityConfig#configure 方法中關於 form 表單的配置就會失效,那些失效的屬性,都可以在配置 LoginFilter 實例的時候配置。

另外記得配置一個 AuthenticationManager,根據 WebSecurityConfigurerAdapter 中提供的配置即可。

FilterProcessUrl 則可以根據實際情況配置,如果不配置,默認的就是 /login。

最後,我們用自定義的 LoginFilter 實例代替 UsernamePasswordAuthenticationFilter,如下:

<code>@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
//省略
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}/<code>

調用 addFilterAt 方法完成替換操作。

篇幅原因,我這裡只展示了部分代碼,完整代碼小夥伴們可以在 GitHub 上看到:https://github.com/lenve/vhr。

配置完成後,重啟後端,先用 POSTMAN 測試登錄接口,如下:

前後端分離中,使用 JSON 格式登錄原來這麼簡單!

3.前端修改

原本我們的前端登錄代碼是這樣的:

<code>this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.postKeyValueRequest('/doLogin', this.loginForm).then(resp => {
this.loading = false;
//省略
})
} else {
return false;
}
});/<code>

首先我們去校驗數據,在校驗成功之後,通過 postKeyValueRequest 方法來發送登錄請求,這個方法是我自己封裝的通過 key/value 形式傳遞參數的 POST 請求,如下:

<code>export const postKeyValueRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = '';
for (let i in data) {
ret += encodeURIComponent(i) + '=' + encodeURIComponent(data[i]) + '&'
}
return ret;
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
export const postRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params
})

}/<code>

postKeyValueRequest 是我封裝的通過 key/value 形式傳遞參數,postRequest 則是通過 JSON 形式傳遞參數。

所以,前端我們只需要對登錄請求稍作調整,如下:

<code>this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.postRequest('/doLogin', this.loginForm).then(resp => {
this.loading = false;
//省略
})
} else {
return false;
}
});/<code>

配置完成後,再去登錄,瀏覽器按 F12 ,就可以看到登錄請求的參數形式了:

前後端分離中,使用 JSON 格式登錄原來這麼簡單!

好啦,這就是松哥和大家介紹的 SpringSecurity+JSON+驗證碼登錄,「如果覺得還不錯,記得點一下右下角在看哦。」

完整代碼小夥伴們可以在 GitHub 上下載:https://github.com/lenve/vhr


分享到:


相關文章: