寫了這麼多年代碼,這樣的登陸方式仍是頭一回見!

Spring Security 系列還沒搞完,最近還在研究。html

有的時候我不由想,若是從 Spring Security 誕生的第一天開始,咱們就一直在追蹤它,那麼今天再去看它的源碼必定很簡單,由於咱們瞭解到每一行代碼的原因。java

然而事實上咱們大部分人都是中途接觸到它的,包括鬆哥本身。因此在閱讀源碼的時候,有時候會遇到一些不是那麼容易理解的東西,並非說這個有多難,只是咱們不瞭解 N 年前的開發環境,所以也就不容易理解某一行代碼出現的意義。mysql

因此爲了搞透徹這個框架,有時候咱們還得去了解以前發生了什麼。git

這就跟學 Spring Boot 同樣,不少小夥伴問要不要跳過 SSM ,我說不要,甚至還專門寫了一篇文章(Spring Boot 要怎麼學?要學哪些東西?要不要先學 SSM?),跳過了 SSM ,Spring Boot 中的不少東西就沒法真正理解。github

扯遠了。。。web

Spring Security 中對 HttpServletRequest 請求進行了封裝,重寫了 HttpServletRequest 中的幾個和安全管理相關的方法,想要理解 Spring Security 中的重寫,就要先從 HttpServletRequest 開始看起。sql

有小夥伴可能會說,HttpServletRequest 能跟安全管理扯上什麼關係?今天鬆哥就來和你們捋一捋,咱們不講 Spring Security,就來單純講講 HttpServletRequest 中的安全管理方法。數據庫

1.HttpServletRequest

在 HttpServletRequest 中,咱們經常使用的方法如:apache

  • public String getHeader(String name);
  • public String getParameter(String name);
  • public ServletInputStream getInputStream()
  • ...

這些常見的方法可能你們都有用過,還有一些不常見的,和安全相關的方法:tomcat

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 中寫代碼判斷了。

4.小結

好啦,通過本文的介紹,相信小夥伴們對於 HttpServletRequest 中關於認證的幾個方法基本上都瞭解了,接下來的文章鬆哥將繼續和你們介紹這些方法在 Spring Security 框架中是如何進行演化的,看懂了本文,後面的文章就很好理解了~

本文案例下載地址:https://github.com/lenve/javaboy-code-samples

好啦,小夥伴們若是以爲有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索