day02-功能实现02

Posted liyuelian

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了day02-功能实现02相关的知识,希望对你有一定的参考价值。

功能实现02

2.功能01-短信登录

2.2集群的session共享问题

集群的Session共享问题:多台Tomcat并不共享它们之间的Session存储空间,如果有多台tomcat服务器,当请求切换到不同tomcat服务时,会导致数据丢失的问题。

(1)问题具体分析:

如上,当请求进入Nginx时,Nginx会做一个负载均衡(一般是对tomcat集群进行轮询)。

假设用户的请求第一次被负载均衡到了tomcat_1,例如之前的登录验证,那么tomcat_1就会将信息保存到它的session域中;

如果用户第二次的请求被负载均衡到了tomcat_2,当tomcat_2要去获取验证码或者用户信息的时候,由于多台tomcat不能共享它们之间的Session存储空间,导致tomcat_2不能获取存储在tomcat_1的session数据,表现出来就是数据丢失的问题。

(2)解决方案:

如图,session的替代方案必须满足:数据共享、内存存储(快)、k-v结构(方便存/取数据)。

如果使用redis,则刚好可以满足这些需求:

首先,redis是在tomcat之外的一个存储方案,所有的tomcat都可以访问到,解决了数据共享问题;

其次,redis是内存存储,性能很强,读写很快,读写延时基本上是在微秒级别;

最后,redis是k-v结构,方便数据的存取。

综上,我们下面将会使用redis来实现session登录。

2.3基于Redis实现共享session登录

2.3.1思路分析

(1)发送短信验证码:

如左图所示,当用户发送短信验证码时,将生成的验证码存入到redis中,为了让一个手机号码对应一个验证码,这里使用手机号作为key,value则是验证码。(使用Redis-String类型)

(2)短信验证码登录、注册:

使用redis代替之后,我们在获取短信验证码的时候,也使用手机号为key去取验证码,这就解决了发送和登录手机号可能不一致的问题。如果需要保存用户信息,可以使用Redis-Hash类型。

(3)登录状态校验:

session机制:之前直接使用session存储用户信息后,tomcat会自动给浏览器返回一个cookie(包含jsessionid),每次向服务端发送请求时,都会自动携带。这样tomcat根据jsessionid,就能自动找到session,也就能找到session里的数据。

登录成功后,会存储一个token到redis中作为key,value就是用户信息。我们可以模仿jsessionid,存储token到redis时,同时将token放到cookie中,返回给客户端,之后每次客户端发送请求,都会携带token,服务器就可以获取token,到redis去获取用户信息,从而进行一系列业务流程。

登录凭证token保存在前端浏览器:前端代码会存储token到SessionStorage中,设置request拦截器,将用户token放入请求头中。这样,每次发送请求的时候都会使用token作为请求头。我们在服务端就可以获取token数据校验。

2.3.2代码实现

2.3.2.1发送短信验证码

在2.1代码的基础上修改

修改UserServiceImpl.java的sendCode()方法

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result sendCode(String phone, HttpSession session) 
    //1.校验手机号
    boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
    //2.如果不符合,返回错误信息
    if (phoneInvalid) //true表示不符合格式
        return Result.fail("手机号格式错误!");
    
    //3.符合,生成验证码
    String code = RandomUtil.randomNumbers(6);//生成6位数的随机号码

    //4.保存验证码到 redis中,这里的 key 最好使用业务前缀作为区分
    // set key value ex
    stringRedisTemplate.opsForValue()
            .set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
    
    //5.发送验证码(由于这里涉及到一些其他方的工具,先不做)
    log.debug("发送短信验证码成功,验证码=", code);
    //6.返回OK
    return Result.ok();

2.3.2.2短信验证码登录/注册

