二、实现登陆功能
- 数据库设计
- 明文密码两次MD5处理
- JSR303参数校验+全局异常处理器
- 分布式Session
1、数据库设计(借用以往使用过的SQL)
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
2、明文密码两次MD5处理
- 用户端:pass = MD5(明文+固定Salt)
- 服务端:pass = MD5(用户输入+随机Salt)
因为http在网络上传输数据是通过明文来传输的,也即是说,如果用户登陆时,若输入明文密码,不做任何处理,那么他的明文密码就会在网络上传输,如果其他人截取到数据包,那么他就可以得到你的明文密码,所以要对用户输入的密码做一次MD5,然后再把MD5的密码传输给服务端,这就是第一次MD5。服务端接收到来自客户端经过第一次MD5加密后的密码,并不是直接将其写入到数据库中,而是生成一个随机的Salt,然后跟用户输入的密码进行拼装,再做一次MD5,然后把MD5和Salt同时写入数据库中,这是第二次MD5.。
为什么这么做?
主要是为安全考虑。第一次MD5是为了防止用户的明文密码在网络上传输。
服务端的MD5(第二次MD5)主要是为了防止:假如我们的数据库被盗取,如果只做一次MD5,有一种叫彩虹表,可以反查,他可以根据你的MD5值反推出密码是多少,这样,就有可能盗取数据库之后,通过反查那个表,直接获得密码,所以一定要再做一次MD5,.这是为了双重保险。
2.1)首先要添加MD5的依赖
2.2)然后写一个MD5的工具类,添加主函数,进行简单测试
public class MD5Util {
/**
* md5 操作
* @param src
* @return
*/
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
/**
* 这里为什么写死?如果不写死,服务端根本不知道这个串是什么东西
*/
private static final String salt = "1a2b3c4d";
/**
* 第一次 md5,提高传输过程中的数据安全性
* @param inputPass
* @return
*/
public static String inputPassFromPass(String inputPass) {
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
/**
* 第二次 md5,提高数据库中数据的安全性
* @param formPass
* @param salt
* @return
*/
public static String formPassToDBPass(String formPass, String salt) {
String str = "" + salt.charAt(0) + salt.charAt(2) + formPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String inputPassToDbPass(String input, String saltDB) {
String formPass = inputPassFromPass(input);
String dbPass = formPassToDBPass(formPass, saltDB);
return dbPass;
}
public static void main(String[] args) {
System.out.println(inputPassFromPass("123456"));
System.out.println(formPassToDBPass(inputPassFromPass("123456"), "1a2b3c4d"));
System.out.println(inputPassToDbPass("123456", "1a2b3c4d"));
}
}
2.3)接着在数据库中添加一条记录
2.4)建立一个跟数据库用户表对应的pojo:SecKillUser
public class SecKillUser {
private Long id;
private String nickname;
private String password;
private String salt;
private String head;
private Date registerDate;
private Date lastLoginDate;
private Integer loginCount;
...
2.5)然后新建对应的SecKillUserDao
@Mapper
public interface SecKillUserDao {
@Select("select * from miaosha_user where id = #{id}")
public SecKillUser getById(@Param("id")long id);
}
2.6)再写对应得service:SecKillUserService
@Service
public class SecKillUserService {
@Autowired
private SecKillUserDao secKillUserDao;
public SecKillUser getById(long id) {
return secKillUserDao.getById(id);
}
public CodeMsg login(LoginVo loginVo) {
if(loginVo == null) {
return CodeMsg.SERVER_ERROR;
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
SecKillUser user = getById(Long.parseLong(mobile));
if(user == null) {
return CodeMsg.MOBILE_NOT_EXIST;
}
//验证密码
String dbPass = user.getPassword();
String slatDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, slatDB);
if(!calcPass.equals(dbPass)) {
return CodeMsg.PASSWORD_ERROR;
}
return CodeMsg.SUCCESS;
}
}
2.7)在这个过程中,为了方便,还写了一个VO:
public class LoginVo {
private String mobile;
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 + "]";
}
}
2.8)补充CodeMsg类的内容
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, "密码错误");
2.9)写LoginController:
@Controller
@RequestMapping("/login")
public class LoginController {
/**
* 使用slf4j,因为它是一个接口,具体实现可以选择其他,非常方便
*/
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
private SecKillUserService userService;
@GetMapping("/to_login")
public String toLogin() {
return "login";
}
@PostMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(LoginVo loginVo) {
log.info(loginVo.toString());
//参数校验
String passinput = loginVo.getPassword();
String mobile = loginVo.getMobile();
if (StringUtils.isEmpty(passinput)) {
return Result.error(CodeMsg.PASSWORD_EMPTY);
}
if (StringUtils.isEmpty(mobile)) {
return Result.error(CodeMsg.MOBILE_EMPTY);
}
if (!ValidatorUtil.isMobile(mobile)) {
return Result.error(CodeMsg.MOBILE_ERROR);
}
//登陆
CodeMsg cm = userService.login(loginVo);
if (cm.getCode() == 0) {
return Result.success(true);
} else {
return Result.error(cm);
}
}
}
2.10)补上一个校验手机号的工具类:ValidatorUtil
2.11)写页面(这里直接使用原有的页面)
使用bootstrap来画页面,jquery-validation做form表单验证,用layer的js做弹框,用md5的js做md5
2.12)启动工程,进行测试。
登陆的功能基本完成!
3、JSR303参数校验+全局异常处理器
为什么要加入JSR303?
因为,在登陆中有参数校验,如果我们还有其他方法,每一个方法的开头都要做参数校验,那么造成大量的冗余代码(各种判空操作…),那么有没有更简单的方式呢?
此时就需要JSR303,现在已经是JSR的一个标准。
3.1)先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
3.2)下面我们用JSR303来校验,改造我们的登陆代码:
(1)参数前加一个标签:@Valid LoginVo loginVo
@PostMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo) {
log.info(loginVo.toString());
...
(2)在实体类中,需要加校验的字段上加注解
public class LoginVo {
@NotNull
@isMobile
private String mobile;
@NotNull
@Length(min=32)
private String password;
...
这里学习如何自定义校验器@isMobile
3.3)我们可以仿照@NotNull来定义@isMobile
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}
(1)新建一个类
public class IsMobile {
}
(2)仿照@NotNull进行修改
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface IsMobile {
}
(3)再添加:
public @interface IsMobile {
//参数默认值,必须有,不可为空
boolean required() default true;
//若校验不通过,提示什么信息
String message() default "手机号码格式有误!!!";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
这样做完是不是就可以了呢?
很明显,即便打上@IsMobile的注解,系统也并不知道如何来判断手机号是不是合法。
(4)还需要写一个类IsMobileValidator,来实现具体验证功能:
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
@Override
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
@Override
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);
}
}
}
}
(5)修改注解
@Constraint(validatedBy = { })
public @interface IsMobile {
为:
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {
这就是说,遇到注解@IsMobile,就去执行IsMobileValidator类的操作。
(6)修改LoginController:我们使用一个@Valid注解,替代了参数校验,简化了代码
@PostMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo) {
log.info(loginVo.toString());
/*
//参数校验
String passinput = loginVo.getPassword();
String mobile = loginVo.getMobile();
if (StringUtils.isEmpty(passinput)) {
return Result.error(CodeMsg.PASSWORD_EMPTY);
}
if (StringUtils.isEmpty(mobile)) {
return Result.error(CodeMsg.MOBILE_EMPTY);
}
if (!ValidatorUtil.isMobile(mobile)) {
return Result.error(CodeMsg.MOBILE_ERROR);
}
*/
//登陆
CodeMsg cm = userService.login(loginVo);
if (cm.getCode() == 0) {
return Result.success(true);
} else {
return Result.error(cm);
}
}
(7)运行程序,测试自定义注解是否起作用:
{"timestamp":1547867695605,"status":400,"error":"Bad Request","exception":"org.springframework.validation.BindException","errors":[{"codes":["IsMobile.loginVo.mobile","IsMobile.mobile","IsMobile.java.lang.String","IsMobile"],"arguments":[{"codes":["loginVo.mobile","mobile"],"arguments":null,"defaultMessage":"mobile","code":"mobile"},true],"defaultMessage":"手机号码格式有误!!!","objectName":"loginVo","field":"mobile","rejectedValue":"35036356734","bindingFailure":false,"code":"IsMobile"}],"message":"Validation failed for object='loginVo'. Error count: 1","path":"/login/do_login"}
从以上来看,我们的自定义校验器已经起作用了,那么如何使得提示信息显得更友好呢?
此时就用到异常的拦截,也就是说,我们只要拦截到这样的绑定异常,然后输出一个错误信息,就可以了
3.4)如何自定义一个全局的异常拦截器呢?
(1)新建一个类:GlobalExceptionHandler
@ControllerAdvice //切面
@ResponseBody //为了方便输出,使用@ResponseBody
public class GlobalExceptionHandler {
//这个类就相当于一个controller
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
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);
}
}
}
(2)在CodeMsg类中添加这个绑定异常
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
(3)因为异常绑定错误的错误码中,带有参数%s,所以要写一个带参数的错误码fillArgs,直接添加在CodeMsg类中:
//可返回一个带参数的错误码
public CodeMsg fillArgs(Object...args) {
//原生的code
int code = this.code;
//将原生的msg拼接上参数,形成新的msg
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}
(4)再进行测试:
完成!!!
这样一来,所有的方法都不需要做参数校验了,代码冗余低。
这样是很完美了吗?
当然不是,还有个小问题:
SecKillUserService中的public CodeMsg login(LoginVo loginVo) {...} 这个方法的返回值是CodeMsg,实际来说,我们的正常业务,不应该返回这么个对象,而是应该返回一个确切的表达我们这个方法的含义的一个对象。CodeMsg在这里并不合适。那么我们该怎么做呢?
3.5)我们定义一个全局的异常类GlobalException:
public class GlobalException extends RuntimeException {
private static final long serialVersionUID = 2376744703700063097L;
private CodeMsg cm;
public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
}
}
修改SecKillUserService中的login:最简单的做法是,出现异常时候直接抛异常。
public SecKillUser getById(long id) {
return secKillUserDao.getById(id);
}
public boolean login(LoginVo loginVo) {
// public CodeMsg login(LoginVo loginVo) {
// TODO Auto-generated method stub
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
// return CodeMsg.SERVER_ERROR;
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
SecKillUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
// return CodeMsg.MOBILE_NOT_EXIST;
}
//验证密码
String dbPass = user.getPassword();
String slatDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, slatDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
// return CodeMsg.PASSWORD_ERROR;
}
// return CodeMsg.SUCCESS;
return true;
}
在抛出异常之后,我们需要在GlobalExceptionHandler中做一个处理,很简单,。只需要加一个分支就可以。
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}
else if(e instanceof BindException) {
//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);
}
}
修改LoginController中的doLogin方法
@PostMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo){
log.info(loginVo.toString());
//登陆
//CodeMsg cm = userService.login(loginVo);
userService.login(loginVo);
//如果出现异常,则直接抛出了,所有直接返回true就行
return Result.success(true);
// if(cm.getCode() == 0) {
// return Result.success(true);
// }else {
// return Result.error(cm);
// }
}
现在,代码清爽了很多,现在对这些异常进行测试:
输入不存在的手机号:
输入错误的密码:
OK!
4、分布式Session
这个非常重要,因为秒杀功能是基础,我们实际应用中肯定不会只用一台应用服务器,肯定是多台服务器分布式部署。这个时候就有一个问题,用户的session如何处理,如果我们只是使用应用服务器提供的原生的session,显然没办法满足我们的需求,假如说用户第一个请求落到了第一台服务器上,但是第二个请求没有落到第一个服务器上,而是落到第二台服务器上了,那么用户的session信息全部丢失,针对这种情况,容器现在有很多处理方式,比如有的容器提供了叫做session同步,原生的session同步,也就是说他会自带一台服务器上的session同步到另一台服务器上,这样,在一个集群里面,你访问哪一台服务器,session都可以取到,但是这种方式在实际中应用的并不多,为什么呢?很明显,性能有问题,再一个就是实现起来比较复杂。你可以想象一下,假如说应用服务器是有20台或者更多,相互之间做一个同步,很可怕,那么我们真正使用当中是如何做的?
我们在登陆成功后,service层上,一般来说,会给用户生成一个类似于sessionId的一个东西:token,来标识这个用户,然后写到cookie当中,传递给客户端,客户端在随后的访问当中,都在cookie中上传这个token,服务端拿到token之后,就根据这个token,来取到用户对应的session信息,道理很简单,就跟容器实现原生的session是一样的。容器实现session也是通过生成一个sessionId,然后写到cookie当中,原理很简单,但是如果想不明白,赶紧还是挺复杂的,今天我们就来实现一下。
4.1)第一步,登陆成功之后要生成一个cookie,这里我们使用UUID,所以先创建一个UUID工具类。
public class UUIDUtil {
public static String uuid() {
//原生的uuid带‘-’,我们希望去掉它
return UUID.randomUUID().toString().replace("-", "");
}
}
我们要把token写入cookie当中,然后传递给客户端,但是我们要标识一下,你这个token对应的是哪一个用户,所以说,我们需要把这个用户信息,写到redis当中。放到第三方,这是常用做法,也是最高效的。
4.2)第二步修改SecKillUserService,添加redisDao
- 生成token
- 将token添加到redis
- 新建cookie
- 设置cookie的有效期
- 设置cookie保存目录
- 将cookie写入response,返回到客户端
public boolean login(HttpServletResponse response, LoginVo loginVo) {
//public boolean login(LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
SecKillUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String slatDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, slatDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
String token = UUIDUtil.uuid();
redisService.set(SecKillUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
//设置Cookie的有效期,为了和session保持一致,就设置成MiaoshaUserKey的有效期
cookie.setMaxAge(SecKillUserKey.token.expireSeconds());
//设置保存路径,为网站的根目录
cookie.setPath("/");
//这样生成完之后,我们只需要写道response中即可
response.addCookie(cookie);
// return CodeMsg.SUCCESS;
return true;
}
4.3)第三步,修改LoginController里面的doLogin,添加response参数
@PostMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo){
log.info(loginVo.toString());
//登陆
userService.login(response, loginVo);
//如果出现异常,则直接抛出了,所有直接返回true就行
return Result.success(true);
}
4.4)登陆成功之后,需要跳转到商品列表页面,先写一个页面goods_list.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>商品列表</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'hello:'+${user.nickname}" ></p>
</body>
</html>
4.5)然后再新建一个GoodsController类
@Controller
@RequestMapping("/goods")
public class GoodsController {
@GetMapping("/to_list")
public String list() {
return "goods_list";
}
}
4.6)测试:
提示登陆成功之后,出现如下:
后台日志:EL1007E: Property or field 'nickname' cannot be found on null
提示没找到 nickname,因为list中 user对象为空,
@GetMapping("/to_list")
public String list() {
return "goods_list";
}
我们先随便给他new个对象
@GetMapping("/to_list")
public String list(Model model) {
model.addAttribute("user", new SecKillUser() );
return "goods_list";
}
我们再测试一下
跳转成功,由于user是新建的,并没有设置属性, 所以打印null
我们打个断点,再来看一下
response中存在我们设置的token,Max_Age也不为0,下面我们看看,跳转到商品列表页面时,请求头中是否携带token
OK,在跳转页面时,请求头中携带了我们之前设置好的token
4.7)既然他请求时可以带上cookie,那我们就能取到cookie
有时候为了兼容手机端,很多手机客户端,并不会把token放到cookie里面,传给我们的服务端,他们有时候直接放到参数里面来传,为了兼容这种情况,再从request中取一下cookie,在此,我们还要设置优先级,先在request中token,如果取不到,再去cookie中取token。有可能通过request传递,也有可能通过cookie传递,所以属性设置required=false
GoodsController中的list方法如下:
public String list(Model model,
@CookieValue(value=SecKillUserService.COOKIE_NAME_TOKEN, required=false)String cookieToken,
@RequestParam(value=SecKillUserService.COOKIE_NAME_TOKEN, required=false) String paramToken) {
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return "login";
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
SecKillUser user = userService.getByToken(token);
model.addAttribute("user", user);
return "goods_list";
}
SecKillUserService类中添加getByToken()方法:
public SecKillUser getByToken(String token) {
//public 的方法,一定要进行参数校验
if(StringUtils.isEmpty(token)) {
return null;
}
return redisService.get(SecKillUserKey.token, token, SecKillUser.class);
}
4.8)测试:
好的,我们获得了用户,也就是成功取到session,成功实现把一个token映射成一个用户,这个分布式session,session并没有存到容器里面来,而是存到我们一个单独的缓存中,用一个redis单独管理我们的session,这就是所谓的分布式session。
以上实现了:根据服务端,把一个token,写入到cookie当中,然后呢,客户端在随后的访问当中,携带这个cookie,服务端就通过这个cookie里面的token,找到token对应的用户,但是这里还有个小问题,我们容器当中的session是这样的,如果说你在10点钟访问了,假如说你的session的有效期是30分钟,那么你的有效期应该到10点半,但如果10点10分又访问了,你的有效期应该到10点40,有效期是最后一次访问+有效期时间,我们如何来实现这个功能呢???
其实也很简单,我们在通过token来获取用户信息时,先不着急返回,先延长一下有效期,再返回。所谓延长,就是重新把缓存中的值进行设置,然后重新生成一个cookie,写出去就OK。
4.9)下面,先修改SecKillUserService:将添加cookie的操作抽取成方法,方便复用
addCookie(response, user);
return true;
}
private void addCookie(HttpServletResponse response, SecKillUser user) {
String token = UUIDUtil.uuid();
redisService.set(SecKillUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
//设置Cookie的有效期,为了和session保持一致,就设置成MiaoshaUserKey的有效期
cookie.setMaxAge(SecKillUserKey.token.expireSeconds());
//设置保存路径,为网站的根目录
cookie.setPath("/");
//这样生成完之后,我们只需要写道response中即可
response.addCookie(cookie);
}
4.10)修改SecKillUserService类中的getByToken方法,延长有效期:
public SecKillUser getByToken(HttpServletResponse response, String token){
//public SecKillUser getByToken(String token) {
//public 的方法,一定要进行参数校验
if(StringUtils.isEmpty(token)) {
return null;
}
//return redisService.get(SecKillUserKey.token, token, SecKillUser.class);
SecKillUser user = redisService.get(SecKillUserKey.token, token, SecKillUser.class);
if(user != null) {
//延长有效期
addCookie(response, user);
}
return user;
}
4.11)到目前为止,分布式session的功能已经实现,但是代码写的不够优雅。
例如:GoodsController中除了商品列表还有商品详情页,商品详情页,我们仍然需要判断用户是不是已经登陆,所以说,为了获取用户信息,我们想啊,我们最终要实现什么功能?
假如说,我们在方法参数上,直接把user注入进来:
@GetMapping("/to_list")
public String list(HttpServletResponse response, Model model,
// @CookieValue(value=SecKillUserService.COOKIE_NAME_TOKEN, required=false)String cookieToken,
// @RequestParam(value=SecKillUserService.COOKIE_NAME_TOKEN, required=false) String paramToken,
SecKillUser user) {
// if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
// return "login";
// }
// String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
// SecKillUser user = userService.getByToken(response, token);
model.addAttribute("user", user);
return "goods_list";
}
是不是会非常方便,如何实现这个功能呢?
很简单,第一步,我们是需要相当于实现一个叫ArgumentResolver
先新建一个WebConfig类
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
/*
* springmvc的controller中可以带很多参数,比如request,response,model。。。
* 那这些都参数是怎么来的呢?值是谁给他赋的呢,就是addArgumentResolvers来赋值的,
* 框架回调这个方法,往我们的controller方法中的参数进行赋值。
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#addArgumentResolvers(java.util.List)
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
// TODO Auto-generated method stub
super.addArgumentResolvers(argumentResolvers);
argumentResolvers.add(//这里暂且空着,后面会补上);
}
}
这里是要添加一个ArgumentResolvers,于是我们为
@GetMapping("/to_list")
public String list(HttpServletResponse response, Model model,SecKillUser user) {...}
中的user对象新建一个类,用来解析user对象
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private SecKillUserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
//获取参数类型
Class<?> clazz = parameter.getParameterType();
//SecKillUser,则返回true,如果不是返回false
return clazz == SecKillUser.class;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//获取http的request/response
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
//获取token
String paramToken = request.getParameter(SecKillUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, SecKillUserService.COOKIE_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookieName) {
//获得所有cookie
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
//遍历,如果和我们需要的cookie同名,则取值
if(cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
return null;
}
}
然后把我们写好的UserArgumentResolver注册到我们的WebConfig中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
super.addArgumentResolvers(argumentResolvers);
argumentResolvers.add(userArgumentResolver);
}
}
而此时GoodsController中的list方法变为
@GetMapping("/to_list")
public String list(Model model, SecKillUser user) {
model.addAttribute("user", user);
return "goods_list";
}
十分清爽!
下面进行测试:
成功!!!
这样的话,就算我们获取session的方式改变了,只需要在UserArgumentResolver类下的resolveArgument方法下进行修改就可以了,其他的业务代码,不需要进行任何调整。
4.12)还有个小问题需要修改:
我们写了一个延长时间的函数,在SecKillUserService类中
private void addCookie(HttpServletResponse response, SecKillUser user) {
String token = UUIDUtil.uuid();
redisService.set(SecKillUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
//设置Cookie的有效期,为了和session保持一致,就设置成MiaoshaUserKey的有效期
cookie.setMaxAge(SecKillUserKey.token.expireSeconds());
//设置保存路径,为网站的根目录
cookie.setPath("/");
//这样生成完之后,我们只需要写道response中即可
response.addCookie(cookie);
}
这里每次都会生成一个新的UUID,其实没有必要,我们只要第一次生成,以后更新它就是
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
//这里不用每次都生成新的token,只要第一次生成,后面更新就行
//String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);
//设置Cookie的有效期,为了和session保持一致,就设置成MiaoshaUserKey的有效期
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
//设置保存路径,为网站的根目录
cookie.setPath("/");
//这样生成完之后,我们只需要写道response中即可
response.addCookie(cookie);
}
顺便把前面的代码也改一下:
String token = UUIDUtil.uuid();
addCookie(response, token, user);
// addCookie(response, user);
return true;
}
好了,这就基本完成了。