登录总结

前几章总结了登录各个步骤中遇到的问题,现在完成的做一个登录的案例,其难点不在于实现功能,而在于抽象各种功能模块,提高复用性,较低耦合度。

前端页面:

对于前端页面来说,不是后端程序员要考虑的事,但为了有备无患,需要了解一些基本的东西,即看的懂即可,原则是,可以不去管css的样式,但js代码还是要多了解。

比如,对于登录页面来说,一般是不会使用表单直接提交,因为有大量的验证工作,因此,需要使用ajax请求技术来完成登录的请求。在请求之前,势必要先对表单上输入的一些内容进行验证,比如,输入的手机号是否为数字型的,长度以及是否为空,这样的验证。

  • jquery-validation

  在页面中引入该插件,以及中文提醒js插件,即可对一些规则默认校验

  
(1)required:true               必输字段
(2)remote:"check.php" 使用ajax方法调用check.php验证输入值
(3)email:true 必须输入正确格式的电子邮件
(4)url:true 必须输入正确格式的网址
(5)date:true 必须输入正确格式的日期
(6)dateISO:true 必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22 只验证格式,不验证有效性
(7)number:true 必须输入合法的数字(负数,小数)
(8)digits:true 必须输入整数
(9)creditcard: 必须输入合法的信用卡号
(10)equalTo:"#field" 输入值必须和#field相同
(11)accept: 输入拥有合法后缀名的字符串(上传文件的后缀)
(12)maxlength:5 输入长度最多是5的字符串(汉字算一个字符)
(13)minlength:10 输入长度最小是10的字符串(汉字算一个字符)
(14)rangelength:[5,10] 输入长度必须介于 5 和 10 之间的字符串")(汉字算一个字符)
(15)range:[5,10] 输入值必须介于 5 和 10 之间
(16)max:5 输入值不能大于5
(17)min:10 输入值不能小于10
  •   ajax请求

  

  $(function(){
//请求参数
var list = {};
//
$.ajax({
//请求方式
type : "POST",
//请求的媒体类型
contentType: "application/json;charset=UTF-8",
//请求地址
url : "http://127.0.0.1/admin/list/",
//数据,json字符串
data : JSON.stringify(list),
//请求成功
success : function(result) {
console.log(result);
},
//请求失败,包含具体的错误信息
error : function(e){
console.log(e.status);
console.log(e.responseText);
}
});
});
  • thymeleaf

  引入该技术,只需要在开头使用<html xmlns:th="http://www.thymeleaf.org">,使能th:标签,之后就可以使用th:来获取数据,抛弃jsp页面技术。

  • 完整的页面代码如下

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script> <style type="text/css">
html,body{
height:100%;
width:100%;
}
body{
background:url('/img/bg.jpg') no-repeat;
background-size:100% 100%;
padding-top:100px;
}
</style> </head>
<body> <form name="loginForm" id="loginForm" method="post" style="width:30%; margin:0 auto;">
<h2 style="text-align:center; margin-bottom: 20px">用户登录</h2> <div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入手机号码</label>
<div class="col-md-8">
<input id="mobile" name = "mobile" class="form-control" type="text" placeholder="手机号码" required="true" minlength="11" maxlength="11" />
</div>
<div class="col-md-1">
</div>
</div>
</div> <div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入密码</label>
<div class="col-md-8">
<input id="password" name="password" class="form-control" type="password" placeholder="密码" required="true" minlength="6" maxlength="16" />
</div>
</div>
</div> <div class="row" style="margin-top:40px;">
<div class="col-md-6">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-6">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
</div> </form>
</body>
<script>
function login(){
$("#loginForm").validate({
submitHandler:function(form){
doLogin();
}
});
}
function doLogin(){
g_showLoading(); var inputPass = $("#password").val();
var salt = g_passsword_salt;
var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
var password = md5(str); $.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
// window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
</html>

  


完成页面的设计之后,开始设计数据库,对于一个完整的用户来说,所存储的数据库不仅仅是该用户的手机号和密码,还有其昵称,头像,注册时间,上次登录时间,以及登录次数等,甚至对于一个用户,要产生随机密码加密的盐值也需要存储,因此,可以设计如下数据库表:

  • 数据表user设计

  

CREATE TABLE `miaosha_user` (
`id` BIGINT(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` VARCHAR(255) NOT NULL,
`password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
`salt` VARCHAR(10) DEFAULT NULL,
`head` VARCHAR(128) DEFAULT NULL COMMENT '头像,云存储的ID',
`register_date` DATETIME DEFAULT NULL COMMENT '注册时间',
`last_login_date` DATETIME DEFAULT NULL COMMENT '上蔟登录时间',
`login_count` INT(11) DEFAULT '' COMMENT '登录次数',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

设计好数据库之后,根据数据库,设计实体类,实体类中包括所有的数据库字段,当然也可以有数据库没有的字段,按照项目的代码逻辑来定,而我们的这个项目,就和数据库字段一致。

  • domain包下的User类

  

public class MiaoShaUser {

    private Long id;
private String nickname;
private String password;
private String salt;
private String head;
private Date registerDate;
private Date lastLoginDate;
private Integer loginCount; public Long getId() {
return id;
} public void setId(Long id) {
this.id = id;
} public String getNickname() {
return nickname;
} public void setNickname(String nickname) {
this.nickname = nickname;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
} public String getSalt() {
return salt;
} public void setSalt(String salt) {
this.salt = salt;
} public String getHead() {
return head;
} public void setHead(String head) {
this.head = head;
} public Date getRegisterDate() {
return registerDate;
} public void setRegisterDate(Date registerDate) {
this.registerDate = registerDate;
} public Date getLastLoginDate() {
return lastLoginDate;
} public void setLastLoginDate(Date lastLoginDate) {
this.lastLoginDate = lastLoginDate;
} public Integer getLoginCount() {
return loginCount;
} public void setLoginCount(Integer loginCount) {
this.loginCount = loginCount;
} @Override
public String toString() {
return "MiaoShaUser{" +
"id=" + id +
", nickname='" + nickname + '\'' +
", password='" + password + '\'' +
", salt='" + salt + '\'' +
", head='" + head + '\'' +
", registerDate=" + registerDate +
", lastLoginDate=" + lastLoginDate +
", loginCount=" + loginCount +
'}';
}
}

这里还有一种更简单的书写方式,引入@Data包,可以自动加入getshet方法,不需要手动添加了。该注解属于lombok.Data;包。

设计好了实体类,理论上应该设计持久层dao,提供所有CURD操作的方法,供给业务层使用,但事实上,如果我们只编写登录业务的话,是不需要很多与数据库交互的方法,只需要查询方法即可。而且对于本项目来说,只需要提供根据id查询的方法即可。

首先,我们引入配置信息,springboot的配置信息都可以写到application.properties文件中去

  • application.properties中mysql数据库的配置 

# druid数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

因为springboot的高度集成化,我们不需要任何操作,只需要在dao类前加注解即可识别该配置文件。事实上,如果不能识别,我们也可以直接添加一个Util类,只需要建立一个连接,将配置信息获取到,填入即可。

获取到数据库的配置之后,我们可以直接编写我们的dao类,前面分析过,只需要提供一个查询方法即可,dao类的实现有两种方法,一种是用xml配置的方式,一种是注解的方式。使用xml的方式需要额外在配置文件中说明指定的类,但是有很好的解耦合性,可以适合协同开发。但注解的方式可以更加简单,事实上也不会太影响解耦合。下面使用注解的方式编写dao类

  • UserDao类的开发

  

@Repository
@Mapper
public interface MiaoShaUserDao { //1.根据id查询
@Select("select * from miaosha_user where id = #{id}")
MiaoShaUser getById(@Param("id") Long id); //2.更新秒杀用户,用于修改密码
@Update("update miaosha_user set password = #{password} where id = #{id}")
void update(MiaoShaUser tobeUpdate);
}

开发到此,我们的持久层算是开发完了一半,如果一个网站所有的数据都存到数据库中,在查询中势必会造成响应慢,所以使用REDIS缓存技术对数据库的数据进行缓存,在数据没有修改的时候,不需要更新缓存,因此,比直接查询数据库要快很多。

使用redis技术,是key-value形式,因此在存储的时候key值得选取就很重要,对于一个大型项目,要存储的数据有多种多样的分类,因此,将key值单独抽象出来很有必要,抽象某一概念,可以使用这样的步骤:接口->抽象类->适用类。对于key来说,首先要提供一个前缀的方法,这个前缀一定是与所要存储的类有关的;还需要一个过期时间,过期后从redis中删去,避免占据内存。

  • KeyPrefix接口

  

public interface KeyPrefix {

    public int expireSeconds();//返回过期时间

    public String getPrefix();//获取key

}

接下来,定义一个基础的抽象类,提供属性和一些默认的方法。

  • BasePrefix抽象类

  

public abstract class BasePrefix implements KeyPrefix{

    private int expireSeconds;

    private String prefix;

    public BasePrefix(String prefix) {//0代表永不过期
this(0, prefix);
} public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
} public int expireSeconds() {//默认0代表永不过期
return expireSeconds;
} public String getPrefix() {
String className = getClass().getSimpleName();//利用反射获取子类的名称
return className+":" + prefix;//使用类名+前缀(id)拼接
} }

该抽象类提供了两个属性,和两个构造方法,两个get方法。一个默认永不过期的构造方法,一个可以指定过期日期的构造方法。而提供获取前缀的方法,是通过获取类名加上前缀来获得的。

  • UserKey实现类

  继承BasePrefix抽象类,来实现具体的UserKey类,此类中主要指定实现类的过期时间,UserKey的过期时间是两天,然后提供了两个静态的实例对象,一边其他类能获取到具体的key值

public class MiaoshaUserKey extends BasePrefix{

    public static final int TOKEN_EXPIRE = 3600*24 * 2;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
public static MiaoshaUserKey getById = new MiaoshaUserKey(0, "id"); }

  以上定义了我们用于redis存储的key值抽象,当需要存储User类时,只需要将UserKey中的方法获取到key的前缀,然后加上指定的key值就会保证不会存储重复的值,也便于管理。

  下面我们来实现一个redis服务。首先要在配置文件中获取到redis对象-jedis。

  • application.properties中redisl数据库的配置

  

#redis
redis.host=192.168.10.204
redis.port=6379
redis.timeout=3
redis.password=123456
redis.poolMaxTotal=10
redis.poolMaxIdle=10
redis.poolMaxWait=3

接下来,需要整合jedis的配置类以及redis的工厂类

  • RedisConfig

  

@Component
@ConfigurationProperties(prefix="redis") // 可以读取application.properties文件中redis开头的值
public class RedisConfig {
// redis.host=192.168.10.204
// redis.port=6379
// redis.timeout=3
// redis.password=123456
// redis.poolMaxTotal=10
// redis.poolMaxIdle=10
// redis.poolMaxWait=3
private String host;
private int port;
private int timeout;//秒
private String password;
private int poolMaxTotal;
private int poolMaxIdle;
private int poolMaxWait;//秒
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getPoolMaxTotal() {
return poolMaxTotal;
}
public void setPoolMaxTotal(int poolMaxTotal) {
this.poolMaxTotal = poolMaxTotal;
}
public int getPoolMaxIdle() {
return poolMaxIdle;
}
public void setPoolMaxIdle(int poolMaxIdle) {
this.poolMaxIdle = poolMaxIdle;
}
public int getPoolMaxWait() {
return poolMaxWait;
}
public void setPoolMaxWait(int poolMaxWait) {
this.poolMaxWait = poolMaxWait;
}
}

该类使用了这样一个注解@ConfigurationProperties,可以读取我们在配置文件中的内容,当然也可以使用@value来获取,但@ConfigurationProperties注解更简单一些,只要属性名和字段名相同,就可以得到配置文件中的值。

接下来,该提供一个会的jedisPool的类,来获取JedisPool对象

  • RedisPoolFactory

@Service
public class RedisPoolFactory { @Autowired
RedisConfig redisConfig; @Bean//将JedisPool声明为bean
public JedisPool JedisPoolFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort());
return jp;
} }

