Sa-Token OAuth2 单点登陆实战(适用于spring boot + spring cloud)及常见问题解答

前言:

看本文前,请熟悉OAuth2的的基本概念及原理。本文主要讲解授权码模式的实战。同时请仔细阅读官方文档。

官方文档地址:OAuth2.0简述 - Sa-Token (dev33.cn)

Sa-Token OAuth2 可以帮你把oauth2的事情都搞定,你只需要实现自己的用户认证(doLogin方法)即可!不得不为作者是设计大大的点个赞!

搭建OAuth2-Server

注意:官方文档已经讲的非常明白。请按照官方文档流程搭建服务。本文会把一些需要额外注意的操作列举出来。

1.引入依赖:

注意:此处相比官方文档,额外引入了 sa-token-dao-redis-jackson 和 common-pool2  相关依赖。实际开发及生产环境,也请加入该依赖。实际版本号,建议查阅官方最新版本号。(确保授权码和用户token的重启服务后丢失,以及解决无法在分布式环境中共享数据的问题)。



    cn.dev33
    sa-token-spring-boot-starter
    1.31.0




    cn.dev33
    sa-token-oauth2
    1.31.0




    cn.dev33
    sa-token-dao-redis-jackson
    1.31.0


    org.apache.commons
    commons-pool2
    2.11.1

2.开放服务:

新建 SaOAuth2TemplateImpl

/**
 * Sa-Token OAuth2.0 整合实现 
 */
@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {

    // 根据 id 获取 Client 信息 
    @Override
    public SaClientModel getClientModel(String clientId) {
        // 此为模拟数据,真实环境需要从数据库查询 
        if("1001".equals(clientId)) {
            return new SaClientModel()
                    .setClientId("10001")
                    .setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
                    .setAllowUrl("*")
                    .setContractScope("userinfo")
                    .setIsAutoMode(true);
        }
        return null;
    }

    // 根据ClientId 和 LoginId 获取openid 
    @Override
    public String getOpenid(String clientId, Object loginId) {
        // 此为模拟数据,真实环境需要从数据库查询 
        return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
    }

}

你可以在 框架配置 了解有关 SaClientModel 对象所有属性的详细定义

3.新建controller

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Handle;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.cloud.base.springcloudbaseserveroauth.pojo.request.LoginRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * oAuth2 Server端 controller
 *
 * @author ZT
 */
@Slf4j
@RestController
@Api(value = "oAuth2 相关接口",tags = "oAuth2 相关接口")
public class Oauth2ServerController {

