当前位置:   article > 正文

spring security token认证_SpringSecurity认证流程详解(附源码)

securityconfig 做 token验证

盐值加密

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() ==> 密码比较//我们在数据库中存储的都是加密后的密码, 只有在网页上输入时是明文的@Beanpublic PasswordEncoder passwordEncoder() {    return new BCryptPasswordEncoder();}` 
  • 然后在我们的用户管理实现类中实现向数据库添加新用户(注册功能) 时对密码加密
`@Overridepublic Integer addUser(UserDTO user) {    //先查看要添加的用户是否在数据库中    String username = user.getUsername();    UserDTO userByUsername = getUserByUsername(username);    //如果待插入的用户存在在数据库中, 插入0if (null != userByUsername) {        return 0;    } else {        //不存在, 则插入用户        //先对密码进行盐值加密, SpringSecurity中使用的是随机盐值加密        String hashpw = passwordEncoder.encode(user.getPassword());        user.setPassword(hashpw);        return userMapper.addUser(user);    }}` 
  • 在我们提交用户名和密码的表单之后, 在数据库中查看我们存储的用户名和密码
160629c6e051d7bb4a6cb545f40ed55b.png

可以看到, 密码与我们明文输入的 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;//登录成功处理, 用于比对验证码@Componentpublic 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的配置类中使用我们自己定义的处理类
`@Overrideprotected 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中的对象

好了, 让我们来看看这个坑吧!

  • 前端代码
    
    `                登录界面
`
*   这里主要是$.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 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 authInfo = (Map) 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 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 authInfo = (Map) 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 result = new HashMap<>();            if (verifyResult) {                HashMap 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;//认证失败的处理器@Componentpublic class LoginFailHandler implements AuthenticationFailureHandler {    @Override    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {        HashMap 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回调信息, 这里的标准做法是定义返回的信息类

如果觉得本文对你有帮助,可以点赞关注支持一下

作者: 山人西来

出处:https://www.cnblogs.com/wang-sky/p/14011660.html

如果觉得本文对你有帮助,可以点赞关注支持一下

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/741900
推荐阅读
相关标签
  

闽ICP备14008679号