上面这些工作属于脚手架的工作,真正的redis服务应该是提供存储的具体功能,当然这个功能类最好是泛型的,即redis能存储各种类型的数据,但事实上,redis只能存储五种类型的数据,因此需要将待存储的类转换为json字符串,然后通过字符串存到redis中。

public static <T> String beanToString(T value) {
if(value == null) {
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
}

这里的JSON类使用com.alibaba.fastjson.JSON;这个包。

同样的,当我们从redis中取出数据后,需要将字符串恢复成类

public static <T> T stringToBean(String str, Class<T> clazz) {
if(str == null || str.length() <= 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return (T)Integer.valueOf(str);
}else if(clazz == String.class) {
return (T)str;
}else if(clazz == long.class || clazz == Long.class) {
return (T)Long.valueOf(str);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}

完整的代码,来进行存取删等操作

  • RedisService

@Service
public class RedisService { @Autowired
JedisPool jedisPool; /**
* 获取当个对象
* */
public <T> T get(KeyPrefix prefix, String key, Class<T> clazz) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
String str = jedis.get(realKey);
T t = stringToBean(str, clazz);
return t;
}finally {
returnToPool(jedis);
}
} /**
* 设置对象
* */
public <T> boolean set(KeyPrefix prefix, String key, T value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String str = beanToString(value);
if(str == null || str.length() <= 0) {
return false;
}
//生成真正的key
String realKey = prefix.getPrefix() + key;
int seconds = prefix.expireSeconds();
if(seconds <= 0) {
jedis.set(realKey, str);
}else {
jedis.setex(realKey, seconds, str);
}
return true;
}finally {
returnToPool(jedis);
}
} /**
* 判断key是否存在
* */
public <T> boolean exists(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.exists(realKey);
}finally {
returnToPool(jedis);
}
} /**
* 增加值
* */
public <T> Long incr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.incr(realKey);
}finally {
returnToPool(jedis);
}
} /**
* 减少值
* */
public <T> Long decr(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
return jedis.decr(realKey);
}finally {
returnToPool(jedis);
}
} public static <T> String beanToString(T value) {
if(value == null) {
return null;
}
Class<?> clazz = value.getClass();
if(clazz == int.class || clazz == Integer.class) {
return ""+value;
}else if(clazz == String.class) {
return (String)value;
}else if(clazz == long.class || clazz == Long.class) {
return ""+value;
}else {
return JSON.toJSONString(value);
}
} @SuppressWarnings("unchecked")
public static <T> T stringToBean(String str, Class<T> clazz) {
if(str == null || str.length() <= 0 || clazz == null) {
return null;
}
if(clazz == int.class || clazz == Integer.class) {
return (T)Integer.valueOf(str);
}else if(clazz == String.class) {
return (T)str;
}else if(clazz == long.class || clazz == Long.class) {
return (T)Long.valueOf(str);
}else {
return JSON.toJavaObject(JSON.parseObject(str), clazz);
}
}
public List<String> scanKeys(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
List<String> keys = new ArrayList<String>();
String cursor = "0";
ScanParams sp = new ScanParams();
sp.match("*"+key+"*");
sp.count(100);
do{
ScanResult<String> ret = jedis.scan(cursor, sp);
List<String> result = ret.getResult();
if(result!=null && result.size() > 0){
keys.addAll(result);
}
//再处理cursor
cursor = ret.getStringCursor();
}while(!cursor.equals("0"));
return keys;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public boolean delete(KeyPrefix prefix) {
if(prefix == null) {
return false;
}
List<String> keys = scanKeys(prefix.getPrefix());
if(keys==null || keys.size() <= 0) {
return true;
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(keys.toArray(new String[0]));
return true;
} catch (final Exception e) {
e.printStackTrace();
return false;
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 删除
* */
public boolean delete(KeyPrefix prefix, String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//生成真正的key
String realKey = prefix.getPrefix() + key;
long ret = jedis.del(realKey);
return ret > 0;
}finally {
returnToPool(jedis);
}
}
private void returnToPool(Jedis jedis) {
if(jedis != null) {
jedis.close();
}
} }

事实上,springboot提供了一个RedisTemplate类不需要配置,只需要在application.properties中配置连接信息即可,可以替代上述方案。


截止现在,持久层的工作已经全部完成,这些工作极少设计业务逻辑,因为业务层代码比较复杂,因此先编写表现层的代码,首先,页面已经完成,需要的是对ajax发来的请求进行响应,我们知道,提交的数据只有id和密码,如果使用User类作为接收的bean对象,既不符合逻辑,又浪费资源。因此,可以新建一个实体类,专门响应页面的传值,该类只有两个属性,一个手机号id,一个密码。

在该类的属性中,有需要验证的内容,因此可以使用javax.validation包下的注解进行验证。也可以自定义验证注解。

对于手机号码来说,有一定的验证规则:11位数字,首数字为1,这个规则通过正则匹配很好验证,但如果需要注解验证,还需要明白其中的原理。

首先,要自定义一个IsMobile注解

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile { boolean required() default true; String message() default "手机号码格式错误"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { };
}

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })、 @Retention(RUNTIME) 、@Documented 这三个注解是java的元注解,定义一个注解的基本注解,规定了注解能用在哪里,用多长时间等内容,而@Constraint(validatedBy = {IsMobileValidator.class })这个注解代表了注解所代表的逻辑代码功能。

