Oauth2方式实现单点登录

下面先简单介绍一下Oauth2的原理

Oauth2是什么?

Oauth2是一种授权机制,用来授权给第三方应用,获取用户数据。

Oauth2是什么的解释 这是阮一峰老师的解释,因此解释的比较好了,就不在此重复了。

Oauth2 的原理

Oauth2 有四种授权模型 授权码,隐藏式,密码式,凭证式 目前主流的形式是授权码方式。 我们项目中使用的也是授权码方式。 这里就只介绍一下授权码方式 。

授权码方式

这块的内容完全是引用于Oauth2原理 只是为了方便大家截阅读不用在来回跳转。 其他的几种方式可以去这篇文章中查看。

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。

https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。
在这里插入图片描述

第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code参数就是授权码。
在这里插入图片描述

第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。


https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=authorization_code&
 code=AUTHORIZATION_CODE&
 redirect_uri=CALLBACK_URL

上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

在这里插入图片描述

第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

{    
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}

上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了。

在这里插入图片描述

单点登录的实现

下面我们就要开始动手了。

第一步,配置哪些应用可以到我们服务器中来获取认证。

可以看下图里面的关键往上,clientId,redirectUri 就是我们在上面的原理介绍中包括的, 这里还有一个关键的信息 就是clientSecret 。 因此我们不能随便让人拿到clientId 就能来我们服务器中认证。 我们需要一点案例机制。就是生成一个密钥,告诉第三方的应用,让他请求授权的时候,把这个密钥带上,否则就是非法请求。 其他的参数只是为了管理方便使用。
在这里插入图片描述

第二步: 获取授权码

下面可以看看真实的请求路径

http://127.0.0.1:9999/tboot/oauth2/authorize?username=superman&password=123456&code=560p&client_id=1287990317873762304&redirect_uri=http:%2F%2F127.0.0.1:10001%2F&state=1234

下面看看java代码

  @RequestMapping(value = "/authorize", method = RequestMethod.GET)
    @ApiOperation(value = "认证获取code")
    public Result<Object> authorize(@ApiParam("用户名") @RequestParam String username,
                                    @ApiParam("密码") @RequestParam String password,
                                    @ApiParam("客户端id") @RequestParam String client_id,
                                    @ApiParam("成功授权后回调地址") @RequestParam String redirect_uri,
                                    @ApiParam("授权类型为code") @RequestParam(required = false, defaultValue = "code") String response_type,
                                    @ApiParam("客户端状态值") @RequestParam String state

                                   ){

        Client client = clientService.getById(client_id);

        if(client==null){
            return ResultUtil.error("客户端client_id不存在");
        }

     
        User user = userService.findByUsername(username);
        if(user==null){
            return ResultUtil.error("用户名不存在");
        }
        if(!new BCryptPasswordEncoder().matches(password, user.getPassword())){
            return ResultUtil.error("用户密码不正确");
        }
        // 判断回调地址
        if(!client.getRedirectUri().equals(redirect_uri)){
            return ResultUtil.error("回调地址redirect_uri不正确");
        }
        // 生成code 5分钟内有效
        String code = UUID.randomUUID().toString().replace("-", "");
        // 存入用户及clientId信息
        redisTemplate.opsForValue().set("oauthCode:"+code, new Gson().toJson(new Oauth2TokenInfo(client_id, username)), 5L, TimeUnit.MINUTES);
        Map<String, Object> map = new HashMap<>(16);
        map.put("code", code);
        map.put("redirect_uri", redirect_uri);
        map.put("state", state);
        return ResultUtil.data(map);
    }

第三步: 获取token

这样就返回了授权码,然后夺通过 授权码获取accessToken就算完成了。

查看请求路径

http://127.0.0.1:10001/tboot/oauth2/token?code=3838482b50b1447e81cbf9e52403f629&response_type=code&grant_type=authorization_code&client_id=1287990317873762304&client_secret=f3ede985786a40c2b0d7c341c83af4f3&redirect_uri=http:%2F%2F127.0.0.1:10001%2F

