SpringSecurity之認證

山人西來發表於2020-11-20

SpringSecurity之認證

1. 鹽值加密

1. 原理概述

SpringSecurity使用的是隨機鹽值加密

隨機鹽是在對密碼摘要之前隨機生成一個鹽,並且會把這個鹽的明文和摘要拼接一起儲存

舉個例子:密碼是pwd,隨機鹽是abc,pwd+abc摘要後的資訊是xyz,最後儲存的密碼就是abcxyz

隨機鹽 同一個密碼,每次摘要後的結果都不同,但是可以根據摘要裡儲存的鹽來校驗摘要和明文密碼是否匹配

在hashpw函式中, 我們可以看到以下這句

real_salt = salt.substring(off + 3, off + 25);

說明我們真正用於鹽值加密的是real_salt, 從而保證了我們生成隨機鹽值也能再校驗時通過相同的規則得到需要的結果

2. 使用說明

1. 加密

  • 首先我們要在SpringSecurity的配置檔案中配置密碼的加密方式
/密碼使用鹽值加密 BCryptPasswordEncoder
//BCrypt.hashpw() ==> 加密
//BCrypt.checkpw() ==> 密碼比較
//我們在資料庫中儲存的都是加密後的密碼, 只有在網頁上輸入時是明文的
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
  • 然後在我們的使用者管理實現類中實現向資料庫新增新使用者(註冊功能) 時對密碼加密
@Override
public Integer addUser(UserDTO user) {
    //先檢視要新增的使用者是否在資料庫中
    String username = user.getUsername();
    UserDTO userByUsername = getUserByUsername(username);
    //如果待插入的使用者存在在資料庫中, 插入0條
    if (null != userByUsername) {
        return 0;
    } else {
        //不存在, 則插入使用者
        //先對密碼進行鹽值加密, SpringSecurity中使用的是隨機鹽值加密
        String hashpw = passwordEncoder.encode(user.getPassword());
        user.setPassword(hashpw);
        return userMapper.addUser(user);
    }
}
  • 在我們提交使用者名稱和密碼的表單之後, 在資料庫中差看我們儲存的使用者名稱和密碼

image-20201118135809970

可以看到, 密碼與我們明文輸入的 123456 完全不同

  • 這裡要注意一點, 設計資料庫時密碼不要少於60位!

2. 認證

講在前面的話:

認證的配置類的 setFilterProcessesUrl("/login") (這裡是自定義過濾器的配置, form方式與其一致)中, url只是我們提交表單或者ajax請求的地址, 不需要在Controller中註冊, 註冊了PostMapping也不會走, 但是會走Get方式, 此時SpringSecurity不會幫我們認證(認為是不安全的提交方式)

1. 頁面成功跳轉的坑

頁面成功跳轉有兩個方法

  • defaultSuccessUrl
  • successForwardUrl

前者是重定向, 後者是轉發, 由於轉發位址列不會變化, 而我們SpringSecurity要求提交表單的方法必須為post(此處也是大坑!切記!), 因此請求型別後者依然為post

此時, 如果我們在addViewControllers中配置了首頁的路徑對映, 同時我們成功後要跳轉到首頁, 使用後一種方法就會報405錯誤, 提示我們請求型別錯誤

有兩種解決方法

  • 使用第一種方法, 可以接受一個get請求的url
  • 配置一個Controller進行Post方式的頁面跳轉

2. 使用驗證碼校驗的坑

驗證碼校驗我在之前的文章中提到過, 這裡就不再贅述

主要說說驗證碼隨認證一起提交的坑

設定提交的url和我們login的form url一致, 注意此時一定要用GET請求提交表單!

如果我們使用相同的url在controller層試圖進行校驗並重定向跳轉, 可以發現根本就不會走我們的controller!

同時, 我們試圖用攔截器攔截響應的url, 並在表單提交之前攔截來下進行校驗, 也失敗了

說明SpringSecurity自己的校驗的優先順序相當的高

