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

1 篇文章 1 订阅
1 篇文章 0 订阅

前言:

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

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

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

搭建OAuth2-Server

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

1.引入依赖:

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

<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.31.0</version>
</dependency>

<!-- Sa-Token-OAuth2.0 模块 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-oauth2</artifactId>
    <version>1.31.0</version>
</dependency>

<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-dao-redis-jackson</artifactId>
    <version>1.31.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

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端尚未登录,请先访问"
                            + "<a href='http://localhost:8099/gateway/service-oauth/oauth2/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登录 </a>"
                            + "进行登录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 = "<p>应用 " + clientId + " 请求授权:" + scope + "</p>"
                            + "<p>请确认:<a href='http://localhost:8099/gateway/service-oauth/oauth2/doConfirm?client_id=" + clientId + "&scope=" + scope + "' target='_blank'> 确认授权 </a></p>"
                            + "<p>确认之后刷新页面</p>";
                    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<String, Object> map = new LinkedHashMap<String, Object>();
        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>步骤的页面

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等授权信息

 测试完毕

常见问题

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

答: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服务器验证用户合法性么?

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
集成 sa-token 可以让 Spring Boot 应用快速实现权限认证、RBAC、SSO、踢人下线等功能。下面是集成的步骤: 1. 引入 sa-token 的依赖 在 pom.xml 文件中添加以下依赖: ```xml <dependency> <groupId>cn.dev33.satoken</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.22.1-RELEASE</version> </dependency> ``` 2. 配置 sa-token 在 application.properties 或 application.yml 文件中添加以下配置: ```yaml # sa-token 配置 sa-token: # token 持久化类型:0-内存版、1-redis版、2-mongodb版、3-jwt版 store-type: 1 # redis 配置 redis: mode: standalone host: 127.0.0.1 port: 6379 database: 0 timeout: 0 # 其他配置... ``` 其中,store-type 表示 sa-token 的持久化类型,可以选择内存版、redis版、mongodb版或 jwt 版。此处选择了 redis 版。 3. 开启 sa-tokenSpring Boot 应用启动类上加上 @SaTokenApplication 注解即可开启 sa-token 的功能: ```java @SpringBootApplication @SaTokenApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` 4. 使用 sa-token 现在,我们可以在代码中使用 sa-token 提供的 API 来实现权限认证、RBAC、SSO、踢人下线等功能了。比如: ```java @RestController public class MyController { // 登录接口 @PostMapping("/login") public String login(String account, String password) { // 模拟登录 if ("admin".equals(account) && "123456".equals(password)) { // 登录成功,生成token String token = SaTokenManager.createToken(account); // 返回token return token; } else { // 登录失败 return "账号或密码错误"; } } // 需要登录才能访问的接口 @GetMapping("/user") public String user() { // 获取当前登录账号 String account = SaTokenManager.getAccount(); // 返回当前登录账号 return "当前登录账号:" + account; } // 需要权限才能访问的接口 @GetMapping("/admin") public String admin() { // 检查是否具有admin角色 boolean isAdmin = SaTokenManager.hasRole("admin"); if (isAdmin) { return "欢迎管理员访问"; } else { return "没有访问权限"; } } // 注销登录接口 @PostMapping("/logout") public String logout() { // 注销登录 SaTokenManager.logout(); return "注销成功"; } } ``` 以上是使用 sa-token 的基本示例,你可以根据自己的需求,使用 sa-token 提供的更多 API 实现更复杂的功能。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值