LoginController
/**
* 用户登录:账号密码登录
* @param vo
* @return
*/
@PostMapping("/login")
public String login(UserLoginVO vo, RedirectAttributes redirectAttributes,HttpSession session) {
//调用远程接口
R r = memberFeignService.login(vo);
if (r.getCode() != 0) {
Map<String, String> errors = new HashMap<>();
String msg = r.getData("msg", new TypeReference<String>() {});
errors.put("msg", msg);
redirectAttributes.addFlashAttribute("errors", errors);
//登录失败,重定向到登录页面
return "redirect:http://auth.gmall.com/login.html";
}
//将用户信息放入到session中
MemberVO memberVO = r.getData("data", new TypeReference<MemberVO>() {});
session.setAttribute(AuthConstant.SESSION_LOGIN_USER, memberVO);
//登录成功,重定向到首页
return "redirect:http://gmall.com";
}
MemberController
/**
* 会员登录
* @param vo
* @return
*/
@PostMapping("/login")
public R login(@RequestBody MemberLoginVO vo){
MemberEntity memberEntity = memberService.login(vo);
if (memberEntity == null) {
return R.error(BizCode.ACCOUNT_PASSWORD_EXCEPTION.getCode(),
BizCode.ACCOUNT_PASSWORD_EXCEPTION.getMessage());
}
return R.ok().put("data", memberEntity);
}
MemberServiceImpl
/**
* 会员登录
* @param vo
* @return
*/
@Override
public MemberEntity login(MemberLoginVO vo) {
String account = vo.getAccount();
String password = vo.getPassword();
//数据库查询密文密码
MemberEntity entity = baseMapper.selectOne(new QueryWrapper<MemberEntity>()
.eq("username",account).or().eq("mobile", account));
if (entity != null) {
//密文
String passwordDb = entity.getPassword();
//密码匹配
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean matches = passwordEncoder.matches(password, passwordDb);
if (matches) {
return entity;
} else {
return null;
}
}
return null;
}
QQ、微博、微信、GitHub等网站的的用户量非常大,别的网站为了简化自我网站的登录与注册逻辑,就会引入社交登录功能。
以QQ为例,接入QQ登录步骤如下:
OAuth(开发授权)是一个开发标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth2.0较1.0相比,整个授权验证流程更简单更安全,也是未来最主要的用户身份验证和授权方式。对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显示的向用户征求授权。
授权流程:
微博开放平台:https://open.weibo.com
注册并完善开发者信息和开发者认证,等待审核通过(1-3天时间)。
点击 立即接入 ,开始创建应用
核心:使用code换取access_token
1)引导用户到授权页登录
API文档:https://open.weibo.com/wiki/Oauth2/authorize
GET
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
2)用户登录,并同意授权
输入社交账号和密码,进行登录,登录成功后,点击授权
返回 code,通过code来换取 Access Token
http://auth.gmall.com/oauth/weibo/success?code=XXX
3)使用code换取Access Token
API文档:https://open.weibo.com/wiki/Oauth2/access_token
POST
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
返回值:
4)使用获得的Access Token调用API
获取用户信息接口API描述:
OAuth2Controller
package com.atguigu.gmall.auth.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.atguigu.common.constant.AuthConstant;
import com.atguigu.common.utils.HttpUtils;
import com.atguigu.common.utils.JsonUtils;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberVO;
import com.atguigu.gmall.auth.feign.MemberFeignService;
import com.atguigu.gmall.auth.vo.WeiboUser;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* 社交登录 {@link OAuth2Controller}
*
* @author zhangwen
* @email: [email protected]
*/
@Controller
public class OAuth2Controller {
@Autowired
private MemberFeignService memberFeignService;
/**
* 微博登录
* @param code
* @param session
* @return
*/
@GetMapping("/oauth/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) {
Map<String, String> body = new HashMap<>();
body.put("client_id", "2605963680");
body.put("client_secret", "6758915013e266ee3c0c90f839c4b472");
body.put("grant_type", "authorization_code");
body.put("redirect_uri", "http://auth.gmall.com/oauth/weibo/success");
body.put("code", code);
try {
// 根据code换取access_token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post",
new HashMap<String, String>(), null, body);
// 处理响应
if (response.getStatusLine().getStatusCode() == 200) {
// 获取到access_token
String json = EntityUtils.toString(response.getEntity());
WeiboUser weiboUser = JsonUtils.jsonToPojo(json, WeiboUser.class);
// 登录或者注册
// 当前用户第一次登录网站,自动注册,为当前用户生成一个会员信息账号
R r = memberFeignService.weiboLogin(weiboUser);
MemberVO memberVO = r.getData("data", new TypeReference<MemberVO>() {});
session.setAttribute(AuthConstant.SESSION_LOGIN_USER, memberVO);
// 登录成功重定向到首页
return "redirect:http://gmall.com";
} else {
return "redirect:http://auth.gmall.com/login.html";
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
MemberController
/**
* 微博登录
* @param weiboUser
* @return
*/
@PostMapping("/oauth/weibo/login")
public R weiboLogin(@RequestBody WeiboUser weiboUser){
MemberEntity memberEntity = memberService.login(weiboUser);
if (memberEntity == null) {
return R.error(BizCode.ACCOUNT_PASSWORD_EXCEPTION.getCode(),
BizCode.ACCOUNT_PASSWORD_EXCEPTION.getMessage());
}
return R.ok().put("data", memberEntity);
}
MemberServiceImpl
/**
* 微博登录
* @param weiboUser
* @return
*/
@Override
public MemberEntity login(WeiboUser weiboUser) {
// 登录和注册合并逻辑
// 判断当前微博用户是否已经登录过
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>()
.eq("weibo_uid", weiboUser.getUid()));
if (memberEntity != null) {
// 用户已经注册,更新access_token和expires_in
MemberEntity updateEntity = new MemberEntity();
updateEntity.setId(memberEntity.getId());
updateEntity.setAccessToken(weiboUser.getAccess_token());
updateEntity.setExpiresIn(weiboUser.getExpires_in());
baseMapper.updateById(updateEntity);
memberEntity.setAccessToken(weiboUser.getAccess_token());
memberEntity.setExpiresIn(weiboUser.getExpires_in());
return memberEntity;
} else {
// 没有查到微博用户,则需要注册
MemberEntity regEntity = new MemberEntity();
// 查询当前微博用户的社交账号信息(昵称,性别等)
Map<String, String> querys = new HashMap<>(2);
querys.put("access_token", weiboUser.getAccess_token());
querys.put("uid", weiboUser.getUid());
try {
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json",
"get", new HashMap<String, String>(0), querys);
if (response.getStatusLine().getStatusCode() == 200) {
String json = EntityUtils.toString(response.getEntity());
Map<String, String> map = JsonUtils.jsonToObject(json, new TypeReference<Map>() {});
// 昵称
String name = map.get("name");
// 性别
String gender = map.get("gender");
regEntity.setNickname(name);
regEntity.setGender("m".equals(gender)?1:0);
}
} catch (Exception e) {
e.printStackTrace();
}
regEntity.setWeiboUid(weiboUser.getUid());
regEntity.setAccessToken(weiboUser.getAccess_token());
regEntity.setExpiresIn(weiboUser.getExpires_in());
baseMapper.insert(regEntity);
return regEntity;
}
}
同一个服务(同域名),复制多份,Session不同步问题。
不同服务(不同域名),Session不能共享问题。
Session复制
客户端存储
Hash一致性
统一存储
官方文档:https://docs.spring.io/spring-session/docs/2.3.2.RELEASE/reference/html5/
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring.session.store-type=redis # Session store type.
server.servlet.session.timeout=30m # Session timeout. If a duration suffix is not specified, seconds is used.
spring.session.redis.flush-mode=on_save # Sessions flush mode.
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
主启动类上使用 @EnableRedisHttpSession 注解开启 SpringSession
@EnableRedisHttpSession
@SpringBootApplication
public class GmallAuthApplication {
public static void main(String[] args) {
SpringApplication.run(GmallAuthApplication.class, args);
}
}
使用JSON序列化方式来序列化对象存储到Redis
默认使用JDK序列化机制,Java类需要实现序列化接口 Serializable
解决子域Session共享,Cookie令牌统一分配到父域
默认发的令牌是当前作用域
package com.atguigu.gmall.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Spring Session 配置类 {@link SessionConfig}
*
* @author zhangwen
* @email: [email protected]
*/
@Configuration
public class SessionConfig {
/**
* 序列化JSON存储
* @return
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
/**
* 指定Cookie令牌作用域
* @return
*/
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 指定Cookie令牌作用域
cookieSerializer.setDomainName("gmall.com");
cookieSerializer.setCookieName("GMALLSESSION");
return cookieSerializer;
}
}
gmall-product 商城 index 和 item 修改登录显示逻辑,从session中获取登录用户名
<li>
<a th:if="${session.loginUser!=null}">欢迎,[[${session.loginUser.nickname}]]</a>
<a th:if="${session.loginUser==null}" href="http://auth.gmall.com/login.html">你好,请登录</a>
</li>
<li>
<a th:if="${session.loginUser==null}" href="http://auth.gmall.com/reg.html" class="li_2">免费注册</a>
</li>
1)登录成功,显示用户昵称
2)Redis中存储用户信息
业务子系统三这种部署方式为水平扩展,前端通过Nginx提供反向代理,会话管理可以通过SpringSession,使用Redis来存放Session。部署Spring Boot应用到任意一台Web服务器上,从而提高了系统可靠性和可伸缩性。
核心源码分析:
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
//把session操作类放入到当前请求中
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//对原始的request和response进行包装
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);
try {
//包装后的对象应用到了后面的整个执行链
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
}
@EnableRedisHttpSession 导入 RedisHttpSessionConfiguration 配置
给容器中添加了一个组件 RedisIndexedSessionRepository :Redis操作session增删改查的封装类
继承 SpringHttpSessionConfiguration :给容器中添加了
SessionRepositoryFilter Session存储过滤器,每个请求过来都必须经过Filter
SessionRepositoryFilter
创建的时候,就自动从容器中获取到了 sessionRepository
原始的 request、response都被包装
以后获取session,都要通过原始request.getSession()获取
wrappedRequest 重写了 getSession() 方法,从 SessionRepository 中获取
Session(RedisSession)
Filter + 装饰者模式
/**
* 登录页面
* @param session
* @return
*/
@GetMapping("/login.html")
public String loginPage(HttpSession session) {
Object attribute = session.getAttribute(AuthConstant.SESSION_LOGIN_USER);
if (attribute!=null) {
//用户登录了,跳转到首页
return "redirect:http://gmall.com";
}
return "login";
}
检索服务 gmall-search