一。Auth服务
1.1LoginController(&验证码&注册&账密登录)
@Controller
public class LoginController {
@Autowired
ThirdPartFeignService thirdPartFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
MemberFeignService memberFeignService;
/**
* 发送验证码
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
/**
* 1.接口防刷,防止同一个phone在60秒内重复再次发送验证码
* 2.验证码的再次校验,存redis,因为这个验证码不是永远存储的
* 2.1.存redis,key是一定包含手机号的,值是验证码
* 2.2.如果是60秒以后,那么再发新的验证码
*
* 验证码发送频率过多的解决方案:
* 1.在保存验证码的时候,再保存给当前验证码设置的系统时间,
* 1.1.只要你想要发送验证码,按照redis里面的看有没有,
* 1.2.如果有了,再看一下这个时间,如果这个时间还在60秒以内的,那就60以后再试
*/
//TODO 1、接口防刷。
String rediscode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(rediscode)) { //如果从redis当中拿到的验证码不为空
long l = Long.parseLong(rediscode.split("_")[1]);//拿到redis存的时间
//和当前系统的时间进行比较
if (System.currentTimeMillis() -l <60000){ //60秒内不能再发验证码
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
String code = UUID.randomUUID().toString().substring(0, 5);
String substring = code+"_"+System.currentTimeMillis(); //+时间为了接口防刷操作
// 用redis是因为不能保证第三方接口一定发送了短信
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
/**
* 注册的控制器
* RedirectAttributes 模拟重定向携带数据
* 其原理:
* 利用session原理,将数据放在session中,然后重定向到页面之后,再从session中取出来
* 跳到下一个页面,然后取出数据以后,session里面的数据就会删掉,
* @Valid 数据校验
* BindingResult :校验结果
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes){
if (result.hasErrors()){ //如果校验出错
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage));
//存放错误消息,让前端感知并获取错误信息
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";//注册页
}
//1、校验验证码,从页面提交过来的验证码
String code = vo.getCode();
//获取到redis存的验证码
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)){ //redis里面存了这个手机号对应的验证码
//,验证码正确,就进行截串,拿到第一个数据然后进行对比 redis存的s还带了系统时间,so分割
if (code.equals(s.split("_")[0])){
//验证码通过以后,还必须要删除验证码,因为这样下次若还带着之前的验证码过来,就会验证失败
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证码通过, 如果上面校验并无出错,就进行注册 ,调用远程服务gulimall-member进行注册
R r = memberFeignService.regist(vo);
if (r.getCode() == 0){ //成功就返回登陆页
return "redirect:http://auth.gulimall.com/login.html";
}else { //失败就返回注册页,并且显示错误消息
Map<String, String> errors = new HashMap<>();
errors.put("msg",r.getData("msg ",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";//注册页
}
}else { //验证不通过
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
//存放错误消息,让前端感知并获取错误信息
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";//注册页
}
}else { //redis存的验证码已过期
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
//存放错误消息,让前端感知并获取错误信息
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";//注册页
}
}
/**
* 账号密码登录请求
* 提交的不是json数据,是K,V型数据,so不用加requestBody
*/
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){
//调用远程gulimall-member进行登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0){
//成功登录
//提取出用户信息,
MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
});
//然后将提取出来的用户信息放到session中
session.setAttribute(AuthServerConstant.LOGIN_USER,data);
return "redirect:http://gulimall.com";
}else {
Map<String,String> errors = new HashMap<>();
errors.put("msg",login.getData("msg",new TypeReference<String>(){}));//把错误消息放到map中
redirectAttributes.addFlashAttribute("errors",errors); //提取出错误消息
//失败就返回登录页
return "redirect:http://auth.gulimall.com/login.html";
}
}
/**
* 如果已经登录成功后,再点击登录页不用再登录,直接跳转到商城首页
*/
@GetMapping("/login.html")
public String loginPage(HttpSession session){
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);//拿到已登录的用户信息
if (attribute==null){ //没登录,返回登录页
return "login";
}else { //登录了就直接跳回首页就行了
return "redirect:http://gulimall.com";
}
}
}
Oauth2controller(微博登录)
@Controller
public class OAuth2Controller {
@Autowired
MemberFeignService memberFeignService;
/**
* 微博的成功登录后的回调方法
*/
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code ,HttpSession session) throws Exception {
Map<String,String> header = new HashMap<>();
Map<String,String> query = new HashMap<>();
//换取access Token给map里面封装数据
Map<String,String> map = new HashMap<>();//封装数据用的
map.put("","");//应用的id
map.put("","");
map.put("grant_type","authorization_code");
map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code",code);//社交登录一登录成功,code就会过来了,然后就去换取access Token
//1.根据code码,换取一个access Token,只要能够换取access Token,就说明登录成功
//方法参数解释:第一个参数:主机地址,,第二个参数:请求路径 第三个参数,请求方式 第四个参数 请求头 第五个参数 查询参数 第六个参数:请求体
//执行成功这个doPost方法,就会有响应的数据
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", header, query, map);
//解析响应的数据,处理access Token
if(response.getStatusLine().getStatusCode()==200){ //获取响应状态行以及响应状态码,并且是判断是否换取成功
//如果是200,那么就获取到了access Token
String json = EntityUtils.toString(response.getEntity());//获取到响应体内容
//将响应体的内容转换成对应的对象
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//拿到了access Token,并且还转换了响应的对象了,那么就是知道了当前是哪个社交用户登录成功
//真正是不是登录成功,还得分一些情况
// 1.如果当前用户如果是第一次进gulimall网站,就自动注册进来,(为当前这个社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
//使用社交账号对数据库的用户表ums_member进行关联
//如果这个社交用户,从来没有注册过gulimall网站,就进行注册,如果注册了就查出这个用户的整个详细信息
//远程调用用户服务,接收socialUser这个社交用户,以此来判断是登录还是自动注册这个社交用户,相当于用id关联上某个本系统的用户信息,
R oauthlogin = memberFeignService.oauthlogin(socialUser);
if(oauthlogin.getCode() == 0){ //说明是成功的
//提取登陆成功后的用户信息
MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户信息: {}",data.toString());
/**
* 第一次使用session,就命令浏览器保存相应的卡号,
* 以后浏览器访问哪个网站就会带上这个网站的cookie
* 子域之间:gulimall,auth.gulimall.com等
* 那么发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用
*/
//TODO 1.默认发的令牌。key是session,值是唯一字符串。但是作用域是当前域,那么当前域解决不了session共享的问题,解决子域session共享问题
//TODO 2.使用JSON的序列化方式来序列化对象数据到redis中
session.setAttribute("loginUser",data);
//2.换取成功access Token,也即登录成功就跳到我们应用的首页
return "redirect:http://gulimall.com";
}else {//否则就是失败的
return "redirect:http://auth.gulimall.com/login.html";//重定向到登录页,进行重新登录
}
}else { //不成功,没有获取到access Token
return "redirect:http://auth.gulimall.com/login.html";//重定向到登录页,进行重新登录
}
}
}
1.2Feign远程接口
MemberFeignService
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
public R regist(@RequestBody UserRegistVo vo);
@PostMapping("/member/member/login")
public R login(@RequestBody UserLoginVo vo);
@PostMapping("/member/member/oauth2/login")
public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
}
ThirdPartFeignService:
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
1.3entity:
UserregistryVo
public class UserRegistVo {
@NotEmpty(message = "用户名必须填写")
@Length(min = 6,max = 18,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;
}
SocialUser:
public class SocialUser {
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
UserLoginVo略
二。Member服务
2.1 MemberController登陆注册方法
/**
* 社交登录控制器
* @return
*/
@PostMapping("/oauth2/login")
public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception {
MemberEntity entity = memberService.login(socialUser);
if (entity!=null){ //登录成功
return R.ok().setData(entity);
}else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
}
}
/**
* 会员登录控制器
* @return
*/
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo vo){
MemberEntity entity = memberService.login(vo);
if (entity!=null){ //登录成功
return R.ok().setData(entity);
}else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
}
}
/**
* 注册会员控制器
*/
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){ //请求体的json转成MemberLoginVo对象
try {
memberService.regist(vo);
}catch (PhoneExistException e){
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
}catch (UsernameExistException e){
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
2.2MemberServiceImpl登陆注册实现
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {
@Autowired
MemberLevelDao memberLevelDao;
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<MemberEntity> page = this.page(
new Query<MemberEntity>().getPage(params),
new QueryWrapper<MemberEntity>()
);
return new PageUtils(page);
}
@Override
public void regist(MemberRegistVo vo) {
MemberDao memberDao = this.baseMapper;
MemberEntity entity = new MemberEntity();
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();//获取默认等级信息
entity.setLevelId(levelEntity.getId());//设置默认等级
//检查用户名邮箱和手机号是否唯一,让controller感知异常,此方法中设置异常机制
checkPhoneUnique(vo.getPhone());
checkUsernameUnique(vo.getUserName());
entity.setMobile(vo.getPhone()); //设置手机号
entity.setUsername(vo.getUserName());//设置用户名
entity.setNickname(vo.getUserName());
//密码加密才能存储
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());//原密码加密
entity.setPassword(encode);
memberDao.insert(entity);
}
/**
* 检查手机号是否唯一 ,★异常记得在接口声明
*
* @param phone
* @throws PhoneExistException
*/
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException {
MemberDao memberDao = this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile > 0) { //说明数据库有这条记录,就来抛异常
throw new PhoneExistException();
}
}
/**
* 检查用户名是否唯一
*
* @param username
* @throws UsernameExistException
*/
@Override
public void checkUsernameUnique(String username) throws UsernameExistException {
MemberDao memberDao = this.baseMapper;
Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
if (count > 0) { //说明数据库有这条记录,就来抛异常
throw new UsernameExistException();
}
}
/**
* 登录逻辑
*
* @param vo
* @return
*/
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
//1.去数据库查询数据
MemberDao memberDao = this.baseMapper;
MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
if (entity == null) { //数据库没有这个账号,登录失败
return null;
} else {
//获取数据库的密码字段
String passwordDb = entity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean matches = passwordEncoder.matches(password, passwordDb);//密码比较和匹配,方法的第一个参数是要求传明文的密码,第二个参数是传加密后的密码
if (matches) {//如果匹配成功,就说明登录成功了
return entity;//把当前的用户返回
} else { //如果匹配不成功,就说明登录失败了
return null;
}
}
}
/**
* 使用社交账号进行登录
* 没登陆过就进行注册,注册的目的就是为了保存当前这个社交用户,在我们数据库对应的哪个用户id
*
* @param socialUser
* @return
*/
@Override
public MemberEntity login(SocialUser socialUser) throws Exception {
//登录和注册合并
//首先判断这个用户到低以前登录过没
String uid = socialUser.getUid();//社交账号当前登录这个网站的微博id
//1.根据uid来判断当前社交用户是否已经登录过系统
MemberDao memberDao = this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) { //说明这个用户已经注册过的
//一旦如果是已经注册过的,那么就更新令牌以及更新时间
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
memberDao.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else { //说明这个用户还没有注册过的
//若本商城系统没有查到当前社交的用户对应的记录,就需要注册
MemberEntity regist = new MemberEntity();
//todo 即使远程查询出现有问题了也没关系,也不用管 ???
try {
//查询当前社交用户的社交账号信息(昵称,性别等信息)
Map<String, String> query = new HashMap<>();//用于封装查询参数的
query.put("access_token", socialUser.getAccess_token());
query.put("uid", socialUser.getUid());
//查出用户的详细信息
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", 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");//获取当前微博登录成功后的性别
//进行注册
regist.setNickname(name); //设置默认的昵称
regist.setGender("m".equals(gender) ? 1 : 0);
}
} catch (Exception e) {
}
//以后只要社交用户是第一次登陆,那么数据库里面就会有一条记录,那么相当于它就是注册进来了
//这三条数据无论是否远程查询出问题都要设置??????
regist.setSocialUid(socialUser.getUid());//设置当前微博登录成功后的Uid
regist.setAccessToken(socialUser.getAccess_token());//设置当前登录后的令牌
regist.setExpiresIn(socialUser.getExpires_in());//设置当前登录后的令牌过期时间
//插入数据
memberDao.insert(regist);
//插入数据成功以后,说明这个用户也就登陆成功了
return regist;
}
}
}
三。第三方短信服务略
四 分布式session(跨域共享session)
4.1session基本原理:
类似于银行卡的流程。每个银行只发属于本行的卡。(工商、农业。。。)
4.2session共享带来的两大问题:
一。 第一个服务器存了session,而第二个相同功能的服务器不知道
二。不同的服务怎么做
4.3 同级域的session共享方案:
本文采用这种方式
★4.4 子域的session共享问题方案(思路:放大jsession作用域,让它在其他服务都生效)
1.自定义session作用域:整个父域
2.session统一存入redis,同时任何模块微服务统一带同一张卡,就可以session共享
4.5 配置分布式全局session
4.5.1 自定义springSession完成子域共享(此配置最好加在每个微服务上)
@Configuration
public class MySessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");//放大jsession在整个系统域的作用域,这里是父域
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
/**
* 序列化机制(在写每个实体类的时候用jdk的序列化机制也可)
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
4.5.2所有微服务启动类加上@EnableRedisHttpSession注解
5.0配置文件加redisSession
spring.session.store-type=redis
server.servlet.session.timeout=30m