java初探之登录总结
Posted lovejune
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java初探之登录总结相关的知识,希望对你有一定的参考价值。
登录总结
前几章总结了登录各个步骤中遇到的问题,现在完成的做一个登录的案例,其难点不在于实现功能,而在于抽象各种功能模块,提高复用性,较低耦合度。
前端页面:
对于前端页面来说,不是后端程序员要考虑的事,但为了有备无患,需要了解一些基本的东西,即看的懂即可,原则是,可以不去管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 ‘0‘ 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("1d{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初探之登录总结的主要内容,如果未能解决你的问题,请参考以下文章