里面的默认方法规定了一些属性值等

然后定义IsMobileValidator类,来实现注解的具体逻辑功能。

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
} public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
} }

继承ConstraintValidator接口,要实现两个方法,初始化方法,将该注解@IsMobile 中的require传到类中,作为默认值,验证逻辑在isValid方法中,需要定义一个验证的工具类,这个类中编写具体验证代码

public class ValidatorUtil {

    private static Pattern MOBILE_PATTERN = Pattern.compile("1\\d{10}");

    public static boolean isMobile(String mobile){
if(StringUtils.isEmpty(mobile)){
return false;
} Matcher matcher = MOBILE_PATTERN.matcher(mobile);
return matcher.matches();
} }

这个类就是提供一个简单的验证方法,返回一个布尔值。

以上就是自定义验证注解的过程

这些验证注解的使用在我们定义的验证实体类中,该类只有一个id属性和密码属性

  • LoginVo类

  

public class LoginVo {

    @NotNull
@IsMobile
private String mobile; @NotNull
@Length(min=32)
private String password; public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
}

验证注解只要验证是否为空,长度对不对,以及是否符合手机号规则

有了这样一个实体类,可以编写表现层的代码。表现层在springboot里就是一个controller类,该类获取页面的请求,并将请求数据进行一定处理(并不一定在表现层处理),然后返回值,或者转到渲染页面。由于我们使用ajax请求,所以我们只返回一定规则的值来指示ajax要怎么处理数据。

