敲了這麼多年程式碼,這樣的登入方式還真是頭一回見

千鋒Python唐小強發表於2020-07-07

有的時候我不禁想,如果從 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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章