SpringSecurity使用的是隨機鹽值加密javascript
隨機鹽是在對密碼摘要以前隨機生成一個鹽,而且會把這個鹽的明文和摘要拼接一塊兒保存css
舉個例子:密碼是pwd,隨機鹽是abc,pwd+abc摘要後的信息是xyz,最後保存的密碼就是abcxyzhtml
隨機鹽 同一個密碼,每次摘要後的結果都不一樣,可是能夠根據摘要裏保存的鹽來校驗摘要和明文密碼是否匹配前端
在hashpw函數中, 咱們能夠看到如下這句java
real_salt = salt.substring(off + 3, off + 25);
說明咱們真正用於鹽值加密的是real_salt, 從而保證了咱們生成隨機鹽值也能再校驗時經過相同的規則獲得須要的結果jquery
/密碼使用鹽值加密 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); } }
能夠看到, 密碼與咱們明文輸入的 123456 徹底不一樣web
講在前面的話:ajax
認證的配置類的 setFilterProcessesUrl("/login") (這裏是自定義過濾器的配置, form方式與其一致)中, url只是咱們提交表單或者ajax請求的地址, 不須要在Controller中註冊, 註冊了PostMapping也不會走, 可是會走Get方式, 此時SpringSecurity不會幫咱們認證(認爲是不安全的提交方式)spring
頁面成功跳轉有兩個方法數據庫
前者是重定向, 後者是轉發, 因爲轉發地址欄不會變化, 而咱們SpringSecurity要求提交表單的方法必須爲post(此處也是大坑!切記!), 所以請求類型後者依然爲post
此時, 若是咱們在addViewControllers中配置了首頁的路徑映射, 同時咱們成功後要跳轉到首頁, 使用後一種方法就會報405錯誤, 提示咱們請求類型錯誤
有兩種解決方法
驗證碼校驗我在以前的文章中提到過, 這裏就再也不贅述
主要說說驗證碼隨認證一塊兒提交的坑
設置提交的url和咱們login的form url一致, 注意此時必定要用GET請求提交表單!
若是咱們使用相同的url在controller層試圖進行校驗並重定向跳轉, 能夠發現根本就不會走咱們的controller!
同時, 咱們試圖用攔截器攔截響應的url, 並在表單提交以前攔截來下進行校驗, 也失敗了
說明SpringSecurity本身的校驗的優先級至關的高
此時, 咱們只能實現一個認證成功的處理器來處理咱們的驗證碼
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"); } } }
@Override protected void configure(HttpSecurity http) throws Exception { //指定自定義的登陸頁面, 表單提交的url, 以及成功後的處理器 http.formLogin() .usernameParameter("username") .passwordParameter("password") .loginPage("/toLoginPage") .loginProcessingUrl("/login") .successHandler(loginSuccessHandler) .and() .csrf() .disable(); }
此處有個大坑, 若是設置了成功的處理類, 咱們就千萬不要在配置類中寫成功跳轉的方法了, 這樣會覆蓋掉咱們的成功處理器!
此處爲天坑! 足足費了我快一天半才爬出來! 簡直處處都是坑, 還有一個問題沒解決...
總之不推薦這麼幹, 主要指用AJAX請求再用後臺跳轉
好了, 讓咱們來看看這個坑吧!
前端代碼
<!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>
自定義認證過濾器
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); } } }
認證成功處理器
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"); } } }
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(); } }
這裏主要修改了兩處, 咱們的成功處理器返回的是一個封裝好的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)); } } }
前端修改, 這裏刪除了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("請求失敗"); } });
認證失敗的處理器, 主要是三個部分, 失敗處理器, 配置類中自定義過濾器添加失敗處理器, 以及前端添加回調函數的失敗處理器的跳轉邏輯
其中配置類和前端都很是簡單, 咱們這裏只貼出失敗處理器供你們參考
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)); } }