1、添加登录功能
1.1、配置
现在需要使用会话功能,所以把所有的jsp的页面特性session=”false“变成true或者去掉默认为true。然后再部署描述符里添加
30
true
COOKIE
1.2、创建用户登录的servlet和用户数据库
@WebServlet(
name = "loginServlet",
urlPatterns = "/login"
)
public class LoginServlet extends HttpServlet {
//1、创建用户数据库
private final static Map userDatabase = new HashMap<>();
//2、添加用户
static {
userDatabase.put("ljs", "ljs");
userDatabase.put("csy", "csy");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//3、使用会话
HttpSession session = req.getSession();
//3.1、检查用户是否已经登录
if (session.getAttribute("username") != null) {
//3.2、如果登录就重定向到票据页面
resp.sendRedirect("tickets");
return;
}
//3.3、未登陆就将请求特性设置为false,然后将请求转发到登录jsp
//当jsp中的登录表单被提交时,请求将发生到dopost方法
//该请求特性是为了在登录失败后页面输出提醒
req.setAttribute("loginFailed", false);
req.getRequestDispatcher("/WEB-INF/jsp/view/login.jsp")
.forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//4、当登录表单时候使用会话
HttpSession session = req.getSession();
//4.1、检查用户是否已经登录
if (session.getAttribute("username") != null) {
//4.2、如果登录就重定向到票据页面
resp.sendRedirect("tickets");
return;
}
//4.2、检查提交的表单数据是否与数据一致
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || password == null ||
!LoginServlet.userDatabase.containsKey(username) ||
!password.equals(LoginServlet.userDatabase.get(username))) {
//4.3、登录失败请求特性设置为true
//并且将请求转发到登录页面
req.setAttribute("loginFailed", true);
req.getRequestDispatcher("/WEB-INF/jsp/view/login.jsp")
.forward(req, resp);
}else{
//4.4、登录成功,设置会话Id,重定向在票据页面
session.setAttribute("username", username);
//servlet3.1新添加的特性,迁移会话的方式防止会话固定攻击。
req.changeSessionId();
resp.sendRedirect("tickets");
}
}
}
1.3、创建登录表单
login.jsp.首先是使用loginFailed特性,在用户登录失败的时候提醒用户。
Customer Support
Login
You must log in to access the customer support site.
<%
if(((Boolean)request.getAttribute("loginFailed")))
{
%>
The username or password you entered are not correct. Please try
again.
<%
}
%>
">
Username
<
Password<
1.4、设置TicketServlet
首先往index.jsp中添加,这样访问项目的时候就会重定向到tickets,这是之前忘记设置的。这样就不用再启动服务器之后再输入tickets。
虽然完成了一个简单的登录功能,但是这样并不能阻止用户访问票据页面,输入http://localhost:8080/CustomerSupport/tickets还是照样可以访问得到票据页面,所以需要在TicketServlet的doGet和都doPost中添加检查用户是否已经登录的代码,如下:
if (request.getSession().getAttribute("username") == null) {
response.sendRedirect("login");
return;
}
之前我们在创建票据的时候需要填入用户的姓名,有了登录功能并且登录成功之后我们可以获取session中的特性username,所以不再需要这个字段,修改createTicket方法,并且删除ticketForm.jsp中这个字段:
//ticket.setCustomerName(request.getParameter("customerName"));
ticket.setCustomerName(
(String)request.getSession().getAttribute("username")
);
1.5、测试登录功能
(1)启动服务器,在浏览器中输入http://localhost:8080/CustomerSupport/,浏览器应该显示出登录页面。
(2)输入错误用户名和密码,页面还是在登录页面并且显示重试。
(3)输入正确登录,页面显示票据列表
(4)创建新的票据,提交之后页面跳转到票据显示页面并且用户名自动添加到其中。
(5)关闭浏览器重新打开,使用不同的用户名和密码重新登录
(6)创建另一个票据,该票据应该包含目录登录的用户名,而旧的票据应该使用之前登录的用户名。
1.6、添加注销功能
像上面那样我们如果要登录其他用户的时候需要重新关闭浏览器或者重新开启另一个浏览器登录,这不是一种理想的方式,理想的方式是添加一个注销的链接。在LoginServlet的doget中添加
//注销功能
if (req.getParameter("logout") != null) {
session.invalidate();
resp.sendRedirect("login");
return;
}
//3.1、检查用户是否已经登录
else if (session.getAttribute("username") != null) {
//3.2、如果登录就重定向到票据页面
resp.sendRedirect("tickets");
return;
}
并且在listTickets.jsp,ticketForm.jsp和viewTicket.jsp的
前添加:
">Logout
">Logout
最后重启服务器,登录之后所有页面都出现注销链接,点击页面将回到登录页面。注意
2、使用监听器检测会话的变化
2.1、使用监听器
我们要实现的功能是当创建新的会话时候,控制台输出一条相关记录,删除(关闭浏览器,注销,过期)的时候,控制台输出一条相关记录。这样的功能需要通过创建监听器类并且实现特定监听接口来实现。
有三种方式来实现监听器,注解,编程和部署描述符里声明,我们这里使用注解(最简单)。
- 创建一个监听类,@WebListener表名该类是一个监听器,对会话实现监听就需要实现特定的监听接口,HttpSessionListener接口定义了sessionCreated,sessionDestroyed,它们会在会话创建和会话无效的时候调用,HttpSessionIdListener接口定义了sessionIdChanged当使用请求的changeSessionId方法(请求的时候会话创建,登录成功后修改该会话的Id,避免会话固定攻击)改变会话id时将调用该方法。这三个方法都使用一个简单辅助方法date在会话活动日志中添加时间戳。
@WebListener
public class SessionListener implements HttpSessionListener, HttpSessionIdListener {
private SimpleDateFormat formatter =
new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss");
@Override
public void sessionIdChanged(HttpSessionEvent event, String oldSessionId) {
System.out.println(this.date() + ": Session ID " + oldSessionId +
" changed to " + event.getSession().getId());
}
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println(this.date() + ": Session " + se.getSession().getId() +
" created.");
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println(this.date() + ": Session " + se.getSession().getId() +
" destroyed.");
}
private String date() {
return this.formatter.format(new Date());
}
}
重启服务器,访问程序可以看到,登录和注销可以看到调试窗口出现
最后,为什么创建,id修改,失效会话的时候会调用接口中的方法呢?该功能的实现通过发布订阅模式实现(设计模式),从而可以将修改会话和监听会话变化的代码解耦。
2.2、维护活跃会话列表
我们要实现的功能是例如管理员想看所有session的调试窗口中日志信息。我们需要创建一个servlet来维护一个map,该map是静态,键是会话ID,值是会话对象,并且实现相应的会话增删改查方法,然后扩展之前的会话监听器类。这个map好像是在把会话复制了一份,其实并不是,会话已经在内存中创建,这个map只是存储了这些会话引用。这是一种轻量级的操作。因为该类的所有方法都是私有的,构造函数也是私有的,防止创建它的实例。
public final class SessionRegistry {
private static final Map SESSIONS = new Hashtable<>();
public static void addSession(HttpSession session){
SESSIONS.put(session.getId(), session);
}
public static void updateSession(HttpSession session, String oldSessionId){
synchronized (SESSIONS){
SESSIONS.remove(oldSessionId);
addSession(session);
}
}
public static void removeSession(HttpSession session){
SESSIONS.remove(session);
}
public static List getAllSessions(){
return new ArrayList<>(SESSIONS.values());
}
public static int getNumberOfSessions(){
return SESSIONS.size();
}
//设置私有构造器,避免默认构造器创建实例
private SessionRegistry(){
}
}
接着扩展监听器SessionListener类,在sessionCreated里添加
SessionRegistry.addSession(se.getSession());
在sessionDestroyed里添加
SessionRegistry.removeSession(se.getSession());
在sessionIdChanged里添加
SessionRegistry.updateSession(event.getSession(), oldSessionId);
现在会话会在合适的时间被添加到注册表中或从注册表中删除。接着我们需要创建一个Servlet来处理响应并且创建一个jsp来显示他们。
@WebServlet(
name = "sessionListServlet",
urlPatterns = "/sessions"
)
public class SessionListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//先判断是否登录
if (req.getSession().getAttribute("username") == null) {
resp.sendRedirect("login");
return;
}
//登录后设置请求特性然后转发到sessions.jsp
req.setAttribute("numberOfSessions",
SessionRegistry.getNumberOfSessions());
req.setAttribute("sessionList", SessionRegistry.getAllSessions());
req.getRequestDispatcher("/WEB-INF/jsp/view/sessions.jsp")
.forward(req, resp);
}
}
定义一个方法去换算时间并且格式化显示,getLastAccessedTime()返回的是用户最后访问会话的时间。并且使用jsp中session内置对象判断aSession是否是当前session,是的话加上提示you。
<%@ page import="java.util.List" %>
<%!
private static String toString(Long timeInterval) {
if (timeInterval < 1_000)
return "less than one second";
if (timeInterval < 60_000)
return (timeInterval / 1_000) + "seconds";
return "about" + (timeInterval/60_000) + "minutes";
}
%>
<%
int numberOfSessions = (Integer)request.getAttribute("numberOfSessions");
@SuppressWarnings("unchecked")
List sessions =
(List)request.getAttribute("sessionList");
%>
Customer Support
">Logout
Sessions
There are a total of <%= numberOfSessions %> active sessions in this
application.
<%
long timestamp = System.currentTimeMillis();
for(HttpSession aSession : sessions)
{
out.print(aSession.getId() + " - " +
aSession.getAttribute("username"));
if(aSession.getId().equals(session.getId()))
out.print(" (you)");
out.print(" - last active " +
toString(timestamp - aSession.getLastAccessedTime()));
out.println(" ago
");
}
%>
最后测试,重启服务器,访问项目先登录之后访问http://localhost:8080/CustomerSupport/sessions
奇怪为什么会有四个,按道理只有一个的,原因是jsp也会创建session,servlet的中 HttpSession session = req.getSession();也会创建session。把index.jsp去掉就好了。session是什么时候被创建,session是什么时候被创建
启动另一个浏览器,登录不同的用户之后再访问http://localhost:8080/CustomerSupport/sessions