OAuth2 + JWT 登录流程

最近做了一个登录,要将原有的session验证会话变更为token会话验证,解决各个终端的会话验证的问题。

基于Token机制鉴权架构

  • 1.会话机制
    • 1.1有状态登录
    • 1.2无状态登录
    • 1.3 优缺点
  • 2.登录流程
  • 3.时序图
  • 4.代码实现

1.会话机制

1.1有状态登录

用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。

1.2无状态登录

微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息,客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

1.3 优缺点

有状态登录:
1.服务端保存大量数据,增加服务端压力
2.服务端保存用户状态,不支持集群化部署
无状态登录:
1.客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
2.服务端的集群和状态对客户端透明
3.服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
4.减小服务端存储压力

2.登录流程

一、登录:
OAuth2 + JWT 登录流程_第1张图片
1)、当用户需要登录时,客户端引导用户进行登录

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

OAuth2 + JWT 登录流程_第2张图片
1)、客户端请求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安全上下文中

三、刷新token
OAuth2 + JWT 登录流程_第3张图片

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的存储

3.时序图

OAuth2 + JWT 登录流程_第4张图片

4.代码实现

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;
}

你可能感兴趣的:(商城)