修改UserServiceImpl.java的login()方法

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) 
    //1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) 
        //如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    

    //2.从redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) 
        return Result.fail("验证码错误");
    

    //3.根据手机号查询用户是否已经注册
    User user = query().eq("phone", phone).one();
    if (user == null) 
        //若不存在,则在创建用户
        user = createUserWithPhone(phone);
    

    //4.保存用户到redis中 (这里只保存id、昵称、手机号)
    //随机生成token,作为登录令牌
    String token = UUID.randomUUID().toString(true);
    //将user对象转为redis-hash
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    //使用工具类转(指定转换后的数据类型为String)
    Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(), 
                    CopyOptions.create()
                    .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    //存储到redis中,设置有效期为30分钟
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, map);
    stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);


    //5.将token返回客户端
    return Result.ok(token);

2.3.2.3校验登录状态

(1)修改LoginInterceptor.java

在上述代码中,我们设置了token在redis中的过期时间。但是和session不同,redis的过期时间是绝对时间,即在设定的时间内,无论有没有使用到token,从设定的那一刻起一直到第30分钟,该数据都会被删除。

因此,我们需要在拦截器中重新设置redis的token信息:每当用户有操作时,拦截器就重新设置过期时间。

package com.hmdp.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

/**
 * @author 李
 * @version 1.0
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor 

    private StringRedisTemplate stringRedisTemplate;

    //不能使用注解@Component将拦截器注入到spring容器中!!
    //因为拦截器是在spring容器初始化之前执行的
    //因此这里使用构造器设置,在注册拦截器的时候去注入
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) 
        this.stringRedisTemplate = stringRedisTemplate;
    

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        //如果浏览器的token为空
        if (StrUtil.isBlank(token)) 
            //拦截,返回状态码-401
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;//拦截
        
        //2.基于token获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap =
                stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if (userMap.isEmpty()) 
            //拦截,返回状态码-401
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;//拦截
        
        //4.如果存在,将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO =
                BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.然后保存到ThreadLocal
        UserHolder.saveUser(userDTO);
        //6.刷新token有效期(30mins)
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7.放行
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
        //移除用户(根据当前线程,移除用户信息)
        UserHolder.removeUser();
    

(2)修改配置类 MvcConfig.java

不能使用注解@Component将拦截器注入到spring容器中!!因为拦截器是在spring容器初始化之前执行的,因此这里使用构造器设置,在注册拦截器的时候去注入

2.3.2.4测试

(1)启动redis服务器,重启项目。

(2)输入手机号,输入验证码,点击登录

(3)成功登录,显示个人主页

(4)redis存储的数据如下:

短信验证码:

token:

测试通过。

2.3.3总结

(1)基于Redis实现实现短信登录改造的点:

  • 发送短信验证码时,把短信验证码作为value,使用手机号作为key,存入redis中。

    确保了:a.每个手机号都有唯一的验证码;b.用户登录时可以基于手机号来获取验证码,从而实现验证

  • 短信登录时,保存用户信息到redis(key-token,value-用户信息),同时返回token给用户浏览器,保存在浏览器的SessionStorage。每次校验登录状态,都会从中取出token进行校验。

(2)Redis代替Session需要考虑的问题:

  • 选择合适的数据结构
  • 选择合适的key
  • 选择合适的存储粒度

2.3.4登录拦截器的优化

(1)问题分析

在之前的登录拦截器中,它的作用为:

  1. 获取token
  2. 查询redis的用户
    • 若存在,则继续
    • 若不存在,则拦截
  3. 保存到ThreadLocal
  4. 刷新token有效期
  5. 放行

但是,该拦截器不是拦截的所有请求,比如浏览首页的操作。当一个用户登录后,再去浏览首页,因为该操作不会经过登录拦截器,也就不会刷新token有效期,那么经过有限的时间内,用户的登录状态就会自动失效,这显然是不合理的。

(2)解决方案

我们可以再增加一个拦截器,该拦截器拦截所有路径,我们可以将刷新token的操作放到这个拦截器中进行,真正拦截的动作放到登录拦截器中进行。也就是说:

第一个拦截器的核心工作是得到用户,保存起来并刷新;

第二个拦截器的核心工作才是登录拦截。

(1)RefreshTokenInterceptor.java,token刷新拦截器(不进行真正的拦截)

package com.hmdp.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * @author 李
 * @version 1.0
 * 作用是刷新token,不进行拦截
 */
public class RefreshTokenInterceptor implements HandlerInterceptor 
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) 
        this.stringRedisTemplate = stringRedisTemplate;
    

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        //如果浏览器的token为空,就放行,不用刷新了
        if (StrUtil.isBlank(token)) 
            return true;
        
        //2.基于token获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if (userMap.isEmpty()) 
            return true;
        
        //4.如果存在,将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.然后保存到ThreadLocal
        UserHolder.saveUser(userDTO);
        //6.刷新token有效期(30mins)
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7.放行
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
        //移除用户(根据当前线程,移除用户信息)
        UserHolder.removeUser();
    

(2)LoginInterceptor.java,检验是否需要拦截

package com.hmdp.interceptor;

import com.hmdp.utils.UserHolder;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author 李
 * @version 1.0
 * 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        //判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) 
            //没有,需要拦截,设置状态码
            response.setStatus(401);
            //拦截
            return false;
        
        //如果有用户,则放行
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
        //移除用户(根据当前线程,移除用户信息)
        UserHolder.removeUser();
    

(3)注册拦截器

package com.hmdp.config;

import com.hmdp.interceptor.LoginInterceptor;
import com.hmdp.interceptor.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * @author 李
 * @version 1.0
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer 
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) 
        //登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns().
                excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/login",
                        "/user/code"
                ).order(1);
        //token刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**").order(0);
    

这样,无论我们在前端页面做什么操作,只要有token(登录过),都会刷新token的有效期

Java SSM 项目实战 day04 功能介绍,订单的操作,订单的增删改查,实现登录功能

Java SSM 项目实战 day02 功能介绍,SSM整合,数据库和IDEA的maven工程搭建,产品信息查询和添加
Java SSM 项目实战 day03 功能介绍,订单的操作,订单的增删改查
Java SSM 项目实战 day04 功能介绍,订单的操作,订单的增删改查,实现登录功能
Java SSM 项目实战 day05 用户操作
Java SSM 项目实战 day06 角色操作,资源权限操作
Java SSM 项目实战 day07 SpringSecurity源码分析
Java SSM 项目实战 day08 方法级别的权限操作 服务器端的权限控制(JSR-250注解)(支持表达式的注解)(@Secured)以及页面端的权限控制
Java SSM 项目实战 day09 SSMAOP日志

一、创建数据库

1、用户表

1.1.1 用户表信息描述users

1.1.2 sql语句

CREATE TABLE users( 
  id varchar2(32) default SYS_GUID() PRIMARY KEY, 
  email VARCHAR2(50) UNIQUE NOT NULL, 
  username VARCHAR2(50), 
  PASSWORD VARCHAR2(50), 
  phoneNum VARCHAR2(20), 
  STATUS INT 
)

1.2 角色表

1.2.1 角色表信息描述role

1.2.2 sql语句

CREATE TABLE role(
  id varchar2(32) default SYS_GUID() PRIMARY KEY,
   roleName VARCHAR2(50) , 
   roleDesc VARCHAR2(50)
)
1.2.4 用户与角色关联关系

用户与角色之间是多对多关系,我们通过user_role表来描述其关联,在实体类中User中存在List,在Role中有List.
而角色与权限之间也存在关系,我们会在后面介绍。

CREATE TABLE users_role( 

    userId varchar2(32),
    roleId varchar2(32), 
    PRIMARY KEY(userId,roleId), 
    FOREIGN KEY (userId) REFERENCES users(id), 
    FOREIGN KEY (roleId) REFERENCES role(id)

)

1.3 资源权限表

1.3.1 权限资源表描述permission


CREATE TABLE permission( 
  id varchar2(32) default SYS_GUID() PRIMARY KEY, 
  permissionName VARCHAR2(50) , 
  url VARCHAR2(50) 
)
1.3.4.权限资源与角色关联关系

权限资源与角色是多对多关系,我们使用role_permission表来描述。在实体类Permission中存在List,在Role类中
有List

CREATE TABLE role_permission
( 
  permissionId varchar2(32), 
  roleId varchar2(32), 
  PRIMARY KEY(permissionId,roleId), 
  FOREIGN KEY (permissionId)
  REFERENCES permission(id),
  FOREIGN KEY (roleId) 
  REFERENCES role(id) 
    
)

二、Spring Security

2.1 Spring Security介绍

Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。
(https://projects.spring.io/spring-security/) Spring Security 为基于J2EE企业应用软件提供了全面安全服务。特别
是使用领先的J2EE解决方案-Spring框架开发的企业软件项目。

人们使用Spring Security有很多种原因,不过通常吸引他们的是在J2EE Servlet规范或EJB规范中找不到典型企业应用场景的解决方案。 特别要指出的是他们不能再
WAR 或 EAR 级别进行移植。

这样,如果你更换服务器环境,就要,在新的目标环境进行大量的工作,对你的应用
系统进行重新配 置安全。

使用Spring Security 解决了这些问题,也为你提供很多有用的,完全可以指定的其他安
全特性。 安全包括两个主要操作。

“认证”,是为用户建立一个他所声明的主体。主题一般式指用户,设备或可以在你系 统中执行动作的其他系
统。
“授权”指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由 身份验证
过程建立了。

这些概念是通用的,不是Spring Security特有的。

在身份验证层面,Spring Security广泛支持各种身份验证模式,
这些验证模型绝大多数都由第三方提供,或则正在开发的有关标准机构提供的,例如 Internet Engineering Task
Force.作为补充,Spring Security 也提供了自己的一套验证功能。

Spring Security 目前支持认证一体化如下认证技术:

HTTP BASIC authentication headers (一个基于IEFT RFC 的
标准) HTTP Digest authentication headers (一个基于IEFT RFC 的标准)

HTTP X.509 client certificate exchange
(一个基于IEFT RFC 的标准) LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)

Form-based
authentication (提供简单用户接口的需求) OpenID authentication Computer Associates Siteminder JA-SIG
Central Authentication Service (CAS,这是一个流行的开源单点登录系统) Transparent authentication context
propagation for Remote Method Invocation and HttpInvoker (一个Spring远程调用协议)

二、实现登录工程

1、创建login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>数据 - AdminLTE2定制版 | Log in</title>

<meta
	content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
	name="viewport">

<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
</head>

<body class="hold-transition login-page">
	<div class="login-box">
		<div class="login-logo">
			<a href="all-admin-index.html"><b>ITCAST</b>后台管理系统</a>
		</div>
		<!-- /.login-logo -->
		<div class="login-box-body">
			<p class="login-box-msg">登录系统</p>

			<form action="${pageContext.request.contextPath}/login.do" method="post">
				<div class="form-group has-feedback">
					<input type="text" name="username" class="form-control"
						placeholder="用户名"> <span
						class="glyphicon glyphicon-envelope form-control-feedback"></span>
				</div>
				<div class="form-group has-feedback">
					<input type="password" name="password" class="form-control"
						placeholder="密码"> <span
						class="glyphicon glyphicon-lock form-control-feedback"></span>
				</div>
				<div class="row">
					<div class="col-xs-8">
						<div class="checkbox icheck">
							<label><input type="checkbox"> 记住 下次自动登录</label>
						</div>
					</div>
					<!-- /.col -->
					<div class="col-xs-4">
						<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
					</div>
					<!-- /.col -->
				</div>
			</form>

			<a href="#">忘记密码</a><br>


		</div>
		<!-- /.login-box-body -->
	</div>
	<!-- /.login-box -->

	<!-- jQuery 2.2.3 -->
	<!-- Bootstrap 3.3.6 -->
	<!-- iCheck -->
	<script
		src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script>
	<script
		src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script>
	<script
		src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script>
	<script>
		$(function() {
			$('input').iCheck({
				checkboxClass : 'icheckbox_square-blue',
				radioClass : 'iradio_square-blue',
				increaseArea : '20%' // optional
			});
		});
	</script>
</body>

</html>

2、创建登录失败的页面:failer.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>

<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>数据 - AdminLTE2定制版</title>
<meta name="description" content="AdminLTE2定制版">
<meta name="keywords" content="AdminLTE2定制版">

<!-- Tell the browser to be responsive to screen width -->
<meta
	content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
	name="viewport">

<!-- jQuery 2.2.3 -->
<!-- jQuery UI 1.11.4 -->
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
<!-- Bootstrap 3.3.6 -->
<!-- Morris.js charts -->
<!-- Sparkline -->
<!-- jvectormap -->
<!-- jQuery Knob Chart -->
<!-- daterangepicker -->
<!-- datepicker -->
<!-- Bootstrap WYSIHTML5 -->
<!-- Slimscroll -->
<!-- FastClick -->
<!-- iCheck -->
<!-- AdminLTE App -->
<!-- 表格树 -->
<!-- select2 -->
<!-- bootstrap color picker -->
<!-- bootstrap time picker -->
<!--<script src="${pageContext.request.contextPath}/${pageContext.request.contextPath}/${pageContext.request.contextPath}/plugins/timepicker/bootstrap-timepicker.min.js"></script>-->
<!-- Bootstrap WYSIHTML5 -->
<!--bootstrap-markdown-->
<!-- CK Editor -->
<!-- InputMask -->
<!-- DataTables -->
<!-- ChartJS 1.0.1 -->
<!-- FLOT CHARTS -->
<!-- FLOT RESIZE PLUGIN - allows the chart to redraw when the window is resized -->
<!-- FLOT PIE PLUGIN - also used to draw donut charts -->
<!-- FLOT CATEGORIES PLUGIN - Used to draw bar charts -->
<!-- jQuery Knob -->
<!-- Sparkline -->
<!-- Morris.js charts -->
<!-- Ion Slider -->
<!-- Bootstrap slider -->
<!-- 页面meta /-->

<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/morris/morris.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/jvectormap/jquery-jvectormap-1.2.2.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/datepicker/datepicker3.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/daterangepicker/daterangepicker.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/datatables/dataTables.bootstrap.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/treeTable/jquery.treetable.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/treeTable/jquery.treetable.theme.default.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/select2/select2.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/colorpicker/bootstrap-colorpicker.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/bootstrap-markdown/css/bootstrap-markdown.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/adminLTE/css/skins/_all-skins.min.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/css/style.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/ionslider/ion.rangeSlider.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/ionslider/ion.rangeSlider.skinNice.css">
<link rel="stylesheet"
	href="${pageContext.request.contextPath}/plugins/bootstrap-slider/slider.css">
</head>

<body class="hold-transition skin-purple sidebar-mini">

	<div class="wrapper">

		<!-- 页面头部 -->
		<!-- 页面头部 -->
<header class="main-header">
	<!-- Logo -->
	<a href="all-admin-index.html" class="logo"> <!-- mini logo for sidebar mini 50x50 pixels -->
		<span class="logo-mini"><b>数据</b></span> <!-- logo for regular state and mobile devices -->
		<span class="logo-lg"><b>数据</b>后台管理</span>
	</a>
	<!-- Header Navbar: style can be found in header.less -->
	<nav class="navbar navbar-static-top">
		<!-- Sidebar toggle button-->
		<a href="#" class="sidebar-toggle" data-toggle="offcanvas"
			role="button"> <span class="sr-only">Toggle navigation</span>
		</a>

		<div class="navbar-custom-menu">
			以上是关于day02-功能实现02的主要内容,如果未能解决你的问题,请参考以下文章

JavaWeb网上图书商城完整项目--day02-6.ajax校验功能之页面实现

day02-项目实现01

团队项目个人进展——Day02

Day02-Java basic

04mvc框架原理(8days)02

JavaWeb网上图书商城完整项目--day02-10.提交注册表单功能之页面实现