因此,还需要抽象出一个返回结果的类。此类一般包括三个部分{code:xx,msg:xx,data:xx}这种形式。对于msg我们需要封装号各类消息,以便显示。

首先,封装一个消息类

  • CodeMsg

public class CodeMsg {

    private int code;
private String msg; //通用的错误码
public static CodeMsg SUCCESS = new CodeMsg(0, "success");
public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(500102, "请求非法");
public static CodeMsg ACCESS_LIMIT_REACHED= new CodeMsg(500104, "访问太频繁!");
//登录模块 5002XX
public static CodeMsg SESSION_ERROR = new CodeMsg(500210, "Session不存在或者已经失效");
public static CodeMsg PASSWORD_EMPTY = new CodeMsg(500211, "登录密码不能为空");
public static CodeMsg MOBILE_EMPTY = new CodeMsg(500212, "手机号不能为空");
public static CodeMsg MOBILE_ERROR = new CodeMsg(500213, "手机号格式错误");
public static CodeMsg MOBILE_NOT_EXIST = new CodeMsg(500214, "手机号不存在");
public static CodeMsg PASSWORD_ERROR = new CodeMsg(500215, "密码错误"); //商品模块 5003XX //订单模块 5004XX
public static CodeMsg ORDER_NOT_EXIST = new CodeMsg(500400, "订单不存在"); //秒杀模块 5005XX
public static CodeMsg MIAO_SHA_OVER = new CodeMsg(500500, "商品已经秒杀完毕");
public static CodeMsg REPEATE_MIAOSHA = new CodeMsg(500501, "不能重复秒杀");
public static CodeMsg MIAOSHA_FAIL = new CodeMsg(500502, "秒杀失败"); private CodeMsg( ) {
} private CodeMsg( int code,String msg ) {
this.code = code;
this.msg = msg;
} public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
} public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
} @Override
public String toString() {
return "CodeMsg [code=" + code + ", msg=" + msg + "]";
} }