此時, 我們只能實現一個認證成功的處理器來處理我們的驗證碼

  • 實現AuthenticationSuccessHandler介面並用SpringBoot託管
package com.wang.spring_security_framework.config.SpringSecurityConfig;

import com.wang.spring_security_framework.service.CaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//登入成功處理, 用於比對驗證碼
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    CaptchaService captchaService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //校驗驗證碼
        Boolean verifyResult = captchaService.versifyCaptcha(request.getParameter("token"),
                request.getParameter("inputCode"));
        if (verifyResult) {
            response.sendRedirect("/index");
        } else {
            response.sendRedirect("/toLoginPage");
        }
    }
}
  • 在SpringSecurity的配置類中使用我們自己定義的處理類
@Override
protected void configure(HttpSecurity http) throws Exception {
    //指定自定義的登入頁面, 表單提交的url, 以及成功後的處理器
    http.formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            .successHandler(loginSuccessHandler)
            .and()
            .csrf()
            .disable();
}

此處有個大坑, 如果設定了成功的處理類, 我們就千萬不要在配置類中寫成功跳轉的方法了, 這樣會覆蓋掉我們的成功處理器!

3. 前端用ajax請求並附加驗證碼校驗

此處為天坑! 足足費了我快一天半才爬出來! 簡直到處都是坑, 還有一個問題沒解決...

總之不推薦這麼幹, 主要指用AJAX請求再用後臺跳轉

  • 首先, 我們要明確一點, AJAX會重新整理區域性頁面, 這就造成了重定向請求沒問題, 但是頁面不跳轉, 看請求頭我們會發現url還是當前頁面
  • 其次, SpringSecurity的認證是用request.getparameter()讀出的, 因此無法解析AJAX請求傳來的JSON, 我們要自己寫過濾器解析
  • 最後, SpringSecurity在認證過濾器結束後會關閉request的Stream, 導致我們無法取出前端發來的資料, 需要我們再新增一個request, 再在成功的處理器中獲得request中的物件

