SpringSecurity之認證


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) {
            // 回車鍵事件 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. 退出登錄(注銷)

退出登錄相比認證簡單了許多, 更多的是根據業務要求配置 Session 以及退出后的清理策略

這里主要有兩個處理器, LogoutHandler 和 LogoutSuccessHandler, 前者用於清理策略的配置, 后者與前面的認證處理器一樣, 一旦配置無法與url配置跳轉同時存在, 同時, 要注意一點, 正常情況下我們是不關閉防csrf的功能的, 因此我們的logout的請求也要用post方式提交!

這里我偷了個懶(又一次!), 只寫了LogoutSuccessHandler, 退出后跳轉到登錄頁面

  • 退出成功處理器

    package com.wang.spring_security_framework.config.SpringSecurityConfig;
    
    import com.alibaba.fastjson.JSON;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    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 LogoutHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            HashMap<String, String> result = new HashMap<>();
            String returnUrl = "/toLoginPage";
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            result.put("code", "202");
            result.put("msg", "退出成功!");
            result.put("url", returnUrl);
            PrintWriter writer = response.getWriter();
            writer.write(JSON.toJSONString(result));
        }
    }
    
  • SpringSecurity配置類

    //退出登錄
    http.logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(logoutHandler)
            //退出時讓Session無效
            .invalidateHttpSession(true);
    
  • 前端頁面

    這里使用了layui的后台模板, 將退出也封裝為一個模板, 同時使用ajax傳遞url以及執行跳轉

    要注意一點, a標簽的href一定要寫, 否則無法點擊, 但是如果我們要綁定onclick事件, 就要讓其無效, 用 href="javascript:void(0);"

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>layout 后台大布局 - Layui</title>
        <link rel="stylesheet" th:href="@{css/layui.css}">
    </head>
    <body class="layui-layout-body">
    <div class="layui-layout layui-layout-admin">
        <div class="layui-header">
            <div class="layui-logo">layui 后台布局</div>
            <!-- 頭部區域(可配合layui已有的水平導航) -->
            <ul class="layui-nav layui-layout-left">
                <li class="layui-nav-item"><a href="">控制台</a></li>
                <li class="layui-nav-item"><a href="">商品管理</a></li>
                <li class="layui-nav-item"><a href="">用戶</a></li>
                <li class="layui-nav-item">
                    <a href="javascript:;">其它系統</a>
                    <dl class="layui-nav-child">
                        <dd><a href="">郵件管理</a></dd>
                        <dd><a href="">消息管理</a></dd>
                        <dd><a href="">授權管理</a></dd>
                    </dl>
                </li>
            </ul>
            <ul class="layui-nav layui-layout-right">
                <li class="layui-nav-item">
                    <a href="javascript:;">
                        <img src="http://t.cn/RCzsdCq" class="layui-nav-img">
                        賢心
                    </a>
                    <dl class="layui-nav-child">
                        <dd><a href="">基本資料</a></dd>
                        <dd><a href="">安全設置</a></dd>
                    </dl>
                </li>
                <li class="layui-nav-item"><a id="logout" href="javascript:void(0);" onclick="logout()">退了</a></li>
            </ul>
        </div>
    
        <div class="layui-side layui-bg-black">
            <div class="layui-side-scroll">
                <!-- 左側導航區域(可配合layui已有的垂直導航) -->
                <ul class="layui-nav layui-nav-tree" lay-filter="test">
                    <li class="layui-nav-item layui-nav-itemed">
                        <a class="" href="javascript:;">所有商品</a>
                        <dl class="layui-nav-child">
                            <dd><a href="javascript:;">列表一</a></dd>
                            <dd><a href="javascript:;">列表二</a></dd>
                            <dd><a href="javascript:;">列表三</a></dd>
                            <dd><a href="">超鏈接</a></dd>
                        </dl>
                    </li>
                    <li class="layui-nav-item">
                        <a href="javascript:;">解決方案</a>
                        <dl class="layui-nav-child">
                            <dd><a href="javascript:;">列表一</a></dd>
                            <dd><a href="javascript:;">列表二</a></dd>
                            <dd><a href="">超鏈接</a></dd>
                        </dl>
                    </li>
                    <li class="layui-nav-item"><a href="">雲市場</a></li>
                    <li class="layui-nav-item"><a href="">發布商品</a></li>
                </ul>
            </div>
        </div>
    
        <!--    <div class="layui-body">-->
        <!--        &lt;!&ndash; 內容主體區域 &ndash;&gt;-->
        <!--        <div style="padding: 15px;">內容主體區域</div>-->
        <!--    </div>-->
    
        <div class="layui-footer">
            <!-- 底部固定區域 -->
            © layui.com - 底部固定區域
        </div>
    </div>
    <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="@{js/jquery.mockjax.js}"></script>
    <script th:src="@{layui.js}"></script>
    <script>
        //JavaScript代碼區域
        layui.use('element', function () {
            var element = layui.element;
    
        });
    
        function logout() {
            layui.use('layer', function () {
                //退出登錄
                layer.confirm('確定要退出么?', {icon: 3, title: '提示'}, function (index) {
                    //do something
                    let url = '/logout';
                    $.ajax({
                        url: url,
                        type: "post",
                        dataType: "json",
                        contentType: "application/json;charset=utf-8",
                        success: function (data) {
                            alert("進入success---");
                            let code = data.code;
                            let url = data.url;
                            let msg = data.msg;
                            if (code == 202) {
                                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("請求失敗");
                        }
                    });
                    layer.close(index);
                });
            });
        }
    </script>
    </body>
    </html>
    

4. 寫在最后的話

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM