敲了這麼多年程式碼,這樣的登入方式還真是頭一回見
有的時候我不禁想,如果從 Spring Security 誕生的第一天開始,我們就一直在追蹤它,那麼今天再去看它的原始碼一定很簡單,因為我們瞭解到每一行程式碼的緣由。
然而事實上我們大部分人都是中途接觸到它的,包括我自己。所以在閱讀原始碼的時候,有時候會遇到一些不是那麼容易理解的東西,並不是說這個有多難,只是我們不瞭解 N 年前的開發環境,因此也就不容易理解某一行程式碼出現的意義。
所以為了搞透徹這個框架,有時候我們還得去了解之前發生了什麼。
這就跟學 Spring Boot 一樣,很多小夥伴問要不要跳過 SSM ,我說不要,甚至還專門寫了一篇文章Spring Boot 要怎麼學?要學哪些東西?要不要先學 SSM?,跳過了 SSM ,Spring Boot 中的很多東西就無法真正理解。
扯遠了。。。
Spring Security 中對 HttpServletRequest 請求進行了封裝,重寫了 HttpServletRequest 中的幾個和安全管理相關的方法,想要理解 Spring Security 中的重寫,就要先從 HttpServletRequest 開始看起。
有小夥伴可能會說,HttpServletRequest 能跟安全管理扯上什麼關係?今天就來和大家捋一捋,我們不講 Spring Security,就來單純講講 HttpServletRequest 中的安全管理方法。
1.HttpServletRequest
在 HttpServletRequest 中,我們常用的方法如:
- public String getHeader(String name);
- public String getParameter(String name);
- public ServletInputStream getInputStream()
- …
這些常見的方法可能大家都有用過,還有一些不常見的,和安全相關的方法:
public String
getRemoteUser
();
public
boolean
isUserInRole
(String role);
public java.security.
Principal
getUserPrincipal
();
public
boolean
authenticate
(HttpServletResponse response)
throws IOException, ServletException;
public
void
login
(String username, String password)
throws ServletException;
public
void
logout
()
throws ServletException;
前面三個方法,在之前的 Servlet 中就有,後面三個方法,則是從 Servlet3.0 開始新增加的方法。從方法名上就可以看出,這些都是和認證相關的方法,但是這些方法,我估計很多小夥伴都沒用過,因為不太實用。
在 Spring Security 框架中,對這些方法進行了重寫,進而帶來了一些好玩並且方便的特性,這個在後面的文章中再和大家分享。
要理解 Spring Security 中的封裝,就得先來看看,不用框架,這些方法該怎麼用!
2.實踐出真知
我們建立一個普普通通的 Web 專案,不使用任何框架(後面的案例都基於此),然後在 doGet 方法中列印出 HttpServletRequest 的型別,程式碼如下:
@Override
protected
void
doGet
(HttpServletRequest request, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println(
"request.getClass() = " + request.getClass());
}
程式碼執行列印結果如下:
request.getClass() =
class
org.
apache.
catalina.
connector.
RequestFacade
HttpServletRequest 是一個介面,而 RequestFacade 則是一個正兒八經的 class。
HttpServletRequest 是 Servlet 規範中定義的 ServletRequest,這相當於是標準的 Request;但是在 Tomcat 中的 Request 則是 Tomcat 自己自定義的 Request,自定義的 Request 實現了 HttpServletRequest 介面並且還定義了很多自己的方法,這些方法還是 public 的,如果直接使用 Tomcat 自定義的 Request,開發者只需要向下轉型就能呼叫這些 Tomcat 內部方法,這是有問題的,所以又用 RequestFacade 封裝了一下,以至於我們實際上用到的就是 RequestFacade 物件。
那麼毫無疑問,HttpServletRequest#login 方法具體實現就是在 Tomcat 的 Request#login 方法中完成的。經過原始碼追蹤,我們發現,登入的資料來源是由 Tomcat 中的 Realm 提供的, 注意這個 Realm 不是 Shiro 中的 Realm。
Tomcat 中提供了 6 種 Realm,可以支援與各種資料來源的對接:
- JDBCRealm:很明顯,這個 Realm 可以對接到資料庫中的使用者資訊。
- DataSourceRealm:它透過一個 JNDI 命名的 JDBC 資料來源在關係型資料庫中查詢使用者。
- JNDIRealm:透過一個 JNDI 提供者1在 LDAP 目錄伺服器中查詢使用者。
- UserDatabaseRealm:這個資料來源在 Tomcat 的配置檔案中 conf/tomcat-users.xml。
- MemoryRealm:這個資料來源是在記憶體中,記憶體中的資料也是從 conf/tomcat-users.xml 配置檔案中載入的。
- JAASRealm:JAAS 架構來實現對使用者身份的驗證。
如果這些 Realm 無法滿足需求,當然我們也可以自定義 Realm,只不過一般我們不這樣做,為啥?因為這這種登入方式用的太少了!今天這篇文章純粹是和小夥伴們開開眼界。
如果自定義 Realm 的話,我們只需要實現 org.apache.catalina.Realm 介面,然後將編譯好的 jar 放到 $CATALINA_HOME/lib 下即可,具體的配置則和下面介紹的一致。
接下來我和大家介紹兩種配置方式,一個是 UserDatabaseRealm,另一個是 JDBCRealm。
2.1 基於配置檔案登入
我們先來定義一個 LoginServlet:
@WebServlet(urlPatterns =
"/login")
public
class
LoginServlet
extends
HttpServlet {
@Override
protected
void
doGet
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected
void
doPost
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter(
"username");
String password = req.getParameter(
"password");
try {
req.login(username, password);
}
catch (ServletException e) {
req.getRequestDispatcher(
"/login.jsp").forward(req, resp);
return;
}
boolean login = req.getUserPrincipal() !=
null && req.isUserInRole(
"admin");
if (login) {
resp.sendRedirect(
"/hello");
return;
}
else {
req.getRequestDispatcher(
"/login.jsp").forward(req, resp);
}
}
}
當請求到達後,先提取出使用者名稱和密碼,然後呼叫 req.login 方法進行登入,如果登入失敗,則跳轉到登入頁面。
登入完成後,透過獲取登入使用者資訊以及判斷登入使用者角色,來確保使用者是否登入成功。
如果登入成功,就跳轉到專案應用首頁,否則就跳轉到登入頁面。
接下來定義 HelloServlet:
@WebServlet(urlPatterns =
"/hello")
public
class
HelloServlet
extends
HttpServlet {
@Override
protected
void
doGet
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected
void
doPost
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Principal userPrincipal = req.getUserPrincipal();
if (userPrincipal ==
null) {
resp.setStatus(
401);
resp.getWriter().write(
"please login");
}
else
if (!req.isUserInRole(
"admin")) {
resp.setStatus(
403);
resp.getWriter().write(
"forbidden");
}
else{
resp.getWriter().write(
"hello");
}
}
}
在 HelloServlet 中,先判斷使用者是否已經登入,沒登入的話,就返回 401,已經登入但是不具備相應的角色,就返回 403,否則就返回 hello。
接下來再定義 LogoutServlet,執行登出操作:
@WebServlet(urlPatterns =
"/logout")
public
class
LogoutServlet
extends
HttpServlet {
@Override
protected
void
doGet
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected
void
doPost
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.logout();
resp.sendRedirect(
"/hello");
}
}
logout 方法也是 HttpServletRequest 自帶的。
最後再簡單定義一個 login.jsp 頁面,如下:
<
%@
page
contentType=
"text/html;charset=UTF-8"
language=
"java" %>
<
html>
<
head>
<
title>Title
</
title>
</
head>
<
body>
<
form
action=
"/login"
method=
"post">
<
input
type=
"text"
name=
"username">
<
input
type=
"text"
name=
"password">
<
input
type=
"submit"
value=
"登入">
</
form>
</
body>
</
html>
所有工作都準備好了,接下來就是資料來源了,預設情況下載入的是 conf/tomcat-users.xml 中的資料,找到 Tomcat 的這個配置檔案,修改之後內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<
tomcat-users>
<
role
rolename=
"admin"/>
<
user
username=
"javaboy"
password=
"123"
roles=
"admin"/>
</
tomcat-users>
配置完成後,啟動專案進行測試。登入使用者名稱是 javaboy,登入密碼是 123,具體的測試過程我就不再演示了。
2.2 基於資料庫登入
如果想基於資料庫登入,我們需要先準備好資料庫和表,需要兩張表,user 表和 role 表,如下:
CREATE
TABLE
`user` (
`id`
int(
11)
unsigned
NOT
NULL AUTO_INCREMENT,
`username`
varchar(
255)
COLLATE utf8mb4_unicode_ci
DEFAULT
NULL,
`password`
varchar(
255)
COLLATE utf8mb4_unicode_ci
DEFAULT
NULL,
PRIMARY
KEY (
`id`)
)
ENGINE=
InnoDB AUTO_INCREMENT=
2
DEFAULT
CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
CREATE
TABLE
`role` (
`id`
int(
11)
unsigned
NOT
NULL AUTO_INCREMENT,
`username`
varchar(
255)
COLLATE utf8mb4_unicode_ci
DEFAULT
NULL,
`role_name`
varchar(
255)
COLLATE utf8mb4_unicode_ci
DEFAULT
NULL,
PRIMARY
KEY (
`id`)
)
ENGINE=
InnoDB AUTO_INCREMENT=
2
DEFAULT
CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
然後向表中新增兩行模擬資料:
接下來,找到 Tomcat 的 conf/server.xml 檔案,修改配置,如下:
<
Realm
className=
"org.apache.catalina.realm.LockOutRealm">
<
Realm
className=
"org.apache.catalina.realm.JDBCRealm"
debug=
"99"
driverName=
"com.mysql.jdbc.Driver"
connectionURL=
"jdbc:mysql://localhost:3306/basiclogin"
connectionName=
"root"
connectionPassword=
"123"
userTable=
"user"
userNameCol=
"username"
userCredCol=
"password"
userRoleTable=
"role"
roleNameCol=
"role_name" />
</
Realm>
在這段配置中:
- 指定 JDBCRealm。
- 指定資料庫驅動。
- 指定資料庫連線地址。
- 指定資料庫連線使用者名稱/密碼。
- 指定使用者表名稱;使用者名稱的欄位名以及密碼欄位名。
- 指定角色表名稱;以及角色欄位名。
配置完成後,再次登入測試,此時的登入資料就是來自資料庫的資料了。
3.最佳化
前面的 HelloServlet,我們是在程式碼中手動配置的,要是每個 Servlet 都這樣配置,這要搞到猴年馬月了~
所以我們對此可以在 web.xml 中進行手動配置。
首先我們建立一個 AdminServlet 進行測試,如下:
@WebServlet(urlPatterns =
"/admin/hello")
public
class
AdminServlet
extends
HttpServlet {
@Override
protected
void
doGet
(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write(
"hello admin!");
}
}
然後在 web.xml 中進行配置:
<
security-constraint>
<
web-resource-collection>
<
web-resource-name>admin
</
web-resource-name>
<
url-pattern>/admin/*
</
url-pattern>
</
web-resource-collection>
<
auth-constraint>
<
role-name>admin
</
role-name>
</
auth-constraint>
</
security-constraint>
<
security-role>
<
role-name>admin
</
role-name>
</
security-role>
這個配置表示 /admin/* 格式的請求路徑,都需要具有 admin 角色才能訪問,否則就訪問不到,這樣,每一個 Admin 相關的 Servlet 就被保護起來了,不用在 Servlet 中寫程式碼判斷了。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2702349/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 掃碼登入是這樣登入的
- 這樣寫程式碼,真是帥到沒有朋友
- 我真是服了!AS Debug還能這麼玩?
- 寫了這麼多年 JavaScript ,竟然還不知道這些技巧?JavaScript
- 做了這麼多年前端,為什麼你還是不會寫業務程式碼?前端
- 使用Python這麼多年,竟然還有這些實用的功能和特點!Python
- 做了這麼多年優化師,才發現遊戲廣告素材指令碼是這樣寫的……優化遊戲指令碼
- 唱衰這麼多年,PHP 仍然還是你大爺!PHP
- 這幾年火遍全世界的Python勢頭還這麼強?Python
- 大牛的程式碼是這樣寫的
- 這。。這。。C++標頭檔案居然可以這麼打!!!! 長見識了!!!C++
- 居然還有這樣的榜單! 吃貨慎入!
- 漫畫通訊:驚呆了,手機登入還可以這麼玩!
- 做深度學習這麼多年還不會挑GPU?這兒有份選購全攻略深度學習GPU
- 用Java這麼多年,這些祕密你知道嗎?Java
- postMessage 還能這樣玩
- 進去新專案,接手這樣的程式碼怎麼辦
- [iOS] 接手舊專案,看到這樣的程式碼不要哭 ... 因為你已經在這裡見過iOS
- 這樣的微信商城原始碼,你肯定沒見過。原始碼
- 操控網路還能操控電壓,你見過這樣的黑客嗎?黑客
- 都用vue3了,還這樣用emit麼?VueMIT
- Spring中資源的載入原來是這麼一回事啊!Spring
- 用了這麼多年MySql,這些好習慣你用過哪些MySql
- uniapp 完成兩種方式登入 驗證碼登入 密碼登入APP密碼
- 密碼方式登入redis密碼Redis
- 這麼多年了,有沒有一樣東西始終陪在你身邊
- 微信和Python之間,還能這樣玩的嘛,漲見識了!Python
- 白上這麼多年班,才知道資料視覺化這麼簡單視覺化
- JS阻塞渲染,這麼多年我理解錯啦?JS
- 這麼多年來,你值得驕傲堅持的是什麼?
- 我沒有見過這樣的黃昏
- 原來 Element 的元件原始碼還能這麼看元件原始碼
- 這樣可以寫出無法維護的程式碼
- 再見了 Redux、Recoil、MobX、Zustand、Jotai 還有 Valtio,狀態管理還可以這樣做?ReduxAI
- 這樣規範寫程式碼,同事直呼“666”
- 驚! 大屏還能長這樣!
- 花式玩 Spring Security ,這樣的使用者定義方式你可能沒見過!Spring
- PLM與ERP整合,這個頭疼的問題,可以這樣解決!