该消息类定义了code和msg两个属性,还定义了很多静态的消息实例。

然后封装一个结果类,该类是一个泛型类,可以返回很多不同类型的对象

  • Result

public class Result<T> {
private int code;
private String msg;
private T data;//这里data为泛型,因为可以返回很多种不同类型的对象
private Result(T data) {
this.code=0;
this.msg="success";
this.data=data;
}
private Result(CodeMsg codeMsg) {
if (codeMsg!=null) {
this.code=codeMsg.getCode();
this.msg=codeMsg.getMsg();
this.data=null;
}
}
public static <T> Result<T> success(T data) {
return new Result(data);
}
public static <T> Result<T> error(CodeMsg codeMsg) {
return new Result(codeMsg);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
} }

该类提供了两个静态方法,登录成功之后返回固定的code和msg,而失败之后,返回参数为CodeMsg的结果类,

以上我们就定义好了要从表现层返回的结果类型。

接下来,需要编写表现层的代码,ajax请求里写名了请求的路径,在controller类中必须用该请求路径接收。同时,页面的id和密码属性name标签值必须和LoginVo的属性一致。

  • LoginController

@Controller
@RequestMapping("/login")
public class LoginController { @Autowired
RedisService redisService;
@Autowired
MiaoshaUserService userService;
private static Logger log = LoggerFactory.getLogger(LoginController.class); @RequestMapping("/to_login")
public String toLogin(){
return "login";
} @RequestMapping("/do_login")
@ResponseBody
//@Valid 开启参数校验
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
userService.login(response,loginVo);
return Result.success(true);
}
}

