最近做了一个登录,要将原有的session验证会话变更为token会话验证,解决各个终端的会话验证的问题。
用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。
微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息,客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
有状态登录:
1.服务端保存大量数据,增加服务端压力
2.服务端保存用户状态,不支持集群化部署
无状态登录:
1.客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
2.服务端的集群和状态对客户端透明
3.服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
4.减小服务端存储压力
2)、登录发送用户名及密码至认证服务(passport)
3)、认证服务验证用户名及密码,如果通过,通过jwt工具生成token
同时生成两种token:access_token和refresh_token
token的格式为:
{
uid:1
username:"kingapex",
role:"seller"
}
access_token有效期为15分钟,refresh_token有效期为30分钟(以上时间应为可配置的常量)
4)、将生成的token写入redis
分别 以ACCESSTOKEN\{uid}和REFRESHTOKEN\{uid} 为key存储
有效期为别为各token的有效期+60s
5)、返回token
将token:access_token和refresh_token 、userid同时都返回,格式如下:
{
uid:1,
access_token:"xxxxx",
refresh_token:"xxxxx"
}
二、访问api
携带access_token信息
如果是生产环境不会直接携带access_token信息,详见这里
2)、获取token
根据环境不同而有不同的获取token方式,参考这里
3)、解析token
通过jwt工具将token解析
4)、由redis中读取token
根据uid拼接key读取出access_token
如果不存在这个用户的token说明用户已经登出
5)、验证token
判断此token是否属于此uid
判断token是否已经过期,如果过期,则进行刷新token流程
6)、注入权限
如果token验证成功,根据user信息生成权限注入到spring安全上下文中
1)、客户端请求api
携带refresh_token信息
如果是生产环境不会直接携带refresh_token信息,详见这里
2)、获取token
根据环境不同而有不同的获取token方式,参考这里
3)、解析token
通过jwt工具将token解析
4)、由redis中读取token
根据uid拼接key读取出access_token
如果不存在这个用户的token说明用户已经登出
5)、验证token
判断此token是否属于此uid
判断token是否已经过期,如果过期,则返回refresh_token过期错误,此时用户需要重新登录
6)、刷新token
如果refresh_token 验证成功,则重新生成access_token和refresh_token
上述有效期以当前时间向后计算
替换此用户在redis中的token
并将token返回给客户端
四、注销
请求注销api,服务器端和客户端应同时删除token的存储
controller层
@GetMapping("/login")
@ApiOperation(value = "用户名(手机号)/密码登录API")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "password", value = "密码", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "captcha", value = "验证码", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "uuid", value = "客户端唯一标识", required = true, dataType = "String", paramType = "query"),
})
public MemberVO login(@NotEmpty(message = "用户名不能为空") String username, @NotEmpty(message = "密码不能为空") String password, @NotEmpty(message = "图片验证码不能为空") String captcha, @NotEmpty(message = "uuid不能为空") String uuid) {
//验证图片验证码是否正确
if ("prod".equals(profile)) {
boolean isPass = captchaClient.valid(uuid, captcha, SceneType.LOGIN.name());
if (!isPass) {
throw new ServiceException(MemberErrorCode.E107.code(), "图片验证码错误!");
}
}
//校验账号信息是否正确
return memberManager.login(username, password,1);
}
校验账户密码
@Override
public Member validation(String username, String password) {
String pwdmd5 = "";
//用户名登录处理
Member member = this.getMemberByName(username);
if (member != null) {
if (!StringUtil.equals(member.getUname(), username)) {
throw new ServiceException(MemberErrorCode.E107.code(), "账号密码错误!");
}
pwdmd5 = StringUtil.md5(password + member.getUname().toLowerCase());
if (member.getPassword().equals(pwdmd5)) {
return member;
}
}
//手机号码登录处理
member = this.getMemberByMobile(username);
if (member != null) {
pwdmd5 = StringUtil.md5(password + member.getUname().toLowerCase());
if (member.getPassword().equals(pwdmd5)) {
return member;
}
}
//邮箱登录处理
member = this.getMemberByEmail(username);
if (member != null) {
pwdmd5 = StringUtil.md5(password + member.getUname().toLowerCase());
if (member.getPassword().equals(pwdmd5)) {
return member;
}
}
throw new ServiceException(MemberErrorCode.E107.code(), "账号密码错误!");
}
生成Token,赋予对应权限
@Override
public String createToken(Member member, int time) {
ObjectMapper oMapper = new ObjectMapper();
String token = null;
//获取店员信息
com.enation.app.javashop.core.shop.model.dos.Clerk clerkDb = clerkManager.getClerkByMemberId(member.getMemberId());
//如果店员不为空,则说明他是店铺管理员,需要赋值商家权限
if (clerkDb != null) {
Clerk clerk = new Clerk();
ShopVO shopVO = shopClient.getShop(clerkDb.getShopId());
clerk.setSellerName(shopVO.getShopName());
clerk.setSellerId(shopVO.getShopId());
clerk.setUsername(member.getUname());
clerk.setUid(member.getMemberId());
clerk.setUsername(member.getUname());
clerk.setClerkName(clerkDb.getClerkName());
clerk.setClerkId(clerkDb.getClerkId());
clerk.setSelfOperated(shopVO.getSelfOperated());
//如果是超级店员则赋值超级店员的权限,否则去查询权限赋值
if (clerkDb.getFounder().equals(1)) {
clerk.setRole("SUPER_SELLER");
} else {
ShopRole shopRole = this.shopRoleManager.getModel(clerkDb.getRoleId());
clerk.setRole(shopRole.getRoleName());
}
Map clerkMap = oMapper.convertValue(clerk, HashMap.class);
token = Jwts.builder()
.setClaims(clerkMap)
.setSubject(Role.CLERK.name())
.setExpiration(new Date(System.currentTimeMillis() + time * 1000))
.signWith(SignatureAlgorithm.HS512, JWTConstant.SECRET)
.compact();
return token;
} else {
//如果是会员,则赋值买家权限
Buyer buyer = new Buyer();
buyer.setUid(member.getMemberId());
buyer.setUsername(member.getUname());
Map buyerMap = oMapper.convertValue(buyer, HashMap.class);
token = Jwts.builder()
.setClaims(buyerMap)
.setSubject(Role.BUYER.name())
.setExpiration(new Date(System.currentTimeMillis() + time * 1000))
.signWith(SignatureAlgorithm.HS512, JWTConstant.SECRET)
.compact();
return token;
}
}
将token存储到redis,返回token以及用户信息
@Override
public MemberVO loginHandle(Member member, Integer memberOrSeller) {
if (!member.getDisabled().equals(0)) {
throw new ServiceException(MemberErrorCode.E107.code(), "当前账号已经禁用,请联系管理员");
}
String accessToken = passportManager.createToken(member, javashopConfig.getAccessTokenTimeout());
String refreshToken = passportManager.createToken(member, javashopConfig.getRefreshTokenTimeout());
//Oauth2Token oAuth2Token = passportManager.createOauth2Token(member);
//组织返回数据
MemberVO memberVO = new MemberVO(member, accessToken, refreshToken);
cache.put(TokenKeyGenerate.generateBuyerAccessToken(ThreadContextHolder.getHttpRequest().getHeader("uuid"), member.getMemberId()), accessToken, javashopConfig.getAccessTokenTimeout() + 60);
cache.put(TokenKeyGenerate.generateBuyerRefreshToken(ThreadContextHolder.getHttpRequest().getHeader("uuid"), member.getMemberId()), refreshToken, javashopConfig.getRefreshTokenTimeout() + 60);
try {
TokenResult result = registerRongCloud(memberVO);
if (result.code == 200) {
memberVO.setRongCloudToken(result.getToken());
}
} catch (Exception e) {
logger.error("注册融云信息异常", e);
}
if(StringUtils.isBlank(memberVO.getFace())) memberVO.setFace(DEFAULT_FACE_URL);
//发送登录消息
MemberLoginMsg loginMsg = new MemberLoginMsg();
loginMsg.setLastLoginTime(member.getLastLogin());
loginMsg.setMemberId(member.getMemberId());
loginMsg.setMemberOrSeller(memberOrSeller);
this.amqpTemplate.convertAndSend(AmqpExchange.MEMEBER_LOGIN, AmqpExchange.MEMEBER_LOGIN + "_ROUTING", loginMsg);
try {
LogRecordDO log = createLoginLog(member);
amqpTemplate.convertAndSend(AmqpExchange.USER_ACCESS_LOG_STORE, AmqpExchange.USER_ACCESS_LOG_STORE + "_ROUTING", log);
}catch (Exception e){
logger.error("会员登录记录日志异常", e);
}
Optional.ofNullable(channelProviderClient.getSingle(memberVO.getUid())).ifPresent(channelProviderVO -> memberVO.setFindCode(channelProviderVO.getFindCode()));
return memberVO;
}