版权提醒
本系列文章为原创内容。转载请注明文章来源。

前言

本文为搭建 SSSP 框架极简书店教程系列的其中一篇。本文主要内容:实现管理员登录功能。

因为作者写教程的初衷是为了写作业,而作业给出的数据库设计中没有管理员用户表,所以这里的登录功能只有一个管理员用户,使用字符串相同的方式验证。也正因如此,这个功能很适合在还没有讲 JPA 的时候写。

创建管理员登录相关 JSP 文件

在 web 目录下建立 admin 目录,在 admin 目录下创建 admin_login.jsp 和 admin_welcome.jsp 。

admin_login.jsp

这个文件是管理员登录页面。使用 required 属性使用户必须输入内容才能提交。

<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <title>管理员登录</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
    <script src="https://cdn.staticfile.org/popper.js/1.15.0/umd/popper.min.js"></script>
    <script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
    <style type="text/css">
        /*垂直居中,div上边界距离窗口上边的距离为窗口高度的50%,
        实际上此时div内容整体已经偏下,再把整个身子往上移动一半即可
        并针对不同浏览器进行兼容。
        */
        .col-center-block {
            position: absolute;
            top: 50%;
            -webkit-transform: translateY(-50%);
            -moz-transform: translateY(-50%);
            -ms-transform: translateY(-50%);
            -o-transform: translateY(-50%);
            transform: translateY(-50%);
        }
    </style>
</head>
<body>

    <!-- 在外层添加一个div,把行内容居中,添加.row .justify-content-center -->
    <div class="row justify-content-center ">
        <div class="col-sm-3 card border-dark card text-center col-center-block shadow-lg" style="
                padding-right: 0px;
                padding-left: 0px;
                min-width: 20rem;">
            <div class="card-header">管理员登录</div>
            <div class="card-body text-dark">
                <form action="toLogin.action" method="post">
                    <div class="form-group">
                        <s:fielderror fieldName="loginFall"/>
                    </div>
                    <div class="form-group">
                        <label for="userName" class="sr-only">Email address</label>
                        <input type="text" class="form-control" id="userName" name="userName" placeholder="管理员账号" required autofocus>
                    </div>
                    <div class="form-group">
                        <label for="password" class="sr-only">Password</label>
                        <input type="password" class="form-control" id="password" name="password" placeholder="密码" required>
                    </div>
                    <button type="submit" class="btn btn-primary btn-block">登录</button>
                </form>
            </div>
        </div>
    </div>

</body>
</html>

admin_welcome.jsp

这个文件是管理员登录成功后的欢迎页面。因为后台所有页面的风格都是一致的,所以为了便于维护和使用,将相同的部分进行了提取,使用时通过 <%@include file=""%> 标签导入。

此文件同时演示了 jsp 读取 Session 的方法之一。使用 ${sessionScope.get("adminName")} 读取 Session 中保存的管理员账户名。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
    <title>后台首页</title>
    <!-- 引入CSS样式和JS脚本文件等-->
    <%@include file="common/admin_headContext.jsp"%>
</head>
<body>
    <!-- 引入顶部导航条-->
    <%@include file="common/admin_common_top.jsp"%>
    <div class="container-fluid">
        <div class="row">
            <!-- 引入左侧导航栏-->
            <%@include file="common/admin_common_left.jsp"%>
            <!-- 页面主体 -->
            <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">

                <h1>欢迎进入后台管理,${sessionScope.get("adminName")}!</h1>

            </main>
        </div>
    </div>
    <!-- 设置左侧导航栏为正确选中项-->
    <script type="text/javascript">
        $(document).ready(function(){
            $("#welcome").addClass("active");
        });
    </script>
</body>
</html>

共用文件

在 admin 目录下建立 common 目录,建立以下文件:

admin_headContext.jsp

此文件存放除标题外所有 <head> 标签中的内容。

${pageContext.request.contextPath} 用于获取网站根目录,对应 web 目录。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="https://cdn.staticfile.org/jquery/3.2.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/popper.js/1.15.0/umd/popper.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
<!-- Custom styles for this template -->
<link href="${pageContext.request.contextPath}/css/dashboard.css" rel="stylesheet">
<style>
    .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
    }

    @media (min-width: 768px) {
        .bd-placeholder-img-lg {
            font-size: 3.5rem;
        }
    }
</style>

admin_common_top.jsp

此文件是后台页面顶部导航栏。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 顶部 -->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
    <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="${pageContext.request.contextPath}/admin/index.action">Bookstore后台管理</a>

    <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>

    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">

    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="${pageContext.request.contextPath}/admin/index!logout.action">退出登录</a>
        </li>
    </ul>
</nav>

admin_common_left.jsp

此文件对应后台页面左侧导航栏。

每个导航项都有一个 id 属性,用于被对应的主页面的 js 代码识别,然后添加高亮效果。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 左侧 -->
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
    <div class="sidebar-sticky pt-3">
        <!-- 左侧导航列表 -->
        <ul class="nav flex-column">
            <li class="nav-item">
                <a class="nav-link" id="welcome" href="${pageContext.request.contextPath}/admin/index.action">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-house-door" viewBox="0 0 16 16">
                        <path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"/>
                    </svg>
                    欢迎界面
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" id="order" href="${pageContext.request.contextPath}/admin/index!toOrderManage.action">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-columns" viewBox="0 0 16 16">
                        <path fill-rule="evenodd" d="M0 .5A.5.5 0 0 1 .5 0h9a.5.5 0 0 1 0 1h-9A.5.5 0 0 1 0 .5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2A.5.5 0 0 1 .5 2h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2A.5.5 0 0 1 .5 4h10a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2A.5.5 0 0 1 .5 6h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2A.5.5 0 0 1 .5 8h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm-13 2a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5Zm13 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Z"/>
                    </svg>
                    订单管理
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" id="customer" href="${pageContext.request.contextPath}/admin/index!toCustomerManage.action">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-people-fill" viewBox="0 0 16 16">
                        <path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
                        <path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
                        <path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
                    </svg>
                    客户管理
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" id="book" href="${pageContext.request.contextPath}/admin/index!toBookManage.action">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stack" viewBox="0 0 16 16">
                        <path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
                        <path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
                    </svg>
                    书籍管理
                </a>
            </li>
        </ul>
    </div>
