登錄方式匯總
先講講傳統(tǒng)的登錄方式
1.Cookie方案
用cookie作為媒介存放用戶憑證。
用戶登錄系統(tǒng)之后,會返回一個加密的cookie,當用戶訪問子應用的時候會帶上這個cookie,授權以解密cookie并進行校驗,校驗通過后即可登錄當前用戶。
缺點:
Cookie不安全,Cookie是存到客戶端的,攻擊者可以偽造Cookie偽造成特定用戶。
2.傳統(tǒng)Session方案
最經典的一種
因為HTTP協(xié)議是一種無狀態(tài)的協(xié)議,這就意味著,某個用戶登錄之后,下一次他再請求時,用戶還得登錄,因為客戶端和服務器是多對一的關系,所以我們不知道那些用戶登錄了,哪些沒登錄。
傳統(tǒng)解決方式:
1. 用戶第一次發(fā)送登錄請求時,用Session存下來用戶的信息,將SessionID用Cookie傳給客戶端。
2. 之后這個瀏覽器每次訪問服務器的時候,看它的Cookie里面有沒有SessionID,用SessionID找到對應的Session,找到了那就說明登錄了,沒找到就是沒登錄。
簡單示例
SpringBoot項目中寫一個簡單的接口測試一下:
?
訪問接口
可以在瀏覽器的Cookie中看到有JSESSIONID,就是SessionID
缺點
- 用戶多了之后,Session全部存到服務器中,服務器開銷大。
- 分布式的應用中,有多臺服務器,那么Session是存在單個服務器上的,不共享Session,如果用戶在服務器1上登錄之后,下次的請求跳轉到服務器2上了,就需要再次登錄。
3.分布式Session方案
為了解決分布式系統(tǒng)中,多服務器是不共享session,傳統(tǒng)的單機session不適用于分布式系統(tǒng)中,所以這里使用分布式session。
實現分布式session有四種方案:
- 數據庫統(tǒng)一存儲
- session復制
- 客戶端存儲
- HASH一致性
具體請看:分布式session解決方案_半格咖啡的博客-CSDN博客
流程:
(1) 用戶第一次登錄時,將會話信息(用戶Id和用戶信息),比如以用戶Id為Key,寫入分布式Session;
(2) 用戶再次登錄時,獲取分布式Session,是否有會話信息,如果沒有則調到登錄頁;
(3) 一般采用Cache中間件實現,建議使用Redis,因此它有持久化功能,方便分布式Session宕機后,可以從持久化存儲中加載會話信息;
(4) 存入會話時,可以設置會話保持的時間,比如15分鐘,超過后自動超時;
缺點
(1)服務器壓力增大:通常session是存儲在內存中的,每個用戶通過認證之后都會將session數據保存在服務器的內存中,而當用戶量增大時,服務器的壓力增大。(可以將數據保存在磁盤中)
(2)擴展性不強:如果將來搭建了多個服務器,雖然每個服務器都執(zhí)行的是同樣的業(yè)務邏輯,但是session數據是保存在內存中的(不是共享的),用戶第一次訪問的是服務器1,當用戶再次請求時可能訪問的是另外一臺服務器2,服務器2獲取不到session信息,就判定用戶沒有登陸過。(可以使用分布式session將session在各個集群中保持一致)
(3)CSRF跨站偽造請求攻擊:session是基于cookie進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。
ThreadLocal + 攔截器 + Redis 實現登錄鑒權
就是上面所說的 統(tǒng)一在數據庫存儲的方法。
最終實現的效果
這種方式實現了:允許在分布式環(huán)境中實現線程隔離的單點登錄,確保用戶在不同的請求之間共享登錄狀態(tài),并使用Redis作為分布式會話存儲來保持會話一致性。
其實就是解決了兩個問題
- 分布式環(huán)境中Session不共享的問題。
- 多線程環(huán)境下并發(fā)導致不安全的問題。
解釋
在分布式的環(huán)境下,因為Session是存到服務器上面的,使用session會出現Session不共享數據的問題。那么可以將共享數據存入數據庫中,然后應用服務器就可以去數據庫獲取共享數據。
對于每一次請求,可以在一開始從數據庫里取到數據,然后將其臨時存放在本地的內存里。
考慮到線程安全的問題,所以使用threadlocal進行線程隔離,這樣在本次請求的過程中,就可以隨時獲取到這份共享數據了。
所以,session的替代方案是數據庫,ThreadLocal在這里起輔助的作用。
數據庫不建議用mysql,訪問慢,用Redis的話訪問快。
具體實現流程:
- 在登錄業(yè)務代碼中,當用戶登錄成功時,生成一個登錄憑證存儲到redis中,將憑證中的字符串保存在cookie中返回給客戶端。
- 以后每次的請求,使用一個攔截器攔截請求,從cookie中獲取憑證字符串。
- 將字符串與redis中的憑證進行匹配,獲取用戶信息,將用戶信息存儲到ThreadLocal中,在本次請求中持有用戶信息,即可在后續(xù)操作中使用到用戶信息。
登錄憑證類:
//登錄憑證表
@Data
@ApiModel("登錄憑證類")
public class LoginTicket {
private int id;
private int userId;
//登錄憑證字符串
private String ticket;
private int status;
private Date expired;
}
攔截器
- 攔截每一次請求,從request中獲取cookies。
- 從cookies里面找到 憑證字符串與redis中的憑證進行匹配,獲取用戶信息。
- 將用戶信息存儲到ThreadLocal中,在本次請求中持有用戶信息,即可在后續(xù)操作中使用到用戶信息。
SpringMVC攔截器使用介紹:
1.創(chuàng)建攔截器類
創(chuàng)建一個DemoInterceptor類實現HandlerInterceptor接口。
重寫preHandle(),postHandle(),afterCompletion() 三個方法,如下代碼,我們就創(chuàng)建了一個Spring的攔截器。
- preHandle():在controller執(zhí)行請求之前執(zhí)行
- postHandle():在controller執(zhí)行請求之后執(zhí)行,在模板引擎(例如Thymeleaf)之前執(zhí)行
- afterCompletion() :在模板引擎之后執(zhí)行
2.編寫攔截器配置類
選擇要攔截哪些請求,放行那些資源
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DemoInterceptor())
//放行靜態(tài)資源
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg")
//攔截 注冊 登錄請求
.addPathPatterns("/register","/login");
}
}
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
// 保存登錄信息
// 調用時間:Controller方法處理之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//從cookie中獲取憑證
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null){
//查詢憑證
LoginTicket loginTicket = userService.findLoginTicket(ticket);
//檢查憑證是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())){
//根據憑證查詢用戶
User user = userService.findByUserId(loginTicket.getUserId());
//在本次請求中持有用戶
hostHolder.setUser(user);
//構建用戶認證的結果,并存入SecurityContext,以便于Security進行授權
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), userService.getAuthorities(user.getId()));
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
return true;
}
// 調用時間:Controller方法處理完之后
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//得到當前線程持有的user
User user = hostHolder.getUser();
if (user != null && modelAndView != null){
modelAndView.addObject("loginUser", user);
}
}
// 調用時間:DispatcherServlet進行視圖的渲染之后
// 請求結束,把保存的用戶信息清除掉
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
}
ThreadLocal
他是什么?
- 可以理解成它是一個特殊的map,它的key就是線程本身,value就是你想存儲的數據。
- ThreadLocal本質是以線程為key存儲元素
- ThreadLocal可以把用戶信息保存在線程中,用戶的每一次請求,就是一個線程,保存了用戶信息,方便我們在后續(xù)操作獲取用戶登錄信息。
- 當請求結束,也就是用戶點擊了退出登錄之后,我們會把保存的用戶信息清除掉,防止內存泄漏。
為什么用這個ThreadLocal?
????????解決并發(fā)帶來的問題,有助于確保每個用戶的登錄狀態(tài)在不同的線程中得到正確維護和隔離,防止了混淆、安全問題、并發(fā)問題和會話管理問題,從而提高了系統(tǒng)的可靠性和安全性。
????????為了防止多進程對用戶信息修改造成的數據不一致,因此需要保證每個請求(線程)訪問自己的資源(用戶信息)。隔離起來,就不會發(fā)送資源錯誤的問題了。
????????所以在后續(xù)請求中,這個線程一直是存活的,ThreadLocal里面的數據也是一直在的。
????????當請求處理完,服務器向瀏覽器做出響應之后,這個線程就被銷毀了,我們會把保存的用戶信息清除掉,防止內存泄漏。
/**
*持有用戶信息,用于代替session對象
*/
@Component
public class HostHolder {
//ThreadLocal本質是以線程為key存儲元素
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user){
users.set(user);
}
public User getUser(){
return users.get();
}
public void clear(){
users.remove();
}
}
還有一種常用的:JWT認證
????????登錄功能的實現可以有多種方式,具體取決于開發(fā)人員的需求和技術選擇。有傳統(tǒng)的Session會話機制的,也有JWT的。
????????JSON Web Token 是個令牌,就是通過Json形式作為web應用中的令牌,用于在各方之間安全的將信息作為JSON對象傳輸。
????????JSON Web Token是在各方之間安全地傳輸信息的好方法。
????????因為可以對JWT進行簽名(例如,使用公鑰/私鑰對),所以您可以確保發(fā)件人是他們所說的人。此外,由于簽名是使用標頭和有效負載計算的,因此您還可以驗證內容是否遭到篡改。
JWT是存儲在客戶端的。
流程
- 首先,前端通過Web表單將自己的用戶名和密碼發(fā)送到后端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協(xié)議),從而避免敏感信息被嗅探。
- 后端核對用戶名和密碼成功后,將用戶的id等其他信息作為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接后簽名
- 形成一個JWT(Token)。形成的JWT就是一個形同111.zzz.xxx的字符串。token head.payload.singurater
- 后端將JWT字符串作為登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage或sessionStorage上,退出登錄時前端刪除保存的JWT即可。
- 前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSS和XSRF問題)HEADEF。
- 后端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
- 驗證通過后后端使用JWT中包含的用戶信息進行其他邏輯操作,返回相應結果。
優(yōu)勢
- 傳輸速度快:可以通過URL,POST參數或者在HTTP header發(fā)送,因為數據量小,傳輸速度快。
- 自包含:負載中包含了所有用戶所需要的信息,避免了多次查詢數據庫。
- 跨語言:因為Token是以JSON加密的形式保存在客戶端的,所以JWT是跨語言的,原則上任何web形式都支持。
- 支持分布式:因為是存儲到客戶端的,所以不需要在服務端保存會話信息。
?
結構
token string ====> header.payload.singnature token
JWT令牌組成
- 1.標頭(Header)
- 2.有效負載(Payload)
- 3.簽名(Signature)
- 因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature
?
1.Header(標頭)
- 標頭通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法,例如HMAC SHA256或RSA。它會使用 Base64 編碼組成 JWT 結構的第一部分。
- 注意:Base64是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它并不是一種加密過程。
{
"alg": "HS256",
"typ": "JWT"
}
2.Payload(有效負載)
- 令牌的第二部分是有效負載,其中包含聲明。聲明是有關實體(通常是用戶)和其他數據的聲明。
- 同樣的,它會使用Base64 編碼組成 JWT 結構的第二部分
- 官方不建議放用戶的敏感信息,放些用戶ID、用戶名什么的沒事,不要放密碼。不安全。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3.Signature(簽名)
前面兩部分都是使用 Base64 進行編碼的,即前端可以解開知道里面的信息。
Signature 需要使用編碼后的 header 和 payload以及我們提供的一個密鑰
然后使用 header 中指定的簽名算法(HS256)進行簽名。
也就是:3.Signature = 編碼后的1.Header + 編碼后的2.Payload + 密鑰
簽名的作用是保證 JWT 沒有被篡改過
例如
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);
?
最后一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被竄改。
如果有人對頭部以及負載的內容解碼之后進行修改,再進行編碼,最后加上之前的簽名組合形成新的JWT的話,那么服務器端會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的。
如果要對新的頭部和負載進行簽名,在不知道服務器加密時用的密鑰的話,得出來的簽名也是不一樣的。
?
關于密鑰
通常,密鑰存儲在服務器的配置文件或環(huán)境變量中,以確保只有授權的人員可以訪問它。
密鑰的安全性非常重要,因為如果密鑰泄漏,攻擊者可能會偽造有效的JWT令牌來訪問受保護的資源。
不同的請求,服務器的密鑰通常是相同的。文章來源:http://www.zghlxwxcb.cn/news/detail-733216.html
密鑰管理的方式:文章來源地址http://www.zghlxwxcb.cn/news/detail-733216.html
- 儲在安全的配置文件或環(huán)境變量中:密鑰應該存儲在服務器上的安全位置,而不是硬編碼在代碼中,以避免泄漏。
- 定期輪換密鑰:定期更改密鑰以減小泄漏的風險。如果密鑰不再安全,及時進行輪換。
- 限制訪問:確保只有授權的人員可以訪問密鑰存儲。使用訪問控制和權限管理來限制對密鑰的訪問。
到了這里,關于登錄認證方式匯總,例如ThreadLocal+攔截器+Redis、JWT的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!