好了, 讓我們來看看這個坑吧!

  • 前端程式碼

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登入介面</title>
        <link th:href="@{css/default.css}" rel="stylesheet" type="text/css"/>
        <!--必要樣式-->
        <link th:href="@{css/styles.css}" rel="stylesheet" type="text/css"/>
        <link th:href="@{css/demo.css}" rel="stylesheet" type="text/css"/>
        <link th:href="@{css/loaders.css}" rel="stylesheet" type="text/css"/>
    </head>
    <body>
    <div class='login'>
        <div class='login_title'>
            <span>登入</span>
        </div>
        <div class='login_fields'>
            <!--        <form action="/login" method="post">-->
            <div class='login_fields__user'>
                <div class='icon'>
                    <img alt="" src='img/user_icon_copy.png'>
                </div>
                <input name="username" placeholder='使用者名稱' maxlength="16" type='text' autocomplete="off"/>
                <div class='validation'>
                    <img alt="" src='img/tick.png'>
                </div>
            </div>
            <div class='login_fields__password'>
                <div class='icon'>
                    <img alt="" src='img/lock_icon_copy.png'>
                </div>
                <input name="password" placeholder='密碼' maxlength="16" type='text' autocomplete="off">
                <div class='validation'>
                    <img alt="" src='img/tick.png'>
                </div>
            </div>
            <div class='login_fields__password'>
                <div class='icon'>
                    <img alt="" src='img/key.png'>
                </div>
                <input name="inputCode" placeholder='驗證碼' maxlength="4" type='text' autocomplete="off">
                <div class='validation' style="opacity: 1; top: -3px;">
                    <!-- 當使用者連結時,void(0)計算為0,使用者點選不會發生任何效果 -->
                    <a href="javascript:void(0);" title="點選更換驗證碼">
                        <!--this引數, 返回當前的DOM元素-->
                        <img src="" alt="更換驗證碼" id="imgVerify" onclick="getVerify(this)">
                    </a>
                </div>
            </div>
            <div class='login_fields__submit'>
                <input type='button' value='登入'>
            </div>
            <div>
                <!--通過隱藏域傳遞值, 在下面的驗證碼點選事件中, 將值繫結過來, 這樣就可以獲得最新的驗證碼對應的值了!-->
                <input name="token" value="" type="hidden" id="token">
            </div>
            <!--        </form>-->
        </div>
    </div>
    
    <link th:href="@{layui/css/layui.css}" rel="stylesheet" type="text/css"/>
    
    <script type="text/javascript" th:src="@{js/jquery.min.js}"></script>
    <script type="text/javascript" th:src="@{js/jquery-ui.min.js}"></script>
    <script type="text/javascript" th:src="@{layui/layui.js}"></script>
    <script type="text/javascript" th:src="@{js/Particleground.js}"></script>
    <script type="text/javascript" th:src="@{js/Treatment.js}"></script>
    <script type="text/javascript" th:src="@{js/jquery.mockjax.js}"></script>
    <script type="text/javascript">
        $(document).keypress(function (e) {
            // Enter鍵事件 ascii 13
            if (e.which === 13) {
                $('input[type="button"]').click();
            }
        });
    
        //粒子背景特效
        $('body').particleground({
            dotColor: '#39db24',
            lineColor: '#133b88'
        });
        $('input[name="password"]').focus(function () {
            $(this).attr('type', 'password');
        });
        $('input[type="text"]').focus(function () {
            $(this).prev().animate({'opacity': '1'}, 200);
        });
        $('input[type="text"],input[type="password"]').blur(function () {
            $(this).prev().animate({'opacity': '.5'}, 200);
        });
        $('input[name="username"],input[name="password"]').keyup(function () {
            var Len = $(this).val().length;
            if (!$(this).val() === '' && Len >= 5) {
                $(this).next().animate({
                    'opacity': '1',
                    'right': '30'
                }, 200);
            } else {
                $(this).next().animate({
                    'opacity': '0',
                    'right': '20'
                }, 200);
            }
        });
    
        layui.use('layer', function () {
            //非空驗證
            $('input[type="button"]').click(function () {
                let login = $('input[name="username"]').val();
                let pwd = $('input[name="password"]').val();
                let code = $('input[name="inputCode"]').val();
                let token = $('input[name="token"]').val();
                let JsonData = {"username": login, "password": pwd, "inputCode": code, "token": token};
                if (login === '') {
                    ErroAlert('請輸入您的賬號');
                } else if (pwd === '') {
                    ErroAlert('請輸入密碼');
                } else if (code === '' || code.length !== 4) {
                    ErroAlert('輸入驗證碼');
                } else {
                    let url = "/login";
                    $.ajaxSetup({
                        url: url,
                        type: "post",
                        dataType: "json",
                        contentType: "application/json;charset=utf-8",
                        complete: function (XMLHttpRequest, textStatus) {
                            console.log(XMLHttpRequest.status);
                            //通過XMLHttpRequest獲取響應頭
                            let redirect = XMLHttpRequest.getResponseHeader("REDIRECT");
                            console.log(redirect);
                            if (redirect === "REDIRECT") {
                                let win = window;
                                while (win != win.top) {
                                    win = win.top;
                                }
                                win.location.href = XMLHttpRequest.getResponseHeader("CONTEXTPATH");
                            }
                        }
                    });
                    $.ajax({
                        data: JSON.stringify(JsonData),
                        success: function () {
                            console.log("進入回撥函式了!");
                        },
                        error: function (xhr, textStatus, errorThrown) {
                            alert("進入error---");
                            alert("狀態碼:"+xhr.status);
                            alert("狀態:"+xhr.readyState); //當前狀態,0-未初始化,1-正在載入,2-已經載入,3-資料進行互動,4-完成。
                            alert("錯誤資訊:"+xhr.statusText );
                            alert("返回響應資訊:"+xhr.responseText );//這裡是詳細的資訊
                            alert("請求狀態:"+textStatus);
                            alert(errorThrown);
                            alert("請求失敗");
                        }
                    });
                }
            });
        });
        //獲得img物件
        let imgVerify = $("#imgVerify").get(0);
        //$(function())等同於$(document).ready(function()) ==> 頁面載入完畢之後, 才執行函式
        $(function () {
            getVerify(imgVerify);
        });
    
        //onclick時間繫結的getVerify函式
        function getVerify(obj) {
            $.ajax({
                type: "POST",
                url: "/captcha",
                success: function (result) {
                    obj.src = "data:image/jpeg;base64," + result.img;
                    $("#token").val(result.token);
                }
            });
        }
    </script>
    
    </body>
    </html>
    
    • 這裡主要是$.ajaxSetup()方法, 可以定義全域性的(同一個函式中的)ajax的一些引數, 尤其是裡面的complete方法, 是在全部執行完之後呼叫的, 為了能強行跳轉AJAX, 我們要天劍請求頭, 我們在後面的後端程式碼中可以看到
    • 我們還需要寫$.ajax()傳遞資料, 注意, json資料就算我們用json的格式寫了, 還是要用JSON.stringify()方法轉一下, 否則傳到後端的不是JSON!
    • 此處有一個沒有解決的問題, 不知道為什麼不會走成功的回撥函式, 只會走失敗的回撥函式
  • 自定義認證過濾器

    package com.wang.spring_security_framework.config.SpringSecurityConfig;
    
    import com.alibaba.fastjson.JSON;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Map;
    
    //預設的提取使用者名稱和密碼是通過 request.getParameter() 方法來提取的, 所以通過form我們可以提取到
    //但是如果我們用ajax傳遞的話, 就提取不到了, 需要自己寫過濾器!
    //這裡不能寫 @Component, 因為我們要在SpringSecurity配置類中註冊 myCustomAuthenticationFilter 並配置
    //否則會爆出重名的Bean!
    public class MyCustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            //如果request請求是一個json同時編碼方式為UTF-8
            if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)
                    || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
                UsernamePasswordAuthenticationToken authRequest = null;
                
                Map<String, String> authenticationBean = null;
                try (InputStream inputStream = request.getInputStream()) {
                    //將JSON轉為map
                    authenticationBean = JSON.parseObject(inputStream, Map.class);
                    //將使用者名稱和密碼放入 authRequest
                    authRequest = new UsernamePasswordAuthenticationToken(
                            authenticationBean.get("username"), authenticationBean.get("password"));
                    System.out.println(authenticationBean);
                } catch (IOException e) {
                    e.printStackTrace();
                    //出現IO異常, 放空的使用者資訊
                    authRequest = new UsernamePasswordAuthenticationToken("", "");
                } finally {
                    //將請求 request 和解析後的使用者資訊 authRequest 放入userDetails中
                    setDetails(request, authRequest);
                    //將我們前端傳遞的JSON物件繼續放在request裡傳遞, 這樣我們就可以在認證成功的處理器中拿到它了!
                    request.setAttribute("authInfo", authenticationBean);
    
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
            } else {
                return super.attemptAuthentication(request, response);
            }
        }
    }
    
    • 這裡還是要強調一點, @Component會自動註冊內部的全部的方法, 如果我們在別的地方@Bean了方法, 會報一些奇怪的錯誤, 本質上是衝突了!
    • 此處我們是用FastJSON將JSON轉為了Map
  • 認證成功處理器

    package com.wang.spring_security_framework.config.SpringSecurityConfig;
    
    import com.alibaba.fastjson.JSON;
    import com.wang.spring_security_framework.service.CaptchaService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    //登入成功處理
    //我們不能在這裡獲得request了, 因為我們已經在前面自定義了認證過濾器, 做完後SpringSecurity會關閉inputStream流
    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Autowired
        CaptchaService captchaService;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            //我們從自定義的認證過濾器中拿到的authInfo, 接下來做驗證碼校驗和跳轉
            Map<String, String> authInfo = (Map<String, String>) request.getAttribute("authInfo");
            System.out.println(authInfo);
            System.out.println("success!");
            String token = authInfo.get("token");
            String inputCode = authInfo.get("inputCode");
    
            //校驗驗證碼
            Boolean verifyResult = captchaService.versifyCaptcha(token, inputCode);
            System.out.println(verifyResult);
            if (verifyResult) {
                HashMap<String, String> map = new HashMap<>();
                map.put("url", "/index");
                System.out.println(map);
                String VerifySuccessUrl = "/index";
                response.setHeader("Content-Type", "application/json;charset=utf-8");
    //            response.setContentType("application/json;charset=utf-8");
                response.addHeader("REDIRECT", "REDIRECT");
                response.addHeader("CONTEXTPATH", VerifySuccessUrl);
            } else {
                String VerifyFailedUrl = "/toRegisterPage";
                response.setHeader("Content-Type", "application/json;charset=utf-8");
    //            response.setContentType("application/json;charset=utf-8");
                response.addHeader("REDIRECT", "REDIRECT");
                response.addHeader("CONTEXTPATH", VerifyFailedUrl);
    //            response.sendRedirect("/toRegisterPage");
            }
        }
    }
    
    • 這裡需要注意一點, 我們需要從前面的Request拿到物件
    • addHeader裡面我們為了重定向, 新增了響應頭, 可以和前端的ajaxSetup對應著看
  • SpringSecurity配置類

    package com.wang.spring_security_framework.config;
    
    import com.wang.spring_security_framework.config.SpringSecurityConfig.LoginSuccessHandler;
    import com.wang.spring_security_framework.config.SpringSecurityConfig.MyCustomAuthenticationFilter;
    import com.wang.spring_security_framework.service.UserService;
    import com.wang.spring_security_framework.service.serviceImpl.UserDetailServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    //SpringSecurity設定
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        UserService userService;
        @Autowired
        UserDetailServiceImpl userDetailServiceImpl;
        @Autowired
        LoginSuccessHandler loginSuccessHandler;
    
        //授權
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //指定自定義的登入頁面, 表單提交的url, 以及成功後的處理器
            http.formLogin()
                    .loginPage("/toLoginPage")
                    .failureForwardUrl("/index")
                    .and()
                    .csrf()
                    .disable();
    //        .failureForwardUrl();
            //登出
    
            //設定過濾器鏈, 新增自定義過濾器
            http.addFilterAt(
                    myCustomAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class
            );
            //允許iframe
    //        http.headers().frameOptions().sameOrigin();
        }
    
        //註冊自定義過濾器
        @Bean
        MyCustomAuthenticationFilter myCustomAuthenticationFilter() throws Exception {
            MyCustomAuthenticationFilter filter = new MyCustomAuthenticationFilter();
            //設定過濾器認證管理
            filter.setAuthenticationManager(super.authenticationManagerBean());
            //設定filter的url
            filter.setFilterProcessesUrl("/login");
            //設定登入成功處理器
            filter.setAuthenticationSuccessHandler(loginSuccessHandler);
            //TODO 設定登入失敗處理器
    
            return filter;
        }
    
        //密碼使用鹽值加密 BCryptPasswordEncoder
        //BCrypt.hashpw() ==> 加密
        //BCrypt.checkpw() ==> 密碼比較
        //我們在資料庫中儲存的都是加密後的密碼, 只有在網頁上輸入時是明文的
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }
    
    • 這裡主要乾了兩件事
      • 註冊了我們自定義的過濾器
      • 在過濾器鏈中註冊我們的過濾器

