文章目录
一、短信验证功能
// 在gulimall-third-party中编写发送短信的逻辑代码如下
@Data
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Component
public class SmsComponent {
private String host;
private String path;
private String skin;
private String sign;
private String appCode;
public String sendSmsCode(String phone, String code){
String method = "GET";
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "APPCODE " + this.appCode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("code", code);
querys.put("phone", phone);
querys.put("skin", this.skin);
querys.put("sign", this.sign);
HttpResponse response = null;
try {
response = HttpUtils.doGet(this.host, this.path, method, headers, querys);
if(response.getStatusLine().getStatusCode() == 200){
return EntityUtils.toString(response.getEntity());
}
} catch (Exception e) {
e.printStackTrace();
}
return "fail_" + response.getStatusLine().getStatusCode();
}
}
@Controller
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
private SmsComponent smsComponent;
/*** 提供给别的服务进行调用的
只提供短信发送
*/
@GetMapping("/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
if(!"fail".equals(smsComponent.sendSmsCode(phone, code).split("_")[0])){
return R.ok();
}
return R.error(BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getMsg());
}
}
来思考一个问题,如果有恶意用户 反复提交短信请求怎么办?
- 在redis中以phone-code为前缀将电话号码和验证码进行存储并将当前时间与code一起存储,如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息,60s以后再次调用,需要删除之前存储的phone-code,code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
@ResponseBody
@GetMapping("/sms/snedcode")
public R sendCode(@RequestParam("phone") String phone){
//存redis
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
// 如果不为空说明反复提交了
if(null != redisCode && redisCode.length() > 0){
long CuuTime = Long.parseLong(redisCode.split("_")[1]);
if(System.currentTimeMillis() - CuuTime < 60 * 1000){ // 60s
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
}
}
// 生成验证码
String code = UUID.randomUUID().toString().substring(0, 6);
String redis_code = code + "_" + System.currentTimeMillis();
// 缓存验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, redis_code , 10, TimeUnit.MINUTES);
try {// 调用第三方短信服务
return thirdPartFeignService.sendCode(phone, code);
} catch (Exception e) {
log.warn("远程调出错");
}
return R.ok();
}
二、注册功能(略过)
三、登录功能
3.1 oauth
一般第三方登录流程
- 点击xx登录
- 引导用户跳转到授权页
- 用户主动点击授权,认证成功跳回之前网页
ps: 其实上述说的流程就是oauth协议的过程
3.2 整合第三方社交登录(以微博为例)
- 直接百度搜索
微博开放平台
- 创建新应用xx,会得到APP KEY和APP Secret
- 授权回调页:gulimall.com/oauthn2.0/weibo/success
- 取消授权回调页:gulimall.com/oauthn2.0/weibo/fail
- 具体步骤
- 点击首页的微博登录,此时需要引导用户到微博的授权页面
// 其实点击的时候访问的就是这个地址
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=授权后跳转的uri
// 伪代码
https://api.weibo.com/oauth2/authorize?
client_id=刚才申请的APP-KEY &
response_type=code&
redirect_uri=http://gulimall.com/success
- 如果用户同意授权(输入账号密码),带着code,页面跳转至我们指定登录成功的controller这时候会有一个code
- 拿着code去换accesstoken
// 伪代码
https://api.weibo.com/oauth2/access_token?
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
grant_type=authorization_code&
redirect_uri=YOUR_REGISTERED_REDIRECT_URI&
code=CODE
- 拿到他给的acesstoken 就可以对他给出的接口进行一些数据的获取 https://open.weibo.com/wiki/2/users/show
3.2.1 代码
// 认证成功的回调
@GetMapping("/weibo/success")
public String weiBo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 根据code换取 Access Token
Map<String,String> map = new HashMap<>();
map.put("client_id", "1294828100");
map.put("client_secret", "a8e8900e15fba6077591cdfa3105af44");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
Map<String, String> headers = new HashMap<>();
// 去获取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
if(response.getStatusLine().getStatusCode() == 200){
// 获取返回结果
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
// 进行保存数据库 要保存的主要有uid、token、expires_in
R login = memberFeignService.login(socialUser);
if(login.getCode() == 0){
MemberRsepVo rsepVo = login.getData("data" ,new TypeReference<MemberRsepVo>() {});
// 第一次使用session 命令浏览器保存这个用户信息 JESSIONSEID 每次只要访问这个网站就会带上这个cookie
// 在发卡的时候扩大session作用域 (指定域名为父域名)
// TODO 1.默认发的当前域的session (需要解决子域session共享问题)
// TODO 2.使用JSON的方式序列化到redis
// new Cookie("JSESSIONID","").setDomain("gulimall.com");
session.setAttribute(AuthServerConstant.LOGIN_USER, rsepVo);
// 登录成功 跳回首页
return "redirect:http://gulimall.com";
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}
@RequestMapping("/oauth2/login")
public R login(@RequestBody SocialUser socialUser) {
MemberEntity entity=memberService.login(socialUser);
if (entity!=null){
return R.ok().put("memberEntity",entity);
}else {
return R.error();
}
}
@Override
public MemberEntity login(SocialUser socialUser) {
// 微博的uid
String uid = socialUser.getUid();
// 判断社交用户登录过系统
MemberDao dao = this.baseMapper;
MemberEntity entity = dao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
MemberEntity memberEntity = new MemberEntity();
if(entity != null){ // 注册过
// 说明这个用户注册过, 修改它的资料
// 更新令牌
memberEntity.setId(entity.getId());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 更新
dao.updateById(memberEntity);
entity.setAccessToken(socialUser.getAccessToken());
entity.setExpiresIn(socialUser.getExpiresIn());
entity.setPassword(null);
return entity;
}else{ // 如果没有注册过
HashMap<String, String> map = new HashMap<>();
map.put("access_token", socialUser.getAccessToken());
map.put("uid", socialUser.getUid());
try {
// 查询基本信息
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), map);
if(response.getStatusLine().getStatusCode() == 200){
// 查询成功
String json = EntityUtils.toString(response.getEntity());
// 解析json
JSONObject jsonObject = JSON.parseObject(json);
memberEntity.setNickname(jsonObject.getString("name"));
memberEntity.setUsername(jsonObject.getString("name"));
memberEntity.setGender("m".equals(jsonObject.getString("gender"))?1:0);
memberEntity.setCity(jsonObject.getString("location"));
memberEntity.setJob("ss");
memberEntity.setEmail(jsonObject.getString("email"));
}
} catch (Exception e) {
log.warn("第三方登录调用服务出错");
}
memberEntity.setStatus(0);
memberEntity.setCreateTime(new Date());
memberEntity.setBirth(new Date());
memberEntity.setLevelId(1L);
memberEntity.setSocialUid(socialUser.getUid());
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
// 注册 -- 登录成功
dao.insert(memberEntity);
memberEntity.setPassword(null);
return memberEntity;
}
}
3.3 springsession(分布式session)
3.3.1 session
一般访问请求是这样session存储在服务端,jsessionId存在客户端,每次请求的时候带上jsessionid
问题:但是正常情况下session不可跨域,它有自己的作用范围(就是不能跨服务 )
JsessionId的参数Value(值) Domain(gulimall.com要放大域名作用域) Path(作用范围) Expires/Max-Age(过期时间)
- 解决方案1.session复制
- 解决方案2.hash一致性
- 解决方案3.redis(推荐)
3.3.2 SpringSession整合redis
//依赖
<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>
//配置
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=192.168.56.10
// 启动类加上
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {
扩大session作用域
@Configuration
public class GulimallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
// 这个配置需要放到每个微服务上
登录逻辑
@GetMapping({"/login.html","/","/index","/index.html"}) // auth
public String loginPage(HttpSession session){
// 从session从获取loginUser
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);// "loginUser";
System.out.println("attribute:"+attribute);
if(attribute == null){
return "login";
}
System.out.println("已登陆过,重定向到首页");
return "redirect:http://gulimall.com";
}
@PostMapping("/login")
public String login(UserLoginVo userLoginVo,
RedirectAttributes redirectAttributes,
HttpSession session){
R r = memberFeignService.login(userLoginVo);
if(r.getCode() == 0){
MemberRespVo respVo = r.getData("data", new TypeReference<MemberRespVo>() {});
// 放入session // key为loginUser
session.setAttribute(AuthServerConstant.LOGIN_USER, respVo);//loginUser
log.info("\n欢迎 [" + respVo.getUsername() + "] 登录");
// 登录成功重定向到首页
return "redirect:http://gulimall.com";
}else {
HashMap<String, String> error = new HashMap<>();
// 获取错误信息
error.put("msg", r.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", error);
return "redirect:http://auth.gulimall.com/login.html";
}
}
3.4 单点登录
常见 两个不同域名的想共享session 这肯定是不行的 session的域没这么大
解决方案:建一个公共的认证中心sso 其实可以简单把他理解为颁发凭证的地方 在这块登录了其他的地方就可以了
过程
1.a用户访问app1,app1有些功能是需要登录的a没登录
2. 这时候跳转到认证服务(sso登录系统),sso没登录,弹出登录页面
3. 用户填写信息,sso进行认证,成功写状态(session)
4. 认证服务登录成功会产生一个凭据(Service Ticket),然后调到app1,这时候将凭据作为参数传入
5. app1 拿到参数发请求到sso看是不是有效的
6. 验证通过后,app系统将登录状态写入session并设置app域下的Cookie
到上面为止一个跨域的单点登录就完成了 我们在看看app2的流程(app2 app1 sso服务不是在一个域的)
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了,不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
用的开源sso的项目:https://gitee.com/xuxueli0323/xxl-sso