在该类中,使用了@Valid注解来开启校验,然后进行login逻辑判断,最后返回成功的结果。事实上,我们可能登陆不成功,这需要处理登陆不成功该怎么办。@Valid事实上在验证失败后会抛出错误异常,异常类型为Bindexception。因此我们需要被整个项目的异常进行统一管理,恰好spring提供了统一处理异常的机制。

接下来,自定义一个全局异常

  • GlobalException

public class GlobalException extends RuntimeException{
//将错误信息:codeMsg包装起来
//继承RuntimeException,产生GlobalException之后可以被拦截器拦截
private static final long serialVersionUID = 31665074385012932L;
private CodeMsg cm; public GlobalException(CodeMsg cm){
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
} }

该异常类只是一个普通的自定义异常,主要处理登陆失败后的异常信息。

然后定义一个异常统一管理的类

  • GlobalExceptionHandler

  

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}else if(e instanceof BindException) {
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}

该类主要使用了@ControllerAdvice和 @ExceptionHandler注解,实现了对所有异常的管理,只要产生异常,都抛到这里解决。并利用@ResponseBody注解将异常信息返回到页面。

逻辑为,如果是自定义的全局异常,返回信息为产生全局异常的信息(该信息自己在业务逻辑中自己填写),若产生的是验证失败产生的异常,则得到验证错误的信息,将该信息返回。如果是其他异常,则直接返回服务器错误信息。

代码编写至此,表现层的代码已经完毕,如果我们提交id和密码,根据@vaild验证失败,则会返回页面错误信息。


接下来是业务层代码,业务层编写具体的登陆代码

登录逻辑很简单,传回LoginVo的信息,并根据id得到User的对象,若对象存在,就判断密码是否正确,若正确,就返回成功的信息。

这个过程需要对数据库进行查询,但每次都查询数据库比较影响性能,因此,在第一次查询之后,就将该数据存到缓存中去,之后的查询在缓存中获取即可。

在登录成功之后要返回Cookie值,

首先根据id获取对象

public MiaoshaUser getById(long id) {
MiaoshaUser user=redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class);
if (user!=null) {
return user;
}
user=miaoshaUserDao.getById(id);
if (user!=null) {
redisService.set(MiaoshaUserKey.getById, ""+id, user);
}
return user;
}

然后编写添加cookie的代码

private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_TOKEN_NAME, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}

该方法将token和实体类传入,将key值得过期时间设置为cookie的过期时间,USerKey的过期时间为两天,token值为一个随机值,这样改对象与过期时间绑定存入缓存,当cookie过期后就找不到了。

提供根据token寻找数据的类