    /**
     * 统一认证地址
     */
    @RequestMapping("/oauth2/authorize")
    @ApiOperation(value = "统一认证地址",notes = "统一认证地址", produces = "application/json")
    public Object authorize() {
        log.info("------- 进入【统一认证地址】请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.authorize(SaHolder.getRequest(), SaHolder.getResponse(), SaOAuth2Manager.getConfig());
    }

    /**
     * 获取 Access-Token
     */
    @RequestMapping("/oauth2/token")
    @ApiOperation(value = "获取 Access-Token",notes = "获取 Access-Token", produces = "application/json")
    public Object token() {
        log.info("------- 进入【获取 Access-Token】请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.token(SaHolder.getRequest(), SaHolder.getResponse(), SaOAuth2Manager.getConfig());
    }

    /**
     * 刷新 Access-Token
     */
    @RequestMapping("/oauth2/refresh")
    @ApiOperation(value = "刷新 Access-Token",notes = "刷新 Access-Token", produces = "application/json")
    public Object refresh() {
        log.info("------- 进入【刷新 Access-Token】请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.refreshToken(SaHolder.getRequest());
    }

    /**
     * 回收 Access-Token
     */
    @RequestMapping("/oauth2/revoke")
    @ApiOperation(value = "回收 Access-Token",notes = "回收 Access-Token", produces = "application/json")
    public Object revoke() {
        log.info("------- 进入【回收 Access-Token】请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.revokeToken(SaHolder.getRequest());
    }

    /**
     * 登录接口
     */
    @RequestMapping("/oauth2/doLogin")
    @ApiOperation(value = "登录接口",notes = "登录接口", produces = "application/json")
    public Object doLogin(LoginRequest loginRequest) {
        log.info("------- 进入请求: " + SaHolder.getRequest().getUrl());
        // 注意,此处是一个Demo写法,判断登陆类型为1通过,其它都为false
        // 实际生产中。我们的登陆是支持多种登陆方式的。例如:账号+密码,手机—+验证码,扫码登陆,SDK唤醒
        if ("1".equals(loginRequest.getLoginType())) {
            StpUtil.login("10001");
            return null;
        } else if ("2".equals(loginRequest.getLoginType())) {
            throw new RuntimeException("登陆失败");
        }

        return new RuntimeException("无效的请求,请查收");
    }

    /**
     * 确认授权接口
     */
    @RequestMapping("/oauth2/doConfirm")
    @ApiOperation(value = "确认授权接口",notes = "确认授权接口", produces = "application/json")
    public Object doConfirm() {
        log.info("------- 进入【确认授权接口】请求: " + SaHolder.getRequest().getUrl());
        return SaOAuth2Handle.doConfirm(SaHolder.getRequest());
    }

    /**
     * Sa-OAuth2 定制化配置
     */
    @Autowired
    public void setSaOAuth2Config(SaOAuth2Config cfg) {
        cfg.
                // 配置:未登录时返回的View
                        setNotLoginView(() -> {
                    String msg = "当前会话在SSO-Server端尚未登录,请先访问"
                            + " doLogin登录 "
                            + "进行登录l之后,刷新页面开始授权";
                    return msg;
                }).
//                // 此处是官方demo配置:登录处理函数。我们实际是自定义登陆方法
//                        setDoLoginHandle((name, pwd) -> {
//                    if ("sa".equals(name) && "123456".equals(pwd)) {
//                        StpUtil.login(10001);
//                        return SaResult.ok();
//                    }
//                    return SaResult.error("账号名或密码错误");
//                }).
                // 配置:确认授权时返回的View
                        setConfirmView((clientId, scope) -> {
                    String msg = "

应用 " + clientId + " 请求授权:" + scope + "

" + "

请确认: 确认授权

" + "

确认之后刷新页面

"; return msg; }) ; } /** * 获取Userinfo信息:昵称、头像、性别等等 */ @RequestMapping("/oauth2/userinfo") public SaResult userinfo() { // 获取 Access-Token 对应的账号id String accessToken = SaHolder.getRequest().getParamNotNull("access_token"); Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken); log.info("-------- 此Access-Token对应的账号id: " + loginId); // 校验 Access-Token 是否具有权限: userinfo SaOAuth2Util.checkScope(accessToken, "userinfo"); // 模拟账号信息 (真实环境需要查询数据库获取信息) Map map = new LinkedHashMap(); map.put("nickname", "shengzhang_"); map.put("avatar", "http://xxx.com/1.jpg"); map.put("age", "18"); map.put("sex", "男"); map.put("address", "山东省 青岛市 城阳区"); return SaResult.data(map); } }

4.配置文件

# Sa-Token 配置
sa-token:
  token-name: satoken-server
  # OAuth2.0 配置
  oauth2:
    # 是否打开模式:授权码(Authorization Code)
    is-code: true
    # 是否打开模式:隐藏式(Implicit)
    is-implicit: false
    # 是否打开模式:密码式(Password)
    is-password: false
    # 是否打开模式:凭证式(Client Credentials)
    is-client: false
    # 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token
    isNewRefresh: false
    # Code授权码 保存的时间(单位:秒) 默认五分钟
    codeTimeout: 300
    # Access-Token 保存的时间(单位:秒)默认两个小时
    accessTokenTimeout: 7200
    # Refresh-Token 保存的时间(单位:秒) 默认30 天
    refreshTokenTimeout: 2592000
    # Client-Token 保存的时间(单位:秒) 默认两个小时
    clientTokenTimeout: 7200
    # Past-Client-Token 保存的时间(单位:秒) ,默认为-1,代表延续 Client-Token 的有效时间
    pastClientTokenTimeout: 7200

5. 启动项目

6.测试

6.1 由于暂未搭建Client端,我们可以使用百度官网作为重定向URL进行测试:

ip+端口+你baseUrl+/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com&scope=userinfo

6.2 由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图

6.3 访问doLogin接口进行登录之后

ip+端口+你的baseUrl+/oauth2/doLogin?loginType=1

6.4刷新<6.2>步骤的页面

Sa-Token OAuth2 单点登陆实战(适用于spring boot + spring cloud)及常见问题解答_第1张图片

6.5 点击确认授权

6.6 刷新<6.4>步骤的页面,我们会被重定向至 redirect_uri 页面,并携带了code参数

​  

 6.7 我们拿着code参数,访问以下地址:

ip+端口+你baseUrl+/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}

 将得到 Access-TokenRefresh-Tokenopenid等授权信息

Sa-Token OAuth2 单点登陆实战(适用于spring boot + spring cloud)及常见问题解答_第2张图片

 测试完毕

常见问题

问:我想静默授权,不希望用户点击授权,怎么办?

答:scope参数为空,或者请求的权限用户近期已确认过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放code授权码。

问:哪些功能是封装好的,哪些是核心需要实现的?

答:/oauth2/doLogin()  是需要我们核心实现的方法,其它的/oauth2/xxx方法,交给框架处理。

问:我看官方Demo,一个 @RequestMapping("/oauth2/*") 把请求全部处理了,你这里拆开怎么说?

答:在 cn.dev33.satoken.oauth2.logic 包下的 SaOAuth2Consts 类内,定义了所有的Api接口,所有的参数名称,所有的返回类型,所有的授权类型。官方demo用/oauth2/*处理了所有请求,我们参考源码,其实每个api接口都有自己对应的方法调用。所以,我们可以自定义接口,自定接口Url,只需要调用对应的Api方法即可(参考本文调用的各方法)。

问:授权码签发成功后,签发jwt可以么?

答:不合适。jwt是用户在 oauth-server端的敏感数据,不应该透露给client端。

问:授权码签发成功后,怎么让用户持续交互呢?不能一直拿access_token + open_id 一直交互吧?

答:OAuth-Client 的后端调用 OAuth-Server 的后端,用 Access-Token。OAuth-Client 的前端调用 OAuth-Client 的后端,根据openid找到userId,然后根据userId生成的token(StpUtil.login(userId))。简单来说,oauth2本身就是授权签发用户非敏感信息,不是做长交互的。

问: OAuth-Client 端在自己的数据库中创建的用户,绑定了open_id后,怎么交给 OAuth-Server 的后端去识别验证用户呢?

答:client的用户为啥要交给OAuth-Server端来验证呢?我们用QQ快速登陆了CSDN,难道CSDN判断是否登陆,要从QQ服务器验证用户合法性么?

你可能感兴趣的:(Java,sa-token,oauth2,spring,boot,spring,cloud,spring)