查看java代码

 @RequestMapping(value = "/token", method = RequestMethod.GET)
    @ApiOperation(value = "获取accessToken令牌")
    public Result<Object> token(@ApiParam("授权类型") @RequestParam String grant_type,
                                @ApiParam("客户端id") @RequestParam String client_id,
                                @ApiParam("客户端秘钥") @RequestParam String client_secret,
                                @ApiParam("认证返回的code") @RequestParam(required = false) String code,
                                @ApiParam("刷新token") @RequestParam(required = false) String refresh_token,
                                @ApiParam("成功授权后回调地址") @RequestParam(required = false) String redirect_uri){

        Client client = clientService.getById(client_id);
        if(client==null){
            return ResultUtil.error("客户端client_id不存在");
        }
        // 判断clientSecret
        if(!client.getClientSecret().equals(client_secret)){
            return ResultUtil.error("client_secret不正确");
        }
        Oauth2TokenInfo tokenInfo = null;
        if("authorization_code".equals(grant_type)){
            // 判断回调地址
            if(!client.getRedirectUri().equals(redirect_uri)){
                return ResultUtil.error("回调地址redirect_uri不正确");
            }
            // 判断code 获取用户信息
            String codeValue = redisTemplate.opsForValue().get("oauthCode:"+code);
            if(StrUtil.isBlank(codeValue)){
                return ResultUtil.error("code已过期");
            }
            tokenInfo = new Gson().fromJson(codeValue, Oauth2TokenInfo.class);
            if(!tokenInfo.getClientId().equals(client_id)){
                return ResultUtil.error("code不正确");
            }
        } else if ("refresh_token".equals(grant_type)){
            // 从refreshToken中获取用户信息
            String refreshTokenValue = redisTemplate.opsForValue().get("oauthTokenInfo:"+refresh_token);
            if(StrUtil.isBlank(refreshTokenValue)){
                return ResultUtil.error("refresh_token已过期");
            }
            tokenInfo = new Gson().fromJson(refreshTokenValue, Oauth2TokenInfo.class);
            if(!tokenInfo.getClientId().equals(client_id)){
                return ResultUtil.error("refresh_token不正确");
            }
        } else {
            return ResultUtil.error("授权类型grant_type不正确");
        }

        String token = null, refreshToken = null;
        Long expiresIn = null;
        String tokenKey = "oauthToken:"+client_id+":"+tokenInfo.getUsername(), refreshKey = "oauthRefreshToken:"+client_id+":"+tokenInfo.getUsername();
        if("authorization_code".equals(grant_type)){
            // 生成token模式
            String oldToken = redisTemplate.opsForValue().get(tokenKey);
            String oldRefreshToken = redisTemplate.opsForValue().get(refreshKey);
            if(StrUtil.isNotBlank(oldToken)&&StrUtil.isNotBlank(oldRefreshToken)){
                // 旧token
                token = oldToken;
                refreshToken = oldRefreshToken;
                expiresIn = redisTemplate.getExpire("oauthTokenInfo:"+token, TimeUnit.SECONDS);
            } else {
                // 新生成 30天过期
                String newToken = UUID.randomUUID().toString().replace("-", "");
                String newRefreshToken = UUID.randomUUID().toString().replace("-", "");
                redisTemplate.opsForValue().set(tokenKey, newToken, 30L, TimeUnit.DAYS);
                redisTemplate.opsForValue().set(refreshKey, newRefreshToken, 30L, TimeUnit.DAYS);
                // 新token中存入用户信息
                redisTemplate.opsForValue().set("oauthTokenInfo:"+newToken, new Gson().toJson(tokenInfo),30L, TimeUnit.DAYS);
                redisTemplate.opsForValue().set("oauthTokenInfo:"+newRefreshToken, new Gson().toJson(tokenInfo),30L, TimeUnit.DAYS);
                token = newToken;
                refreshToken = newRefreshToken;
                expiresIn = redisTemplate.getExpire(token, TimeUnit.SECONDS);
            }
        } else if("refresh_token".equals(grant_type)) {
            // 刷新token模式 生成新token 30天过期
            String newToken = UUID.randomUUID().toString().replace("-", "");
            String newRefreshToken = UUID.randomUUID().toString().replace("-", "");
            redisTemplate.opsForValue().set(tokenKey, newToken, 30L, TimeUnit.DAYS);
            redisTemplate.opsForValue().set(refreshKey, newRefreshToken, 30L, TimeUnit.DAYS);
            // 新token中存入用户信息
            redisTemplate.opsForValue().set("oauthTokenInfo:"+newToken, new Gson().toJson(tokenInfo),30L, TimeUnit.DAYS);
            redisTemplate.opsForValue().set("oauthTokenInfo:"+newRefreshToken, new Gson().toJson(tokenInfo),30L, TimeUnit.DAYS);
            token = newToken;
            refreshToken = newRefreshToken;
            expiresIn = redisTemplate.getExpire("oauthTokenInfo:"+token, TimeUnit.SECONDS);
            // 旧refreshToken过期
            redisTemplate.delete("oauthTokenInfo:"+refresh_token);
        }

        Map<String, Object> map = new HashMap<>(16);
        map.put("access_token", token);
        map.put("expires_in", expiresIn);
        map.put("refresh_token", refreshToken);
        return ResultUtil.data(map);
    }

就可以获取相当的信息在这里插入图片描述
在通过 accessTonken 就可去资源服务器去获取相应的用户信息了。

重要

猿来衣舍

这是博主开的淘宝小店,主要经营舒适保暖的服饰,**有纯棉防臭袜子、主题卫衣、恒温发热保暖衣**。欢迎大家选购。一个人能够走多远,关键在于与谁同行,我用跨越山海的一路相伴,希望得到您用金钱的称赞。
猿来衣舍
猿来衣舍
猿来衣舍
猿来衣舍

打开淘宝搜索 “猿来衣舍”这四个字就可以搜到小店,希望大家多多支持。

交个朋友吧

在这里插入图片描述

  • 13
    点赞
  • 150
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值