SpringSecurity之认证

目录

  • SpringSecurity之认证
  • 1. 盐值加密
  • 1. 原理概述
  • 2. 使用说明
  • 1. 加密
  • 2. 认证
  • 1. 页面成功跳转的坑
  • 2. 使用验证码校验的坑
  • 3. 前端用ajax请求并附加验证码校验
  • 4. 后端只提供JSON让前端进行跳转
  • 5. 失败处理器
  • 3. 退出登录(注销)
  • 4. 写在最后的话

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);
    }
}
  • 在我们提交用户名和密码的表单之后, 在数据库中差看我们存储的用户名和密码

spring security整合jwt做密码错误次数限制 spring security 密码加盐_css

可以看到, 密码与我们明文输入的 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">-->
    <!--        <!– 内容主体区域 –>-->
    <!--        <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回调信息, 这里的标准做法是定义返回的信息类