登录
1.1. 用户密码
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){
R r = memberFeignService.login(vo);
if(r.getCode() == 0){
MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() {
});
session.setAttribute("loginUser", data);
return "redirect:http://gulimall.com";
}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/login.html";
}
}
@GetMapping("/login.html")
public String loginPage(HttpSession session){
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "redirect:http://gulimall.com";
}
return "login";
}
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
MemberEntity memberEntity = this.getOne(new QueryWrapper<MemberEntity>()
.eq("username", loginacct)
.or().eq("mobile", loginacct));
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if(memberEntity == null){
return null;
}
if (passwordEncoder.matches(password, memberEntity.getPassword())) {
return memberEntity;
}
return null;
}
1.2. 社交登录
QQ 、微博、 github 等网站的用户量非常大,别的网站为了简化自我网站的登陆与注册逻辑,引入社交登陆功能;
步骤:
1 )、用户点击 QQ 按钮
2 )、引导跳转到 QQ 授权页
3)、用户主动点击授权,跳回之前网页。
1.2.1. OAuth2.0
- OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
- OAuth2.0:对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
- 官方版流程:
(A )用户打开客户端以后,客户端要求用户给予授权。
(B )用户同意给予客户端授权。
(C )客户端使用上一步获得的授权,向认证服务器申请令牌。
(D )认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E )客户端使用令牌,向资源服务器申请获取资源。
(F )资源服务器确认令牌无误,同意向客户端开放资源。
OAuth2.0流程:
- 使用Code换取AccessToken,Code只能用一次
- 同一个用户的accessToken一段时间是不会变化的,即使多次获取
1.2.2. 代码实现
1)、进入微博开放平台
2)、添加社交登录回调接口
认证接口
- 通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
- 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
修改“com.cwh.gulimall.auth.feign.MemberFeignService”类,代码如下:
@PostMapping("/member/member/oauth2/login")
public R oauth2Login(@RequestBody SocialUser socialUser);
添加“com.cwh.gulimall.auth.vo.SocialUser”类,代码如下:
package com.cwh.gulimall.auth.vo;
import lombok.Data;
@Data
public class SocialUser {
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
添加“com.cwh.gulimall.auth.vo.MemberResponseVO”类,代码如下:
package com.cwh.gulimall.auth.vo;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
@ToString
@Data
public class MemberResponseVO {
private Long id;
/**
* 会员等级id
*/
private Long levelId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 手机号码
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 头像
*/
private String header;
/**
* 性别
*/
private Integer gender;
/**
* 生日
*/
private Date birth;
/**
* 所在城市
*/
private String city;
/**
* 职业
*/
private String job;
/**
* 个性签名
*/
private String sign;
/**
* 用户来源
*/
private Integer sourceType;
/**
* 积分
*/
private Integer integration;
/**
* 成长值
*/
private Integer growth;
/**
* 启用状态
*/
private Integer status;
/**
* 注册时间
*/
private Date createTime;
private String socialUid;
private String accessToken;
private long expiresIn;
}
添加“com.cwh.gulimall.auth.controller.Oauth2Controller”类,代码如下:
@Controller
public class OauthController {
@Autowired
private MemberFeignService memberFeignService;
@RequestMapping("/oauth2.0/weibo/success")
public String authorize(String code, RedirectAttributes attributes) throws Exception {
// 1、使用code换取token,换取成功则继续2,否则重定向至登录页
Map<String, String> query = new HashMap<>();
query.put("client_id", "2144***074");
query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
query.put("grant_type", "authorization_code");
query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
query.put("code", code);
// 发送post请求换取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
Map<String, String> errors = new HashMap<>();
if (response.getStatusLine().getStatusCode() == 200) {
// 2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
});
R login = memberFeignService.login(socialUser);
// 2.1 远程调用成功,返回首页并携带用户信息
if (login.getCode() == 0) {
String jsonString = JSON.toJSONString(login.get("memberEntity"));
MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {
});
attributes.addFlashAttribute("user", memberResponseVo);
return "redirect:http://gulimall.com";
}else {
// 2.2 否则返回登录页
errors.put("msg", "登录失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}else {
errors.put("msg", "获得第三方授权失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
登录接口
- 登录包含两种流程,实际上包括了注册和登录
- 如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息,注册并将结果返回
- 如果之前已经使用该社交账号登录,则更新token并将结果返回
添加“com.cwh.gulimall.member.vo.SocialUser”类,代码如下:
package com.cwh.gulimall.member.vo;
import lombok.Data;
@Data
public class SocialUser {
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
修改“com.cwh.gulimall.member.controller.MemberController”类,代码如下:
@PostMapping("/oauth2/login")
public R oauth2Login(@RequestBody SocialUser socialUser){
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());
}
}
修改gulimall_ums.ums_member表结构,sql如下:
ALTER TABLE `gulimall_ums`.`ums_member`
ADD COLUMN `social_uid` varchar(255) NULL COMMENT '社交用户id' AFTER `create_time`,
ADD COLUMN `access_token` varchar(255) NULL COMMENT '访问token' AFTER `social_uid`,
ADD COLUMN `expires_in` int NULL COMMENT '过期时间戳' AFTER `access_token`;
修改“com.cwh.gulimall.member.entity.MemberEntity”类,新增三个属性,代码如下:
修改“com.cwh.gulimall.member.service.MemberService”类,代码如下:
MemberEntity login(SocialUser socialUser);
修改“com.cwh.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:
@Override
public MemberEntity login(SocialUser socialUser) {
// 1 根据 uid 判断当前用户是否以前用社交平台登录过系统
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUser.getUid()));
if (!StringUtils.isEmpty(memberEntity)) {
// 说明这个用户之前已经注册过
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
// 未找到则注册 根据社交平台的开放接口查询用户的开放信息存储到系统
MemberEntity register = new MemberEntity();
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<>(), 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");
// ......
register.setNickname(name);
register.setGender("m".equals(gender) ? 1 : 0);
// .....
}
} catch (Exception e) {
log.warn("调用微博接口获取信息异常{}", e);
}
register.setSocialUid(socialUser.getUid());
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.insert(register);
return register;
}
小结
Oauth2.0 ;授权通过后,使用 code 换取 access_token ,然后去访问任何开放 API
1 )、 code 用后即毁
2 )、 access_token 在几天内是一样的
3 )、 uid 永久固定
1.3. SpringSession
1.3.1. Session共享问题
1.3.1.1. session原理
jsessionid相当于银行卡,存在服务器的session相当于存储的现金,每次通过jsessionid取出保存的数据。
问题:但是正常情况下session不可跨域,它有自己的作用范围
1.3.1.2. 分布式下session共享问题
- 同一个服务,复制多份,session不同步问题
- 不同服务,session不能共享问题
1.3.2. Session共享问题解决
1.3.2.1. session复制
1.3.2.2. 客户端存储
1.3.2.3. hash一致性
1.3.2.4. 统一存储
1.3.2.5. 不同服务,子域session共享
1.3.3. SpringSession整合redis
通过SpringSession修改session的作用域
1.3.3.1. 环境搭建
gulimall-auth-server模块
pom导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
修改apllication.properties配置
spring.session.store-type=redis
主配置类添加注解@EnableRedisHttpSession
修改“com.cwh.gulimall.auth.GulimallAuthServerApplication”类,代码如下:
@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
1.3.3.2. 自定义配置
1.3.4. SpringSession核心原理
核心原理:
1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration.class配置
1、给容器中添加了一个组件
SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增删改查的封装类
2、SessionRepositoryFilter=》Filter: session存储过滤器,每个请求过来都必须经过filter
1、创建的时候,就自动从容器中获取到了SessionRepository:
2、原生的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
3、以后获取session.request.getSession()
4、wrapperedRequest.getSession();===>SressionRepository中获取到
自动延期。redis中的数据也是有过期时间的
装饰者模式 - SessionRepositoryFilter
- 原生的获取session时是通过HttpServletRequest获取的
- 这里对request进行包装,并且重写了包装request的getSession()方法
1.3.5. 页面调整
1.3.5.1. 只要登录成功,缓存有用户数据,再点击登录链接,直接调转到首页;把GulimallWebConfig登录页的映射注释掉
修改“com.cwh.gulimall.auth.controller.LoginController”类,代码如下:
@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";
}
}
1.3.5.2. 账号密码方式登录也要显示用户名
正常登录也要显示用户名,返回时也要给他放入用户信息
修改“com.cwh.gulimall.auth.controller.LoginController”类,代码如下:
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {
log.info("登录请求参数:{}", JSON.toJSONString(vo));
//远程登录
R r = memberFeignService.login(vo);
if (r.getCode() == 0) {
MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {
});
// 成功放到session中
session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
return "redirect:http://gulimall.com";
} 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/login.html";
}
}
1.3.5.3. 只要登陆成功每个页面都显示用户名
gulimall-search服务页面显示用户名,需要先搭建好SpringSession环境
导入依赖
<!--整合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
spring.redis.host=192.168.119.127
spring.redis.port=6379
#session存储格式
spring.session.store-type=redis
加注解
添加SpringSession配置类
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
1.4. SSO 单点登录
Single Sign On 一处登陆、处处可用
前置概念
1 )、单点登录业务介绍
早期单一服务器,用户认证。
缺点:单点性能压力,无法扩展
分布式,SSO(single sign on)模式
多系统
解决 :
- 用户身份信息独立管理,更好的分布式管理。
- 可以自己扩展安全策略
- 跨域不是问题
缺点:
- 认证服务器访问压力较大。
xxl-sso流程:
- /xxl-sso-server 登录服务器 8080 ssoserver.com
- /xxl-sso-web-sample-springboot 项目1 8081 client1.com
- /xxl-sso-web-sample-springboot 项目2 8082 client2.com
#----------sso----------
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
1)、中央认证服务器;ssoserver.com
2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来
3)、只要有一个登录,其他都不用登录
4)、全系统统一一个sso-sessionid;所有系统可能域名都不相同