public MiaoshaUser getByToken(HttpServletResponse response, String token) {
if(StringUtils.isEmpty(token)) {
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
//延长有效期
if(user != null) {
addCookie(response, token, user);
}
return user;
}

具体的登陆代码

public String login(HttpServletResponse response, LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = Md5Util.formPass2DbPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return token;
}

该类中用到了加密的类Md5Util,该类的实现知识调用方法。不在此详述。


至此,一个完整的登陆逻辑已经完成。

 

java初探(1)之登录总结的更多相关文章

  1. [java初探总结篇]__java初探总结

    前言 终于,java初探系列的学习,要告一阶段了,java初探系列在我的计划中是从头学java中的第一个阶段,知识主要涉及java的基础知识,所以在笔记上实在花了不少的功夫.虽然是在第一阶段上面花费了 ...

  2. [java初探10]__关于数字处理类

    前言 在我们的日常开发过程中,我们会经常性的使用到数字类型的数据,同时,也会有众多的对数字处理的需求,针对这个方面的问题,在JAVA语言中.提供解决方法的类就是数字处理类 java中的数字处理类包括: ...

  3. Java 实现 ssh命令 登录主机执行shell命令

    Java 实现 ssh命令 登录主机执行shell命令 1.SSH命令 SSH 为 Secure Shell 的缩写,由 IETF 的网络小组(Network Working Group)所制定:SS ...

  4. java初探(1)之登录再探

    https://www.cnblogs.com/lovejune/p/java_login_1.html 上一章内容搭建起了登录应用场景的环境,模拟实现了登录操作,页面与后端数据的交互过程,使用的是异 ...

  5. java初探(1)之登录补充

    在登录之后,可能服务器是分布式的,因此不能通过一个本地的session来管理登录信息,导致登录的信息不能传递,即在这台服务器上可以得到用户登录信息,但在那台就得不到.因此,需要设置分布式的sessio ...

  6. java初探(1)之登录终探

    上一章讲了表单验证,数据验证和加密.这一章,将研究服务器和数据库的交互过程. 后端服务器有两种主流的形式,SQL数据库和NOSQL数据库.其中MYSQL属于SQL数据库,REDIS属于非SQL数据库. ...

  7. java初探(1)之登录初解

    初识登录 登录的应用场景 登录比较常见,大多数网站都有登录的操作.然后登录本身也从简单到复杂有着漫长的发展历史.本文记录博主对登录的应用场景的剖析,深究不在于学习如何实现,主要关注其编码思想,过程中用 ...

  8. Java简单示例-用户登录、单个页面的增删改查及简单分页

    index.html  -登录->stulist.jsp (index.html传递到LoginServlet,进行登录检测及写入session,NO返回index.html界面,OK 跳转到s ...

  9. jasig CAS 实现单点登录 - java、php客户端登录实现

    jasig CAS项目本身就是一个完整的CAS单点登录服务 1.服务端需要把  认证处理类.用户属性返回值处理类 调整成我们自己处理类即可实现单点登录 2.java客户端需要引入cas-client- ...

随机推荐

  1. Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树

    Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 目录 Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 0x00 摘要 0x01 背景概念 1.1 词向量基础 ...

  2. Java项目中经常遇到的一些异常情况

    一. 1. java.lang.nullpointerexception 这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存 ...

  3. linux tcpdump抓包Post请求

    tcpdump -s 0 -A 'tcp dst port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)' -w f ...

  4. IdentityServer4 (4) 静默刷新(Implicit)

    写在前面 1.源码(.Net Core 2.2) git地址:https://github.com/yizhaoxian/CoreIdentityServer4Demo.git 2.相关章节 2.1. ...

  5. 2020-07-13:es是去查id再根据id去查数据库这种方式好,还是所有数据都放es,直接去查es好?

    福哥答案2020-07-13: 有人觉得第一种方法好,也有人觉得第二种方法好.如果搜索字段远小于显示字段,比如搜索字段为3个,显示字段有20个,这个时候用第一种方法好.es+hbase,一般这样搭配. ...

  6. C#LeetCode刷题之#62-不同路径(Unique Paths)

    目录 问题 示例 分析 问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3680 访问. 一个机器人位于一个 m x ...

  7. jieba分词的几种形式

    1.精确模式:试图将句子最精确地分开,适合文本分析 seg_list = jieba.cut(test_text, cut_all=False) seg_list = " ".jo ...

  8. JavaScript在HTML中的基础用法总结

    网页主要由三部分组成,分别为html.CSS和Javascript.如果说HTML是肉身,CSS是皮相,那Javascript就是灵魂.因此,三者的联系与融合则至关重要.本文就来为大家讲解一下Java ...

  9. PAT 2-08. 用扑克牌计算24点(25):

    题目链接:http://www.patest.cn/contests/ds/2-08 解题思路:思路参考24点游戏技巧http://www.24game.com.cn/articles/points2 ...

  10. RPC 框架 Dubbo 从理解到使用(一)

    技术架构演变 单一应用架构 通俗地讲,"单体应用(monolith application)"就是将应用程序的所有功能都打包成一个独立的单元.当网站流量很小时,只需一个应用,将所有 ...