</nav>

dashboard.css

为了便于管理, css 文件应放在 web/css 目录下。

body {
    font-size: .875rem;
  }
  
  .feather {
    width: 16px;
    height: 16px;
    vertical-align: text-bottom;
  }
  
  /*
   * Sidebar
   */
  
  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 100; /* Behind the navbar */
    padding: 48px 0 0; /* Height of navbar */
    box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
  }
  
  @media (max-width: 767.98px) {
    .sidebar {
      top: 5rem;
    }
  }
  
  .sidebar-sticky {
    position: relative;
    top: 0;
    height: calc(100vh - 48px);
    padding-top: .5rem;
    overflow-x: hidden;
    overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
  }
  
  @supports ((position: -webkit-sticky) or (position: sticky)) {
    .sidebar-sticky {
      position: -webkit-sticky;
      position: sticky;
    }
  }
  
  .sidebar .nav-link {
    font-weight: 500;
    color: #333;
  }
  
  .sidebar .nav-link .feather {
    margin-right: 4px;
    color: #999;
  }
  
  .sidebar .nav-link.active {
    color: #007bff;
  }
  
  .sidebar .nav-link:hover .feather,
  .sidebar .nav-link.active .feather {
    color: inherit;
  }
  
  .sidebar-heading {
    font-size: .75rem;
    text-transform: uppercase;
  }
  
  /*
   * Navbar
   */
  
  .navbar-brand {
    padding-top: .75rem;
    padding-bottom: .75rem;
    font-size: 1rem;
    background-color: rgba(0, 0, 0, .25);
    box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
  }
  
  .navbar .navbar-toggler {
    top: .25rem;
    right: 1rem;
  }
  
  .navbar .form-control {
    padding: .75rem 1rem;
    border-width: 0;
    border-radius: 0;
  }
  
  .form-control-dark {
    color: #fff;
    background-color: rgba(255, 255, 255, .1);
    border-color: rgba(255, 255, 255, .1);
  }
  
  .form-control-dark:focus {
    border-color: transparent;
    box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
  }

创建用于管理员登录的 Action

在 Action 包中创建 AdminLogin.java 。

package com.chiyu.action;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;

import java.util.Map;

//使用注释标志此类是一个控制器Bean,类型为prototype,原型,与单例模式相反。Bean的id默认为类首字母小写。
@Controller
@Scope("prototype")
public class AdminLogin extends ActionSupport {
    //使用属性封装获取用户登录时输入的用户名和密码。
    private String userName;
    private String password;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    //验证用户输入的用户名与密码是否正确,不正确将返回input并返回信息:用户名或密码错误!
    @Override
    public void validate() {
        if (!(this.userName.equals("admin") && this.password.equals("123456"))){
            addFieldError("loginFall","用户名或密码错误!");
        }
    }

    //因为有前置的数据校验,到达此方法时,用户名与密码是正确的,此方法将保存“此用户是管理员”和“管理员账户名”到Session
    @Override
    public String execute() throws Exception {
        Map<String, Object> session = ActionContext.getContext().getSession();
        session.put("isAdmin",true);
        session.put("adminName",this.userName);
        return SUCCESS;
    }
}

创建用于后台中控的 Action

在 Action 包中创建 BackgroundDispatcher.java 。此 Action 主要功能是对应导航栏跳转到对应页面以及管理员退出登录。

package com.chiyu.action;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;

import java.util.Map;

@Controller
@Scope("prototype")
public class BackgroundDispatcher extends ActionSupport {
    //先确认当前用户的管理员身份是否有效。
    @Override
    public void validate() {
        Map<String, Object> session = ActionContext.getContext().getSession();
        if (session.get("isAdmin") == null){
            addFieldError("loginFall","请先登录!");
        }
    }

    //默认方法,到达后台首页。
    @Override
    public String execute() throws Exception {
        return SUCCESS;
    }

    //退出登录方法。清空Session。
    public String logout(){
        Map<String, Object> session = ActionContext.getContext().getSession();
        session.clear();
        return LOGIN;
    }
}

配置后台 Struts 配置文件

修改 background.xml 文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
        "http://struts.apache.org/dtds/struts-2.5.dtd">

<struts>
    <package name="background" namespace="/admin" extends="struts-default" strict-method-invocation="false">
        <action name="index" class="backgroundDispatcher">
            <result name="input">/admin/admin_login.jsp</result>
            <result name="success">/admin/admin_welcome.jsp</result>
            <result name="login">/admin/admin_login.jsp</result>
        </action>
        <action name="toLogin" class="adminLogin">
            <result name="success" type="redirectAction">index.action</result>
            <result name="input">/admin/admin_login.jsp</result>
        </action>
    </package>
</struts>

稍微解释一下此配置文件
package 中的 name 属性用于唯一标识 package,namespace="/admin" 表示访问此包中的 Action 应用类似这样的地址: http://localhost:8000/admin/index.action , strict-method-invocation="false" ,不使用严格的方法调用,为了方便的使用动态方法调用。
action 中的 class 属性值是 Spring Bean ,由 Spring 扫描对应注释得到。

完成

到此,你已经完成管理员登录后进入后台的功能以及退出登录的功能。现在访问:http://localhost:8000/admin/index.action 试试吧!