4. 後端只提供JSON讓前端進行跳轉

這裡主要修改了兩處, 我們的成功處理器返回的是一個封裝好的JSON, 同時我們在ajax的回撥函式中寫了頁面跳轉的邏輯

  • 成功處理器

    package com.wang.spring_security_framework.config.SpringSecurityConfig;
    
    import com.alibaba.fastjson.JSON;
    import com.wang.spring_security_framework.service.CaptchaService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.HashMap;
    import java.util.Map;
    
    //登入成功處理
    //我們不能在這裡獲得request了, 因為我們已經在前面自定義了認證過濾器, 做完後SpringSecurity會關閉inputStream流
    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Autowired
        CaptchaService captchaService;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            //我們從自定義的認證過濾器中拿到的authInfo, 接下來做驗證碼校驗和跳轉
            Map<String, String> authInfo = (Map<String, String>) request.getAttribute("authInfo");
            System.out.println(authInfo);
            System.out.println("success!");
            String token = authInfo.get("token");
            String inputCode = authInfo.get("inputCode");
    
            //校驗驗證碼
            Boolean verifyResult = captchaService.versifyCaptcha(token, inputCode);
            System.out.println(verifyResult);
    
            Map<String, String> result = new HashMap<>();
            if (verifyResult) {
                HashMap<String, String> map = new HashMap<>();
                map.put("url", "/index");
                System.out.println(map);
                String VerifySuccessUrl = "/index";
                response.setHeader("Content-Type", "application/json;charset=utf-8");
                result.put("code", "200");
                result.put("msg", "認證成功!");
                result.put("url", VerifySuccessUrl);
                PrintWriter writer = response.getWriter();
                writer.write(JSON.toJSONString(result));
            } else {
                String VerifyFailedUrl = "/toLoginPage";
                response.setHeader("Content-Type", "application/json;charset=utf-8");
                result.put("code", "201");
                result.put("msg", "驗證碼輸入錯誤!");
                result.put("url", VerifyFailedUrl);
                PrintWriter writer = response.getWriter();
                writer.write(JSON.toJSONString(result));
            }
        }
    }
    
    • 這裡只需要注意一點, 及時ContentType一定要加上, 防止出現奇怪的響應頭的問題
  • 前端修改, 這裡刪除了complete方法, 新增了回撥函式, 因此我們只放出ajax

    $.ajax({
        data: JSON.stringify(JsonData),
        success: function (data) {
            alert("進入success---");
            let code = data.code;
            let url = data.url;
            let msg = data.msg;
            if (code == 200) {
                alert(msg);
                window.location.href = url;
            } else if (code == 201) {
                alert(msg);
                window.location.href = url;
            } else {
                alert("未知錯誤!")
            }
        },
        error: function (xhr, textStatus, errorThrown) {
            alert("進入error---");
            alert("狀態碼:" + xhr.status);
            alert("狀態:" + xhr.readyState); //當前狀態,0-未初始化,1-正在載入,2-已經載入,3-資料進行互動,4-完成。
            alert("錯誤資訊:" + xhr.statusText);
            alert("返回響應資訊:" + xhr.responseText);//這裡是詳細的資訊
            alert("請求狀態:" + textStatus);
            alert(errorThrown);
            alert("請求失敗");
        }
    });
    

5. 失敗處理器

認證失敗的處理器, 主要是三個部分, 失敗處理器, 配置類中自定義過濾器新增失敗處理器, 以及前端新增回撥函式的失敗處理器的跳轉邏輯

其中配置類和前端都非常簡單, 我們這裡只貼出失敗處理器供大家參考

package com.wang.spring_security_framework.config.SpringSecurityConfig;

import com.alibaba.fastjson.JSON;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

//認證失敗的處理器
@Component
public class LoginFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        HashMap<String, String> result = new HashMap<>();
        String AuthenticationFailUrl = "/toRegisterPage";
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        result.put("code", "202");
        result.put("msg", "認證失敗!密碼或使用者名稱錯誤!即將跳轉到註冊頁面!");
        result.put("url", AuthenticationFailUrl);
        PrintWriter writer = response.getWriter();
        writer.write(JSON.toJSONString(result));
    }
}

3. 寫在最後的話

  • 本文其實不算是教程, 只是個人在練習SpringSecurity進行認證的踩坑以及總結
  • 當然, 附加驗證碼校驗應該寫在token的自定義類中, 這裡我偷懶了...有機會再補上吧
  • 請忽視我醜陋的AJAX回撥資訊, 這裡的標準做法是定義返回的資訊類

相關文章