前言
前两篇记录都是配置相关。今天开始正式学习功能开发,很多碎碎念可能会影响阅读,但我是真想先记下来要不然容易忘。
明文密码两次MD5处理
首先导入依赖
commons-codec
commons-codec
org.apache.commons
commons-lang3
3.6
动机
为什么两次MD5?
第一次:客户端:Pass1 =MD5(明文+固定Salt)
因为HTTP是明文传输,客户端加密是防止恶意用户获取用户密码。
第二次:服务端:Pass2 =MD5(Pass1+随机Salt)
如果数据库中保存的是 md5(password),一旦数据库泄露,黑客可以使用彩虹表(预先计算的大量密码-hash对)进行反推。使用 salt(随机字符串)混入加密,使得每个密码哈希值都唯一。
什么是Salt?为什么第一次salt写死,第二次是动态的呢。
什么是Salt?Salt可以理解为干扰项,也就是说我们加密的是(密码+干扰项)。举个例子你的pass=123456,Salt=abc。你可以Salt(0)+pass+Salt(1,2)。Salt可以是任意位置。为什么要加就是防止前文提到的彩虹表攻击
为什么第一次salt写死?因为如果客户端每次都是随机Salt,每次加密后提交的密文不一样,服务端这边无法判别。
为什么第二次是动态的?因为第二次在服务端这边做的,它会将每个用户对应的随机Salt存储到数据库中,每次用户pass1过来的时候,都会取出用户对应的随机Salt。这里的动态,是指每个用户对应的不一样。
简单实现以下工具类
实现
package com.imooc.miaosha.util;
import org.apache.commons.codec.digest.DigestUtils;
public class MD5Util {
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
private static final String salt = "1a2b3c4d";
//(明文 +salt1) to 密文1
public static String inputPassToFormPass(String inputPass) {
String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
System.out.println(str);
return md5(str);
}
//(密文1+salt2) to 密文2
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 inputPass, String saltDB) {
String formPass = inputPassToFormPass(inputPass);
String dbPass = formPassToDBPass(formPass, saltDB);
return dbPass;
}
最后实现加密时候我们只需要调用此工具包
JSR303参数校验+全局异常处理器
得到用户名和密码后,我们需要参数校验了。常规的参数校验方式就是验证用户名是否存在,然后再验证密码是否匹配。在本案例中,我们数据库存储的是加密后的密文,所以我们需要先验证用户提交的用户名是否存在,存在的话则取出用户名对应的salt,然后对用户提交的密码进行第二次MD5加密,最后匹配加密后的密文与数据库的密文是否一样。
这里先给出前端的提交时的核心代码(其余的感兴趣私聊我)
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();
}
});
}
我们前端提交的data 有两部分 一个是mobile 一个是密码 也就是我们的用户名和密码。所以我们需要新建一个对象来接受,对象内部的变量名称要与前端提交的一致,这些都是SpringMVC帮我们自动绑定好。还有比如DTO是对应数据库的表,也就是外部的数据源我们都需要定义对应的实体类来接受。
JSR303 参数校验如何使用?
我们只需要在对应的实体类的对应字段添加注解,它就能校验该字段,其中有一些校验功能是帮我们实现好的比如非空,字段长度等如表所示:
注解名 | 说明 |
---|---|
@NotNull |
不能为 null |
@NotEmpty |
字符串或集合不能为空 |
@NotBlank |
字符串不能为空,去掉空格后也不能是空 |
@Size(min, max) |
字符串、集合长度限制 |
@Email |
邮箱格式校验 |
@Pattern(regexp) |
正则表达式校验 |
@Min / @Max |
最小值 / 最大值 |
@Future / @Past |
日期是否是未来 / 过去 |
但有些字段可能没有帮我们实现好,我们需要自己去实现,但我们也不必担心冗余代码。因为我们实现好了后可以封装到对应的注解。比如我们想实现一下是否是手机号。首先创建一个isMobile接口。
package com.imooc.miaosha.validator;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@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 { };
}
前面那些格式不用记住,用的时候去查一下就可以了。
然后我们在创建一个类实现这个接口
package com.imooc.miaosha.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
import com.imooc.miaosha.util.ValidatorUtil;
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);
}
}
}
}
随后在创建一个工具类
package com.imooc.miaosha.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
public class ValidatorUtil {
//正则表达式匹配,是否为1开头后边跟的10位数字
private static final Pattern mobile_pattern = Pattern.compile(“1\d{10}”);
//判断是否为空
public static boolean isMobile(String src) {
if(StringUtils.isEmpty(src)) {
return false;
}
//是否匹配
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
}
随后我们创建好isMobile注解后就可以直接在实体类中使用了。
异常拦截器
先说一下异常的基本机制
当我们通过throw 抛异常时,我们会跳出本方法栈到上一个方法去处理,如果上一个方法也不能处理我们就会一直跳,一直不处理就会终止运行。例如代码
public void method1() {
method2(); // <-- method2 抛出异常,这里接收不到的话就向上传
System.out.println(“继续执行”); // 不会执行!
}
public void method2() {
throw new RuntimeException(“异常”);
}
当我们通过 try catch 抛异常,我们会自己处理异常,不用跳出方法栈。
try {
validate(16); // 调用上面的方法,抛异常
System.out.println(“验证通过”);
} catch (IllegalArgumentException e) {
System.out.println(“发生异常:” + e.getMessage());
}
而我们的异常拦截器就是用来代替try catch的,因为很多方法都要抛异常,要写很多遍trycatch,所以我们对异常进行拦截,只写一遍代码即可。防止冗余代码,容易维护。
如何实现?
创建一个全局异常拦截器,具体注释在代码中
package com.imooc.miaosha.exception;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.imooc.miaosha.result.CodeMsg;
import com.imooc.miaosha.result.Result;
//这个注解是讲controller层抛出的异常全都拦截,上文中提到service dao等等底层抛出的异常都会跳转到上层的方法中去
@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);
}
}
}
实现
新建一个包名为VO,在此包下创建一个LoginVO 用来接受表单提交的参数
package com.imooc.miaosha.vo;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import com.imooc.miaosha.validator.IsMobile;
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 + "]";
}
}
在controller包下新建LoginController.java,处理控制层逻辑返回那些页面等
package com.imooc.miaosha.controller;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.imooc.miaosha.redis.RedisService;
import com.imooc.miaosha.result.Result;
import com.imooc.miaosha.service.MiaoshaUserService;
import com.imooc.miaosha.vo.LoginVo;
@Controller
@RequestMapping("/login")
public class LoginController {
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
userService.login(response, loginVo);
return Result.success(true);
}
}
在service包下新建MiaoshaUserService,登录方法的核心逻辑
package com.imooc.miaosha.service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.imooc.miaosha.dao.MiaoshaUserDao;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.exception.GlobalException;
import com.imooc.miaosha.redis.MiaoshaUserKey;
import com.imooc.miaosha.redis.RedisService;
import com.imooc.miaosha.result.CodeMsg;
import com.imooc.miaosha.util.MD5Util;
import com.imooc.miaosha.util.UUIDUtil;
import com.imooc.miaosha.vo.LoginVo;
@Service
public class MiaoshaUserService {
public static final String COOKI_NAME_TOKEN = "token";
@Autowired
MiaoshaUserDao miaoshaUserDao;
@Autowired
RedisService redisService;
public MiaoshaUser getById(long id) {
return miaoshaUserDao.getById(id);
}
public boolean 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) {
//抛出异常,最终在controller层被拦截到
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
//取出salt
String saltDB = user.getSalt();
//将salt与表单提交的密码按一定的规则加密然后比较数据库中的密码
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
return true;
}
}
分布式session
讲之前,记录一下一个问题。到底什么时候注册成为bean什么时候不用注册成为bean?
这个问题困扰我很久。但我仔细考虑了一下,不知道对不对。从IOC容器本质讲起得,我们讲其中的对象注册成为bean与不注册成为bean的区别到底是什么?其实就是注册成为bean后,spring 帮我们自动new 了,而没注册的你得手动new。手动new有什么坏处?就是你得手动初始化,而且参数变了会影响一整条链的参数你都得每个都需要改。所以spring 的控制反转最大的作用就是这个,我们讲bean的控制权交给容器管理。回到题目什么时候一个对象需要注册成为bean?直接给出我的答案是这样的 对象中成员变量方法不需要依赖我们自定义的对象就不用注册成为bean。比如DTO,VO,工具类这些。既然前文提到注册了我们就不需要new了,那我们这些方法用的话不还是要new嘛。是的,我们要用确实要new,但一般来说DTO,VO这些对象都是数据库或者前端传过来的不需要new,而工具类内部的方法一般定义为static。这样大多数也不用new了。
session是什么?动机?
Session(会话)是服务端保存用户状态的一种机制,用来在多次 HTTP 请求之间**“记住”用户是谁**。
HTTP 是无状态协议,每次请求都是独立的,服务器不知道你上一次是谁。当用户登录后,如果不通过某种方式“记住”,每次请求都要重新登录。分布式session是指,当服务端有多台服务器时,如果session只保存在一个服务器上,那么下一次登录如果是别的服务器处理请求则也会获取不到session从而重新登录。实现原理:登陆时,为该用户生成一个字符串取名叫token,我们将token作为键 用户信息作为值存储到redis中(此处的redis可以理解为多个服务器共享的内存)。然后将token添加到cookie中然后再加到response中去。 用户收到response时,浏览器会自动将内部的cookie写到下一次的request中。然后服务器收到了request在将内部的token取出判断。
实现
实现原理先放一下,我们先讲一下实现后最终的效果是我们只要点击商品页面啊或者订单页面等很多页面,都不需要重新登录都可以实现自动登录。因此我们是不是需要在每一个页面的控制方法前面加上判断token的逻辑呢。就如下图我们分别需要在列表页和详情页分别加上相同代码,这是不是就是代码冗余也不好看了?
冗余代码封装
既然代码冗余,我们首先要去除冗余的代码。那么我们保留什么呢?
我们只需要保存这个。我们分析一下代码,发现只能在方法里传入用户对象,才能往表单内注入用户。实际上方法中很多参数SpringMVC会自动拦截绑定,像假如你在方法定义了Model,Request,Response等,SpringMVC会自动拦截这些方法,将前端/后端提交的参数传入对应的参数。但我们参数定义的是我们自定义的对象,如何传?这些SpringMVC没有给我们自动拦截吧?是的,我们不能总是靠他,我们需要自定义一个拦截方法(自力更生)用来拦截那些方法形参中有MiaoshaUser的方法。这里说一下自动绑定的方法
类型 | 示例 | 来源 / 说明 |
---|---|---|
HttpServletRequest |
request |
当前请求对象 |
HttpServletResponse |
response |
当前响应对象 |
HttpSession |
session |
当前会话 |
Model / ModelMap / ModelAndView |
model |
用于向前端传值 |
BindingResult |
bindingResult |
表单校验结果 |
Principal / Authentication |
登录用户信息(Spring Security) | |
@PathVariable , @RequestParam , @RequestHeader , @RequestBody 等注解参数 |
请求数据、表单参数、Header、Body 等 | |
这些对象的注入,依赖于 Spring MVC 的数据绑定机制(DataBinder): | ||
会从请求参数(Query、Form、JSON Body 等)中,根据字段名自动匹配赋值; | ||
支持嵌套对象、集合类型、JSR-303 校验等 |
类别 | 自动注入来源 | 是否要配置 |
---|---|---|
Model 、HttpServletRequest 等 |
Spring 框架内建支持 | ❌ 不需要 |
VO/DTO(LoginVo 等) | Spring 自动绑定请求参数 | ❌ 不需要 |
登录用户对象(MiaoshaUser) | 你自定义的解析器注册进去 | ✅ 需要 |
返回值(Result、ResponseVO) | 由返回值处理器处理成 JSON | ❌ 不需要(需 @ResponseBody ) |
实现
先捋一遍,首先我们在用户登录成功后,1.我们需要为用户设定一个token放在cookie里面,并响应给用户。 2.用户访问其他页面时比如to_list,我们要定义to_list对应方法。3.服务器执行此方法时,,我们对应的自定义参数拦截器要对其拦截并返回miaoshaUser。4。拦截器实现:我们最终要返回一个对象,首先要把对象查出来,靠什么查?靠token,token在哪?在request中。也就是 先从request中取出token,然后简单校验,校验之后讲token对应的对象取出来返回即可。
首先取出冗余代码,最后的代码是这样
@RequestMapping("/to_list")
public String list(Model model,MiaoshaUser user) {
model.addAttribute("user", user);
return "goods_list";
}
我们在service密码校验成功后,我们需要为其设定token。并将其加入到response中,
package com.imooc.miaosha.service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.imooc.miaosha.dao.MiaoshaUserDao;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.exception.GlobalException;
import com.imooc.miaosha.redis.MiaoshaUserKey;
import com.imooc.miaosha.redis.RedisService;
import com.imooc.miaosha.result.CodeMsg;
import com.imooc.miaosha.util.MD5Util;
import com.imooc.miaosha.util.UUIDUtil;
import com.imooc.miaosha.vo.LoginVo;
@Service
public class MiaoshaUserService {
public static final String COOKI_NAME_TOKEN = "token";
@Autowired
MiaoshaUserDao miaoshaUserDao;
@Autowired
RedisService redisService;
public MiaoshaUser getById(long id) {
return miaoshaUserDao.getById(id);
}
public MiaoshaUser getByToken(HttpServletResponse response, String token) {
if(StringUtils.isEmpty(token)) {
return null;
}
//取出token对应的user
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
//延长有效期
if(user != null) {
addCookie(response, token, user);
}
return user;
}
public boolean 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.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return true;
}
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
//存储token对应的user
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
//这个 Cookie 应该在网站的哪个路径(路径前缀)下被发送回服务端,/就是所有路径都会发回服务端。
cookie.setPath("/");
response.addCookie(cookie);
}
}
新建一个config包,包下创建UserArgumentResolver和 webconfig类。这两个类的目标就是实现自定义的拦截器
package com.imooc.miaosha.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
//为什么要写@Configuration 而不写@Component。因为我们要加的方法是控制器参数扫描可以理解为是自动的
//必须先配置好注册到addArgumentResolvers,才能自动实现。常用于拦截器,参数解析器,第三方bean注册。
// 如果写了@Component,它是普通的bean,无法自动解析
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
//addArgumentResolvers 是增加一个控制器参数,添加后就可以自动拦截我们对应的方法。
//什么是控制器参数?举例子来说我们实现方法时传入参数(response,model) 这些都是spring MVC自动注入的
//所以我们传参数(User)时,我们需要增加一个User参数,告诉Spring MVC 遇到带有这种参数类型的方法时 我们能处理
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
//控制器参数方法实现。
package com.imooc.miaosha.config;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.imooc.miaosha.domain.MiaoshaUser;
import com.imooc.miaosha.service.MiaoshaUserService;
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
//这里首先会调用这个方法判断类型是否与我们拦截的类型一致,一致则执行resolveArgument
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz==MiaoshaUser.class;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
// 取token,这里定义两种方式一种是paramToken这个是为了兼容手机端做的cookieToken 这个是正常电脑端做的
//从request取出来
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
//取出来后判空
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
//每次获取token我们都需要更新token最晚过期时间,比如第一次登录过期时间是24h,
//第二次登录需要在重新设定,也就是说你不断的登录他就不会掉,所以需要把response传过去重新添加一遍
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}