【項目實踐】在用安全框架前公条,我想先讓你手擼一個登陸認證
以項目驅(qū)動學習拇囊,以實踐檢驗真知
前言
登錄認證,估計是所有系統(tǒng)中最常見的功能了靶橱,并且也是最基礎寥袭、最重要的功能。為了做好這一塊而誕生了許多安全框架关霸,比如最常見的Shiro传黄、Spring Security等。
本文是一個系列文章队寇,最終的目的是想與大家分享在實際項目中如何運用安全框架完成登錄認證(Authentication)膘掰、權限授權(Authorization)等功能。只不過一上來就講框架的配置和使用我覺得并不好佳遣,一是對于新手來說會很懵逼识埋,二是不利于大家對框架的深入理解啤覆。所以本文先寫 手擼登錄認證基本功能,下一篇文章再寫 不用安全框架手擼權限授權惭聂,最后再寫 如何運用安全框架整合這些功能。
本文會從最簡單相恃、最基礎的講解起辜纲,新手可放心食用。讀完文章你能收獲:
登錄認證的原理
如何使用
Session
和JWT
分別完成登錄認證功能如何使用過濾器和攔截器分別完成對登錄認證的統(tǒng)一處理
如何實現(xiàn)上下文對象
本文所有代碼全部放在了github上拦耐,clone下來即可運行查看效果耕腾。
基礎知識
登錄認證(Authorization)的概念非常簡單,就是通過一定手段對用戶的身份進行確認杀糯。
確認這還不容易扫俺?就是判斷用戶的賬號和密碼是否正確嘛,if固翰、else搞定狼纬。沒錯,這的確很容易骂际,但是確認過后呢疗琉?要知道在web系統(tǒng)中有一個重要的概念就是:HTTP請求是一個無狀態(tài)的協(xié)議。就是說瀏覽器每一次發(fā)送的請求都是獨立的歉铝,對于服務器來說你每次的請求都是“新客”盈简,它不記得你曾經(jīng)有沒有來過。舉一個例子大家就知道了:
A:你早上吃的啥太示?
B:小籠包柠贤。
A:味道咋樣啊类缤?
B:哈臼勉?啥味道咋樣?呀非!
無狀態(tài)坚俗,也可以叫作無記憶,服務器不會記得你之前做了什么岸裙,它只會看到你當前的請求猖败。所以,在Web系統(tǒng)中確認了用戶的身份后降允,還需要有種機制來記住這個用戶已經(jīng)登錄過了恩闻,不然用戶每一次操作都要輸入賬號密碼,那這系統(tǒng)也沒法用了剧董!
那怎樣才能讓服務器記住你的登錄狀態(tài)呢幢尚?那就是憑證破停!登錄之后每一次請求都攜帶一個登錄憑證來告訴服務器我是誰,這樣才能有以下的效果:
A:你早上吃的啥尉剩?
B:小籠包真慢。
A:你早上吃的小籠包味道咋樣啊理茎?
B:味道不錯黑界。
現(xiàn)在流行兩種方式登錄認證方式:Session和JWT,無論是哪種方式其原理都是Token機制皂林,即保存憑證:
- 前端發(fā)起登錄認證請求
- 后端登錄驗證通過朗鸠,返回給前端一個憑證
- 前端發(fā)起新的請求時攜帶憑證
接下來我們就上代碼,用這兩種方式分別實現(xiàn)登錄認證功能
實現(xiàn)
我們使用SpringBoot來搭建Web項目础倍,只需導入Web項目依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
復制代碼
我們再建一個實體類用來模擬用戶:
public class User{
private String username;
private String password;
}
復制代碼
Session
Session烛占,是一種有狀態(tài)的會話管理機制,其目的就是為了解決HTTP無狀態(tài)請求帶來的問題沟启。
當用戶登錄認證請求通過時忆家,服務端會將用戶的信息存儲起來,并生成一個Session Id
發(fā)送給前端德迹,前端將這個Session Id
保存起來(一般是保存在Cookie中)弦赖。之后前端再發(fā)送請求時都攜帶Session Id
,服務器端再根據(jù)這個Session Id
來檢查該用戶有沒有登錄過:
[圖片上傳中...(image-639e1a-1599030254563-14)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
基本功能
接下來我們就用代碼來實現(xiàn)具體功能浦辨,非常簡單蹬竖,我們只需要在用戶登錄的時候?qū)⒂脩粜畔⒋嬖?code>HttpSession中就完成了:
@RestController
public class SessionController {
@PostMapping("login")
public String login(@RequestBody User user, HttpSession session) {
// 判斷賬號密碼是否正確,這一步肯定是要讀取數(shù)據(jù)庫中的數(shù)據(jù)來進行校驗的流酬,這里為了模擬就省去了
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 正確的話就將用戶信息存到session中
session.setAttribute("user", user);
return "登錄成功";
}
return "賬號或密碼錯誤";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 退出登錄就是將用戶信息刪除
session.removeAttribute("user");
return "退出成功";
}
}
復制代碼
在后續(xù)會話中币厕,用戶訪問其他接口就可以檢查用戶是否已經(jīng)登錄認證:
@GetMapping("api")
public String api(HttpSession session) {
// 模擬各種api,訪問之前都要檢查有沒有登錄芽腾,沒有登錄就提示用戶登錄
User user = (User) session.getAttribute("user");
if (user == null) {
return "請先登錄";
}
// 如果有登錄就調(diào)用業(yè)務層執(zhí)行業(yè)務邏輯旦装,然后返回數(shù)據(jù)
return "成功返回數(shù)據(jù)";
}
@GetMapping("api2")
public String api2(HttpSession session) {
// 模擬各種api,訪問之前都要檢查有沒有登錄摊滔,沒有登錄就提示用戶登錄
User user = (User) session.getAttribute("user");
if (user == null) {
return "請先登錄";
}
// 如果有登錄就調(diào)用業(yè)務層執(zhí)行業(yè)務邏輯阴绢,然后返回數(shù)據(jù)
return "成功返回數(shù)據(jù)";
}
復制代碼
我們現(xiàn)在來測試一下效果,先不登錄調(diào)用一下其他接口看看:
[圖片上傳中...(image-dd631d-1599030254563-13)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
可以看到是調(diào)用失敗的艰躺,那我們再進行登錄:
[圖片上傳中...(image-e246b8-1599030254563-12)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
登錄成功后我們再調(diào)用剛才的接口:
[圖片上傳中...(image-ab73f6-1599030254562-11)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
這樣就完成了基本的登錄功能呻袭!是不是相當簡單?
之前說過保持登錄的核心就是憑證腺兴,可上面的代碼也沒看到傳遞憑證的過程呀左电,這是因為這些工作Servlet都幫我們做好了!
如果用戶第一次訪問某個服務器時,服務器響應數(shù)據(jù)時會在響應頭的Set-Cookie
標識里將Session Id
返回給瀏覽器篓足,瀏覽器就將標識中的數(shù)據(jù)存在Cookie
中:
[圖片上傳中...(image-fa328-1599030254562-10)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
瀏覽器后續(xù)訪問服務器就會攜帶Cookie:
[圖片上傳中...(image-e3f514-1599030254562-9)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
每一個Session Id
都對應一個HttpSession
對象段誊,然后服務器就根據(jù)你這個HttpSession
對象來檢測你這個客戶端是否已經(jīng)登錄了,也就是剛才代碼里演示的那樣栈拖。
有人可能會問连舍,前后端分離一般都是用
ajax
跨域請求后端數(shù)據(jù),怎么攜帶cookie呢涩哟。這個很簡單烟瞧,只需要ajax
請求時設置withCredentials=true
就可以跨域攜帶 cookie信息了
過濾器
完成了基本的登錄認證后我們再加強一下功能!除了登錄接口外染簇,我們其他接口都要在Controller層里做登錄判斷,這太麻煩了强岸。我們完全可以對每個接口過濾攔截一下锻弓,判斷有沒有登錄,如果沒有登錄就直接結(jié)束請求蝌箍,登錄了才放行青灼。這里我們通過過濾器來實現(xiàn):
@Component
public class LoginFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 簡單的白名單,登錄這個接口直接放行
if ("/login".equals(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
// 已登錄就放行
User user = (User) request.getSession().getAttribute("user");
if (user != null) {
filterChain.doFilter(request, response);
return;
}
// 走到這里就代表是其他接口妓盲,且沒有登錄
// 設置響應數(shù)據(jù)類型為json(前后端分離)
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// 設置響應內(nèi)容杂拨,結(jié)束請求
out.write("請先登錄");
out.flush();
out.close();
}
}
復制代碼
這時我們Controller層就可以去除多余的登錄判斷邏輯了:
@GetMapping("api")
public String api() {
return "api成功返回數(shù)據(jù)";
}
@GetMapping("api2")
public String api2() {
return "api2成功返回數(shù)據(jù)";
}
復制代碼
重啟服務后我們再調(diào)用一下登錄接口看下效果:
[圖片上傳中...(image-86c4be-1599030254562-8)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
過濾器生效了!
上下文對象
在有些情況下悯衬,就算加了過濾器后我們現(xiàn)在還不能在controller層將session代碼去掉弹沽!因為在實際業(yè)務中對用戶對象操作是非常常見的,而我們的業(yè)務代碼一般都寫在Service業(yè)務層筋粗,那么我們Service層想要操作用戶對象還得從Controller那傳參過來策橘,就像這樣:
@GetMapping("api")
public String api(HttpSession session) {
User user = (User) session.getAttribute("user");
// 將用戶對象傳遞給Service層
userService.doSomething(user);
return "成功返回數(shù)據(jù)";
}
復制代碼
每個接口都要這么寫太麻煩了,有沒有什么辦法可以讓我直接在Service層獲取到用戶對象呢娜亿?當然是可以的丽已,我們可以通過SpringMVC提供的RequestContextHolder
對象在程序任何地方獲取到當前請求對象,從而獲取我們保存在HttpSession
中的用戶對象买决。我們可以寫一個上下文對象來實現(xiàn)該功能:
public class RequestContext {
public static HttpServletRequest getCurrentRequest() {
// 通過`RequestContextHolder`獲取當前request請求對象
return ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
}
public static User getCurrentUser() {
// 通過request對象獲取session對象沛婴,再獲取當前用戶對象
return (User)getCurrentRequest().getSession().getAttribute("user");
}
}
復制代碼
然后我們在Service層直接調(diào)用我們寫的方法就可以獲取到用戶對象啦:
public void doSomething() {
User user = RequestContext.getCurrentUser();
System.out.println("service層---當前登錄用戶對象:" + user);
}
復制代碼
我們再在Controller層直接調(diào)用Service:
@GetMapping("api")
public String api() {
// 各種業(yè)務操作
userService.doSomething();
return "api成功返回數(shù)據(jù)";
}
復制代碼
這樣一套做好之后,看下前端成功調(diào)用api
接口時的效果:
[圖片上傳中...(image-72d4cd-1599030254562-7)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
Service層成功獲取上下文對象督赤!
JWT
除了Session
之外嘁灯,目前比較流行的做法就是使用JWT
(JSON Web Token)。關于JWT
網(wǎng)上有很多講解資料躲舌,一個工具而已會用就行旁仿,所以在這里我就不過多解釋這玩意了,大家只需要知道這兩點就行:
可以將一段數(shù)據(jù)加密成一段字符串,也可以從這字符串解密回數(shù)據(jù)
可以對這個字符串進行校驗枯冈,比如有沒有過期毅贮,有沒有被篡改
有兩上面兩個特性之后就可以用來做登錄認證了。當用戶登錄成功的時候尘奏,服務器生成一個JWT
字符串返回給瀏覽器滩褥,瀏覽器將JWT
保存起來,在之后的請求中都攜帶上JWT
炫加,服務器再對這個JWT
進行校驗瑰煎,校驗通過的話就代表這個用戶登錄了:
[圖片上傳中...(image-eb5e1c-1599030254562-6)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
咦!這不和Session
一樣嘛俗孝,就是把Session Id
換成了JWT
字符串而已酒甸,這圖啥啊。
沒錯赋铝,整體流程來說是一樣的插勤,我之前也說了,無論哪種方式其核心都是TOKEN機制革骨。但农尖,Session
和JWT
有一個重要的區(qū)別,就是Session
是有狀態(tài)的良哲,JWT
是無狀態(tài)的盛卡。
說人話就是,Session
在服務端保存了用戶信息筑凫,而JWT
在服務端沒有保存任何信息滑沧。當前端攜帶Session Id
到服務端時,服務端要檢查其對應的HttpSession
中有沒有保存用戶信息巍实,保存了就代表登錄了嚎货。當使用JWT
時,服務端只需要對這個字符串進行校驗蔫浆,校驗通過就代表登錄了殖属。
至于這兩種方式各有什么好處和壞處先別著急討論,咱先將JWT
用起來瓦盛!兩者的優(yōu)缺點文章最后會做講解滴洗显。
基本功能
要用到JWT
先要導入一個依賴項:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
復制代碼
為了方便使用,我們先寫一個JWT
的工具類原环,工具類就提供兩個方法一個生成一個解析 :
public final class JwtUtil {
/**
* 這個秘鑰是防止JWT被篡改的關鍵挠唆,隨便寫什么都好,但決不能泄露
*/
private final static String secretKey = "whatever";
/**
* 過期時間目前設置成2天嘱吗,這個配置隨業(yè)務需求而定
*/
private final static Duration expiration = Duration.ofHours(2);
/**
* 生成JWT
* @param userName 用戶名
* @return JWT
*/
public static String generate(String userName) {
// 過期時間
Date expiryDate = new Date(System.currentTimeMillis() + expiration.toMillis());
return Jwts.builder()
.setSubject(userName) // 將userName放進JWT
.setIssuedAt(new Date()) // 設置JWT簽發(fā)時間
.setExpiration(expiryDate) // 設置過期時間
.signWith(SignatureAlgorithm.HS512, secretKey) // 設置加密算法和秘鑰
.compact();
}
/**
* 解析JWT
* @param token JWT字符串
* @return 解析成功返回Claims對象玄组,解析失敗返回null
*/
public static Claims parse(String token) {
// 如果是空字符串直接返回null
if (StringUtils.isEmpty(token)) {
return null;
}
// 這個Claims對象包含了許多屬性滔驾,比如簽發(fā)時間、過期時間以及存放的數(shù)據(jù)等
Claims claims = null;
// 解析失敗了會拋出異常俄讹,所以我們要捕捉一下哆致。token過期、token非法都會導致解析失敗
try {
claims = Jwts.parser()
.setSigningKey(secretKey) // 設置秘鑰
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
// 這里應該用日志輸出患膛,為了演示方便就直接打印了
System.err.println("解析失斕А!");
}
return claims;
}
復制代碼
工具類做好之后我們可以開始寫登錄接口了踪蹬,和之前大同小異:
@RestController
public class JwtController {
@PostMapping("/login")
public String login(@RequestBody User user) {
// 判斷賬號密碼是否正確胞此,這一步肯定是要讀取數(shù)據(jù)庫中的數(shù)據(jù)來進行校驗的,這里為了模擬就省去了
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 如果正確的話就返回生成的token(注意哦跃捣,這里服務端是沒有存儲任何東西的)
return JwtUtil.generate(user.getUsername());
}
return "賬號密碼錯誤";
}
}
復制代碼
在后續(xù)會話中漱牵,用戶訪問其他接口時就可以校驗token
來判斷其是否已經(jīng)登錄。前端將token
一般會放在請求頭的Authorization
項傳遞過來疚漆,其格式一般為類型 + token
酣胀。這個倒也不是一定得這么做,你放在自己自定義的請求頭項也可以愿卸,只要和前端約定好就行。這里我們方便演示就將token直接放在Authorization
項里了:
@GetMapping("api")
public String api(HttpServletRequest request) {
// 從請求頭中獲取token字符串
String jwt = request.getHeader("Authorization");
// 解析失敗就提示用戶登錄
if (JwtUtil.parse(jwt) == null) {
return "請先登錄";
}
// 解析成功就執(zhí)行業(yè)務邏輯返回數(shù)據(jù)
return "api成功返回數(shù)據(jù)";
}
@GetMapping("api2")
public String api2(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
if (JwtUtil.parse(jwt) == null) {
return "請先登錄";
}
return "api2成功返回數(shù)據(jù)";
}
復制代碼
接下來我們測試一下效果截型,先進行登錄:
[圖片上傳中...(image-50e3c4-1599030254561-5)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
可以看到登錄成功后服務器返回了token
過來趴荸,然后我們將這個token設置到請求頭中再調(diào)用其他接口看看效果:
[圖片上傳中...(image-884e0d-1599030254561-4)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
可以看到成功返回數(shù)據(jù)了!我們再試一下不攜帶token
和篡改token
后調(diào)用其他接口會怎樣:
[圖片上傳中...(image-ced573-1599030254561-3)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
[圖片上傳中...(image-26a050-1599030254561-2)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
可以看到宦焦,沒有攜帶token
或者私自篡改了token
都會驗證失敺⒍邸!
攔截器
和之前一樣波闹,如果每個接口都要手動判斷一下用戶有沒有登錄太麻煩了酝豪,所以我們做一個統(tǒng)一處理胖缤,這里我們換個花樣用攔截器來做:
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 簡單的白名單枝恋,登錄這個接口直接放行
if ("/login".equals(request.getRequestURI())) {
return true;
}
// 從請求頭中獲取token字符串并解析
Claims claims = JwtUtil.parse(request.getHeader("Authorization"));
// 已登錄就直接放行
if (claims != null) {
return true;
}
// 走到這里就代表是其他接口竹握,且沒有登錄
// 設置響應數(shù)據(jù)類型為json(前后端分離)
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// 設置響應內(nèi)容娶靡,結(jié)束請求
out.write("請先登錄");
out.flush();
out.close();
return false;
}
}
復制代碼
攔截器類寫好之后缸剪,別忘了要使其生效母赵,這里我們直接讓SpringBoot
啟動類實現(xiàn)WevMvcConfigurer
接口來做:
@SpringBootApplication
public class LoginJwtApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(LoginJwtApplication.class, args);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 使攔截器生效
registry.addInterceptor(new LoginInterceptor());
}
}
復制代碼
這樣就能省去接口中校驗用戶登錄的邏輯了:
@GetMapping("api")
public String api() {
return "api成功返回數(shù)據(jù)";
}
@GetMapping("api2")
public String api2() {
return "api2成功返回數(shù)據(jù)";
}
復制代碼
[圖片上傳中...(image-cf5c-1599030254561-1)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
可以看到特恬,攔截器已經(jīng)生效了舆逃!
上下文對象
統(tǒng)一攔截做好之后接下來就是我們的上下文對象庄撮,JWT
不像Session
把用戶信息直接存儲起來背捌,所以JWT
的上下文對象要靠我們自己來實現(xiàn)。
首先我們定義一個上下文類洞斯,這個類專門存儲JWT
解析出來的用戶信息毡庆。我們要用到ThreadLocal
,以防止線程沖突:
public final class UserContext {
private static final ThreadLocal<String> user = new ThreadLocal<String>();
public static void add(String userName) {
user.set(userName);
}
public static void remove() {
user.remove();
}
/**
* @return 當前登錄用戶的用戶名
*/
public static String getCurrentUserName() {
return user.get();
}
}
復制代碼
這個類創(chuàng)建好之后我們還需要在攔截器里做下處理:
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
...省略之前寫的代碼
// 從請求頭中獲取token字符串并解析
Claims claims = JwtUtil.parse(request.getHeader("Authorization"));
// 已登錄就直接放行
if (claims != null) {
// 將我們之前放到token中的userName給存到上下文對象中
UserContext.add(claims.getSubject());
return true;
}
...省略之前寫的代碼
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 請求結(jié)束后要從上下文對象刪除數(shù)據(jù),如果不刪除則可能會導致內(nèi)存泄露
UserContext.remove();
super.afterCompletion(request, response, handler, ex);
}
}
復制代碼
這樣一個上下文對象就做好了么抗,用法和之前一樣毅否,可以在程序的其他地方直接獲取到數(shù)據(jù),我們在Service層中來使用它:
public void doSomething() {
String currentUserName = UserContext.getCurrentUserName();
System.out.println("Service層---當前用戶登錄名:" + currentUserName);
}
復制代碼
然后Controller層調(diào)用Service層:
@GetMapping("api")
public String api() {
userService.doSomething();
return "api成功返回數(shù)據(jù)";
}
復制代碼
這樣一套做好之后乖坠,看下前端成功調(diào)用api
接口時的效果:
[圖片上傳中...(image-107cb1-1599030254561-0)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>
控制臺成功打印了搀突!
補充
代碼到此就完成了!就像開頭所說熊泵,本文只是講解了基本的登錄認證功能實現(xiàn)仰迁,還有很多很多細節(jié)沒有提及,比如密碼加密顽分、防XSS/CSRF攻擊等徐许。接下來我要補充的也不是這些細節(jié),而是補充一些其他的基礎知識卒蘸,幫助大家更好的理解本文講解的內(nèi)容雌隅!
注意事項
本文為了方便演示省略了很多非登錄認證核心的相關代碼,比如在統(tǒng)一處理中如果發(fā)現(xiàn)用戶沒有登錄應該是直接拋出自定義異常缸沃,然后由異常全局處理返回給前端統(tǒng)一的數(shù)據(jù)響應體恰起,而不是像我們現(xiàn)在代碼中一樣直接用PrintWriter
輸出流輸出數(shù)據(jù)。關于這方面可以參考我之前寫的博客進行改造:【項目實踐】SpringBoot三招組合拳趾牧,手把手教你打出優(yōu)雅的后端接口
再有就是JWT
的相關注意點检盼。通過代碼看到生成一個JWT
字符串很簡單,誰都可以生成翘单。然后字符串這東西也誰都可以篡改吨枉,我們怎么保證這個字符串就是我們系統(tǒng)簽發(fā)出去的呢?又怎么保證我們簽發(fā)出去的字符串有沒有被篡改呢哄芜? 其中關鍵點就是工具類里寫的secretKey
屬性了貌亭,JWT
根據(jù)這個秘鑰會生成一個獨特的字符串,別人沒有這個秘鑰的話是無法偽造或篡改JWT
的认臊!所以這個秘鑰是重中之重圃庭,在實際開發(fā)中一定要謹防泄露:開發(fā)環(huán)境下設置一個秘鑰,生產(chǎn)環(huán)境設置一個秘鑰失晴,這個生產(chǎn)環(huán)境下的秘鑰還要嚴防死守冤议,可以通過配置中心來配置并且要防止開發(fā)人員在代碼中打印出秘鑰!
我們代碼中演示的JWT
是只存放了用戶名师坎,實際開發(fā)中你想存什么就存什么恕酸,不過一定不要存敏感信息(比如密碼)!因為JWT
只能防止被篡改胯陋,不能防止別人解密你這個字符串蕊温!
Session和JWT的優(yōu)劣
兩種方式都可以實現(xiàn)登錄認證袱箱,那么在實際開發(fā)中到底用哪一種估計是大家比較關心的問題!在這里我就簡單說明一下兩者各自的優(yōu)劣义矛,至于具體選型就根據(jù)自己實際業(yè)務需求來了发笔。
首先說一下兩者的優(yōu)點吧:
Session:
- 開箱即用,簡單方便
- 能夠有效管理用戶登錄的狀態(tài):續(xù)期凉翻、銷毀等(續(xù)期就是延長用戶登錄狀態(tài)的時間了讨,銷毀就是清楚用戶登錄狀態(tài),比如退出登錄)
JWT:
- 可直接解析出數(shù)據(jù)制轰,服務端無需存儲數(shù)據(jù)
- 天然地易于水平擴展(ABC三個系統(tǒng)前计,我同一個Token都可以登錄認證,非常簡單就完成了單點登錄)
再說一下缺點:
Session:
- 較
JWT
而言垃杖,需要額外存儲數(shù)據(jù)
JWT:
JWT
簽名的長度遠比一個Session Id
長很多男杈,增加額外網(wǎng)絡開銷無法銷毀、續(xù)期登錄狀態(tài)
秘鑰或
Token
一旦泄露调俘,攻擊者便可以肆無忌憚操作我們的系統(tǒng)
其實上面說的這些優(yōu)缺點都可以通過一些手段來解決伶棒,就看自己取舍了!比如Session
就不易于水平擴展嗎彩库?當然不是肤无,無論是Session
同步機制還是集中管理都可以非常好的解決。再比如JWT
就真的無法銷毀嗎骇钦?當然也不是宛渐,其實可以將Token
也在后端存儲起來讓其變成有狀態(tài)的,就可以做到狀態(tài)管理了司忱!
軟件開發(fā)沒有銀彈皇忿,技術選型根據(jù)自己業(yè)務需求來就好畴蹭,千萬不要單一崇拜某一技術而排斥其他同類技術坦仍!