认证功能
发送短信验证码
发送短信验证码以前做过,使用腾讯云。在第三方服务项目中提供远程调用接口。
接口
@RestController
@RequestMapping("/sms")
public class MsgController {
@Autowired
private MsgService msgService;
@GetMapping("/sendMsg")
public R sendMsg(@RequestParam("phone") String phone, @RequestParam("code") String code) {
boolean b = msgService.sendMessage(phone, code);
return b ? R.ok() : R.error();
}
}
实现类
@Service
public class MsgServiceImpl implements MsgService {
@Override
public boolean sendMessage(String phone, String code) {
try {
// 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
// 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
Credential cred = new Credential(ConstantMsgUtil.SECRET_ID, ConstantMsgUtil.SECRET_KEY);
// 实例化一个http选项,可选的,没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(ConstantMsgUtil.END_POINT);
// 实例化一个client选项,可选的,没有特殊需求可以跳过
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
// 实例化要请求产品的client对象,clientProfile是可选的
SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
// 实例化一个请求对象,每个接口都会对应一个request对象
SendSmsRequest req = new SendSmsRequest();
// 国内短信需要加上86
String[] phoneNumberSet1 = {"86" + phone};
req.setPhoneNumberSet(phoneNumberSet1);
req.setSmsSdkAppId(ConstantMsgUtil.APP_ID);
req.setSignName(ConstantMsgUtil.SIGN_NAME);
req.setTemplateId(ConstantMsgUtil.TEMPLATE_ID);
String[] templateParamSet1 = {code};
req.setTemplateParamSet(templateParamSet1);
// 返回的resp是一个SendSmsResponse的实例,与请求对象对应
SendSmsResponse resp = client.SendSms(req);
// 输出json格式的字符串回包
System.out.println(SendSmsResponse.toJsonString(resp));
return true;
} catch (TencentCloudSDKException e) {
System.out.println(e.toString());
return false;
}
}
}
其他信息参照谷粒学院项目。
发送短信验证码接口
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {
// TODO 接口防刷
// 60s防止用户再次发送请求
String redisCode = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)) {
long l = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - l < 60000) { // 一分钟之内就不发送
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
}
}
// 根据时间生成验证码,存入redis需要带上时间,发短信不需要
// String code = UUID.randomUUID().toString().substring(0, 4);
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
String codeToRedis = code + "_" + System.currentTimeMillis();
// 存入redis
redisTemplate.opsForValue().set(AuthConstant.SMS_CODE_CACHE_PREFIX + phone, codeToRedis, 10, TimeUnit.MINUTES);
thirdPartyFeignService.sendMsg(phone, String.valueOf(code));
return R.ok();
}
注册
基本流程
参数校验
/**
* 注册使用的vo,使用JSR303校验
*/
@Data
public class UserRegisterVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6, max = 19, message="用户名长度必须是6-18字符")
private String userName;
@NotEmpty(message = "密码必须填写")
@Length(min = 6,max = 18,message = "密码长度必须是6—18位字符")
private String password;
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
异常结果封装+重定向(防刷+分布式session)
@Controller
public class RegisterController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private MemberServiceClient memberServiceClient;
/**
* TODO 重定向携带数据,利用session原理。将数据放在session中。
* 下一个页面使用session数据后,session就会失效
* RedirectAttributes redirectAttributes:模拟重定向携带数据
* TODO 分布式下的session问题
*
* @param userRegisterVo
* @param bindingResult
* @param redirectAttributes
* @return
*/
@PostMapping("/register")
public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,
// 重定向报错数据信息
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors", errors);
// 校验出错,重定向
return "redirect:http://auth.gulimalls.com/reg.html";
}
...
}
}
验证码校验
String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
if (!StringUtils.isEmpty(code)) {
if (userRegisterVo.getCode().equals(code.split("_")[0])) {
// 删除验证码
redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
// 远程调用注册服务
UserRegisterTo userRegisterTo = new UserRegisterTo();
BeanUtils.copyProperties(userRegisterVo, userRegisterTo);
R r = memberServiceClient.register(userRegisterTo);
if (r.getCode() == 0) {
// 注册成功,回到登录页
return "redirect:http://auth.gulimalls.com/login.html";
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("msg", r.getData(new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
认证模块_注册接口
@Controller
public class RegisterController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private MemberServiceClient memberServiceClient;
/**
* TODO 重定向携带数据,利用session原理。将数据放在session中。
* 下一个页面使用session数据后,session就会失效
* RedirectAttributes redirectAttributes:模拟重定向携带数据
* TODO 分布式下的session问题
*
* @param userRegisterVo
* @param bindingResult
* @param redirectAttributes
* @return
*/
@PostMapping("/register")
public String register(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult,
// 重定向报错数据信息
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors", errors);
// 校验出错,重定向
return "redirect:http://auth.gulimalls.com/reg.html";
}
String code = redisTemplate.opsForValue().get(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
if (!StringUtils.isEmpty(code)) {
if (userRegisterVo.getCode().equals(code.split("_")[0])) {
// 删除验证码
redisTemplate.delete(AuthConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone());
// 远程调用注册服务
UserRegisterTo userRegisterTo = new UserRegisterTo();
BeanUtils.copyProperties(userRegisterVo, userRegisterTo);
R r = memberServiceClient.register(userRegisterTo);
if (r.getCode() == 0) {
// 注册成功,回到登录页
return "redirect:http://auth.gulimalls.com/login.html";
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("msg", r.getData(new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
} else {
HashMap<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/reg.html";
}
}
}
远程调用注册
在member工程中与数据库交互,进行用户注册。
controller
@PostMapping("/register")
public R register(@RequestBody UserRegisterTo userRegisterTo) {
try {
memberService.register(userRegisterTo);
} catch (PhoneExistException exception) {
R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
} catch (UsernameExistException exception) {
R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
MD5加密
1.可逆加密与不可逆加密 可逆加密:通过密文根据算法可以推算出明文 不可逆加密:无法推算出明文 2.彩虹表 暴力破解所有值的MD5值存储到数据库,然后存储一个映射表,该映射表称为彩虹表 3.不可逆加密实现方法:MD5、MD5盐值加密 ======================================================================== MD5:信息摘要算法,只要一个字节发生变化,结果值就会变化 - Message Digest algorithm 5,信息摘要算法 ·压缩性:任意长度的数据,算出的MD5值长度都是固定的。 ·容易计算:从原数据计算出MD5值很容易。 ·抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。 ·强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。 ·不可逆 【百度网盘秒传功能:计算文件MD5值,如果在百度的服务器里能找到一个一模一样的,就可以使用这个】 ======================================================================== MD5盐值加密:【明文相同,盐值不同密文也不同,增加了彩虹表的难度】 ·通过生成随机数与MD5生成字符串进行组合 ·数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可 ========================================================================
案例
========================================================================
MD5案例:
String s = DigestUtils.md5Hex("123456");// e10adc3949ba59abbe56e057f20f883e
System.out.println(s);
========================================================================
MD5盐值案例:
System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:USI.JoH2】【MD5值:$1$USI.JoH2$6hK88QXt9ijipsa/VcnbR0】
System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));// 随机盐值,随机MD5值:【盐值:tCYQRfTB】【MD5值:$1$tCYQRfTB$thopJ/8DcRSObDwXuKxvn1】
System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】
System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$123"));// 固定盐值,固定MD5值:【盐值:123】【MD5值:$1$123$7mft0jKnzzvAdU4t0unTG1】
========================================================================
使用spring的MD5+随机盐方法生成密文:
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword1 = passwordEncoder.encode("123456");//$2a$10$s0yQ/Tz1aiexGqQGBNgmDuUFpCPjMx8L7TvJ60i9mQSBEmNXbSFEO
String encodedPassword2 = passwordEncoder.encode("123456");//$2a$10$eXhMUTIjoS4cpCB3FRjhlu0QYGwTRgh93CefQSk48hPpvQzzDAvIS
System.out.println(passwordEncoder.matches("123456", encodedPassword1));// 校验结果true
System.out.println(passwordEncoder.matches("123456", encodedPassword2));// 校验结果true
========================================================================
由于可以被暴力破解,所以采用盐值加密。
盐值加密
@Override
public void register(UserRegisterTo userRegisterTo) {
MemberEntity memberEntity = new MemberEntity();
// 设置默认等级
MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(memberLevelEntity.getId());
// 检查手机号和用户名是否唯一
this.existPhone(userRegisterTo.getPhone());
this.existUsername(userRegisterTo.getUserName());
memberEntity.setMobile(userRegisterTo.getPhone());
memberEntity.setUsername(userRegisterTo.getUserName());
memberEntity.setNickname(userRegisterTo.getUserName());
// 存储密码,使用盐值加密(BCryptPasswordEncoder提供的类)
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(userRegisterTo.getPassword());
memberEntity.setPassword(encode);
// TODO 其他信息
baseMapper.insert(memberEntity);
}
普通登录
接口
@PostMapping("login")
public String login(UserLoginVo userLoginVo, RedirectAttributes redirectAttributes, HttpSession session) {
// 远程调用
UserLoginTo userLoginTo = new UserLoginTo();
BeanUtils.copyProperties(userLoginVo, userLoginTo);
R r = memberServiceClient.login(userLoginTo);
if (r.getCode() == 0) {
// 登录成功
MemberRespVo memberRespVo = r.getData("data", new TypeReference<MemberRespVo>() {
});
session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);
return "redirect:http://gulimalls.com";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData(new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimalls.com/login.html";
}
}
远程调用member模块登录
controller
@PostMapping("/login")
public R login(@RequestBody UserLoginTo userLoginTo) {
MemberEntity memberEntity = memberService.login(userLoginTo);
if (memberEntity == null) {
R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
}
return R.ok().setData(memberEntity);
}
接口实现类
@Override
public MemberEntity login(UserLoginTo userLoginTo) {
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", userLoginTo.getLoginacct()).eq("phone", userLoginTo.getLoginacct()));
if (memberEntity != null) {
String password = userLoginTo.getPassword();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(password);
if (encode.matches(memberEntity.getPassword())) {
return memberEntity;
} else {
return null;
}
} else {
// 登录失败
return null;
}
}
社交登录
介绍
微博开放平台注册应用
开发平台首页:🔗
文档:🔗
接口:🔗
注册应用填写回调地址
流程
回调接口
/**
* 授权回调页
*
* @param code 根据code换取Access Token,且code只能兑换一次Access Token
*/
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 1.根据code换取Access Token
Map<String, String> headers = new HashMap<>();
Map<String, String> querys = new HashMap<>();
Map<String, String> map = new HashMap<>();
map.put("client_id", "2516299543");
map.put("client_secret", "58124c5db70121821d778d446af28096");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");
map.put("code", code);
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);
// 判断是否授权成功
if (response.getStatusLine().getStatusCode() == 200) {
// 获取到了 accesstoken(此时使用一个对象封装)
String json = EntityUtils.toString(response.getEntity());
WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);
// 远程调用登录功能
R r = memberServiceClient.loginWB(wbSocialUserTo);
if (r.getCode() == 0) {
MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {
});
log.info(memberRespVo.toString());
session.setAttribute("loginUser", memberRespVo);
// 跳回首页
return "redirect:http://gulimalls.com";
} else {
// 登录失败,调回登录页
return "redirect:http://auth.gulimalls.com/login.html";
}
} else {
return "redirect:http://auth.gulimalls.com/login.html";
}
}
远程调用member模块处理登录
controller
@PostMapping("/oauth2/login")
public R loginWB(@RequestBody WBSocialUserTo wbSocialUserTo) {
MemberEntity memberEntity = memberService.login(wbSocialUserTo);
if (memberEntity == null) {
R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
}
return R.ok().setData(memberEntity);
}
实现类
@Override
public MemberEntity login(WBSocialUserTo wbSocialUserTo) {
String uid = wbSocialUserTo.getUid();
// 数据库中是否有用户
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
// 已注册
MemberEntity entity = new MemberEntity();
entity.setId(memberEntity.getId());
// 更新token
entity.setAccessToken(wbSocialUserTo.getAccess_token());
entity.setExpiresIn(wbSocialUserTo.getExpires_in());
baseMapper.updateById(memberEntity);
memberEntity.setAccessToken(wbSocialUserTo.getAccess_token());
memberEntity.setExpiresIn(wbSocialUserTo.getExpires_in());
return memberEntity;
} else {
// 进行注册
MemberEntity registerMember = new MemberEntity();
try {
Map<String, String> query = new HashMap<>();
query.put("access_token", wbSocialUserTo.getAccess_token());
query.put("uid", wbSocialUserTo.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", new HashMap<String, String>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
String profileImageUrl = jsonObject.getString("profile_image_url");
// 封装注册信息
registerMember.setNickname(name);
registerMember.setGender("m".equals(gender) ? 1 : 0);
registerMember.setHeader(profileImageUrl);
registerMember.setCreateTime(new Date());
}
} catch (Exception exception) {
}
// 不管是否出现异常,这三个字段都要保存
registerMember.setSocialUid(wbSocialUserTo.getUid());
registerMember.setAccessToken(wbSocialUserTo.getAccess_token());
registerMember.setExpiresIn(wbSocialUserTo.getExpires_in());
baseMapper.insert(registerMember);
return registerMember;
}
}
Gitee登录
和weibo相差不大,略。
分布式Session共享问题
问题一:不能跨域名共享cookie
跨域情况下,cookie不共享
放大cookie的作用域
1.方法1:自己设置domain
// 首次使用session时,spring会自动颁发cookie设置domain,所以这里手动设置cookie很麻烦,采用springsession的方式颁发父级域名的domain权限
// Cookie cookie = new Cookie("JSESSIONID", loginUser.getId().toString());
// cookie.setDomain("gulimall.com");
// servletResponse.addCookie(cookie);
2.使用springsession设置domain放大作用域
问题二:集群下同一个服务不能跨JVM共享session
解决方式
session复制解决
客户端存储
hash一致性
问题三:分布式下不同服务共享session
统一存储
本项目使用springSession提供的session存储在redis中,解决session共享问题。
方案二:token令牌
使用redis共享存储 + springsecurity存token令牌,每个调用接口都带令牌访问
流程
查看文档
查看文档:
https://docs.spring.io/spring-session/docs/2.2.1.RELEASE/reference/
https://spring.io
=》project
=》springsession
=》learn
=》Reference Doc.
=》3. Samples and Guides (Start Here)
=》HttpSession with Redis Guide (Source 源码) + HttpSession with Redis Guide (Guide 引导)
简介:【各模块都需要使用springsession】
- 解决了子域cookie无法共享的问题,放大了cookie的作用域domain
- 解决了跨JVM不能共享session的问题【问题2和问题3本质上是同一个问题,不同JVM不能共享session】,springsession采用redis统一存储的的方式解决了session共享问题
导入依赖
<!--springsession解决session共享问题-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置使用redis作为session存储
spring:
redis:
host: 192.168.241.130
port: 6379
session:
store-type: redis # 使用redis保存session
timeout: 30m # 30分钟过期
此时授权回调页将信息保存在session中
/**
* 授权回调页
*
* @param code 根据code换取Access Token,且code只能兑换一次Access Token
*/
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 1.根据code换取Access Token
Map<String, String> headers = new HashMap<>();
Map<String, String> querys = new HashMap<>();
Map<String, String> map = new HashMap<>();
map.put("client_id", "2516299543");
map.put("client_secret", "58124c5db70121821d778d446af28096");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimalls.com/oauth2.0/weibo/success");
map.put("code", code);
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, querys, map);
// 判断是否授权成功
if (response.getStatusLine().getStatusCode() == 200) {
// 获取到了 accesstoken(此时使用一个对象封装)
String json = EntityUtils.toString(response.getEntity());
WBSocialUserTo wbSocialUserTo = JSON.parseObject(json, WBSocialUserTo.class);
// 远程调用登录功能
R r = memberServiceClient.loginWB(wbSocialUserTo);
if (r.getCode() == 0) {
MemberRespVo memberRespVo = r.getData(new TypeReference<MemberRespVo>() {
});
session.setAttribute(AuthConstant.LOGIN_USER, memberRespVo);
// 跳回首页
return "redirect:http://gulimalls.com";
} else {
// 登录失败,调回登录页
return "redirect:http://auth.gulimalls.com/login.html";
}
} else {
return "redirect:http://auth.gulimalls.com/login.html";
}
}
注意此时session只是在一个服务的域名共享auth.gulimalls.com,所以此时要扩大范围,所以需要配置,同时也可以配置序列化存储的格式,配置为Json
@Configuration
public class GulimallSessionConfig {
// 配置Session域名作用范围
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setDomainName("gulimalls.com");
defaultCookieSerializer.setCookieName("GULISESSION");
return defaultCookieSerializer;
}
// 使用fastjson的反序列化,存储json格式在redis中
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
MemberResponseVO实现序列化接口
原理:内存中的对象要序列化成一个二进制流 传输到 redis中存储
public class MemberResponseVO implements Serializable
修改product模块gulimall首页,去除session中的loginUser
<li>
<a th:if="${session.loginUser != null}">欢迎, [[${session.loginUser.nickname}]]</a>
<a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/login.html">你好,请登录</a>
</li>
其他模块使用springSession也需要导包,配置。可以直接放在common中,但此项目还是分开。
SpringSession原理
过滤器使用了装饰者模式,封装原生的request和response