Spring Security OAuth2 微服務認證中心自定義授權模式擴充套件以及常見登入認證場景下的應用實戰

有來技術團隊發表於2021-10-18

一. 前言

【APP 移動端】Spring Security OAuth2 手機簡訊驗證碼模式 【微信小程式】Spring Security OAuth2 微信授權模式
Spring Security OAuth2 微服務認證中心自定義授權模式擴充套件以及常見登入認證場景下的應用實戰 Spring Security OAuth2 微服務認證中心自定義授權模式擴充套件以及常見登入認證場景下的應用實戰
【管理系統】Spring Security OAuth2 密碼模式 【管理系統】Spring Security OAuth2 驗證碼模式
Spring Security OAuth2 微服務認證中心自定義授權模式擴充套件以及常見登入認證場景下的應用實戰 Spring Security OAuth2 微服務認證中心自定義授權模式擴充套件以及常見登入認證場景下的應用實戰

Spring Security OAuth2 預設實現的四種授權模式在實際的應用場景中往往滿足不了預期,如以下需求:

  1. 授權物件分多個使用者體系,例如系統使用者和會員使用者;
  2. 在密碼授權模式的基礎上加個驗證碼校驗;
  3. 基於 Spring Security OAuth2 實現手機和簡訊驗證碼登入;
  4. 基於 Spring Security OAuth2 實現微信小程式授權登入。

相信你會遇到但不僅限上面的場景,網上也有很多對 Spring Security OAuth2 授權模式擴充套件的相關文章,但多少有不全面和實現複雜的通病,一度會讓你覺得 Spring Security OAuth2 很難, Spring 在實現核心功能基礎上同時還提供了很多的擴充套件點,Spring Security OAuth2 亦是如此,相信這篇文章會幫助消除它很難的誤解。

本篇將以實戰為主,原理為輔的方式,本著全面最少改動的原則去對 Spring Security OAuth2 授權模式的擴充套件,本篇涉及內容如下:

  1. Spring Cloud Gateway 微服務閘道器WebFlux整合谷歌驗證碼 Kaptcha
  2. SpringBoot 整合阿里雲SMS簡訊服務;
  3. Spring Security OAuth2 認證授權模式底層原始碼分析;
  4. Spring Security OAuth2 擴充套件驗證碼授權模式;
  5. Spring Security OAuth2 擴充套件手機簡訊驗證碼授權模式;
  6. Spring Security OAuth2 擴充套件微信授權模式;
  7. Spring Security OAuth2 多使用者體系重新整理模式;
  8. vue-element-admin 後臺管理前端登入接入驗證碼授權模式
  9. uni-app 微信小程式登入接入微信授權模式
  10. uni-app H5、移動端手機驗證碼登入接入手機簡訊驗證碼授權模式

? 先做個很重要的宣告吧,本篇文章涉及所有的程式碼地址:

專案名稱 碼雲(Gitee) GitHub
微服務後臺 youlai-mall youlai-mall
管理前端 mall-admin-web mall-admin-web
微信小程式/H5/Android/IOS mall-app mall-app

因為涉及的內容很多,文章中做不到把所有的程式碼完全貼出來,但是放心原始碼全部線上的,同樣文件也是

往期系列文章

微服務

  1. Spring Cloud實戰 | 第一篇:Windows搭建Nacos服務
  2. Spring Cloud實戰 | 第二篇:Spring Cloud整合Nacos實現註冊中心
  3. Spring Cloud實戰 | 第三篇:Spring Cloud整合Nacos實現配置中心
  4. Spring Cloud實戰 | 第四篇:Spring Cloud整合Gateway實現API閘道器
  5. Spring Cloud實戰 | 第五篇:Spring Cloud整合OpenFeign實現微服務之間的呼叫
  6. Spring Cloud實戰 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT實現微服務統一認證授權
  7. Spring Cloud實戰 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2整合統一認證授權平臺下實現登出使JWT失效方案
  8. Spring Cloud實戰 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前後端分離模式下無感知重新整理實現JWT續期
  9. Spring Cloud實戰 | 最九篇:Spring Security OAuth2認證伺服器統一認證自定義異常處理
  10. Spring Cloud實戰 | 第十篇 :Spring Cloud + Nacos整合Seata 1.4.1最新版本實現微服務架構中的分散式事務,進階之路必須要邁過的檻
  11. Spring Cloud實戰 | 第十一篇 :Spring Cloud Gateway閘道器實現對RESTful介面許可權和按鈕許可權細粒度控制
  12. Spring Cloud實戰 | 第十二篇:Sentinel+Nacos實現流控、熔斷降級,賦能擁有降級功能的Feign新技能熔斷,做到熔斷降級雙劍合璧
  13. Spring Cloud實戰 | 總結篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT 實現微服務統一認證授權和鑑權

中介軟體

  1. SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分散式日誌解決方案,開源微服務全棧專案【有來商城】的日誌落地實踐

管理系統前端

  1. vue-element-admin實戰 | 第一篇: 移除mock接入微服務介面,搭建SpringCloud+Vue前後端分離管理平臺
  2. vue-element-admin實戰 | 第二篇: 最小改動接入後臺實現根據許可權動態載入選單

微信小程式

  1. vue+uni-app商城實戰 | 第一篇:從0到1快捷開發一個商城微信小程式,無縫接入Spring Cloud OAuth2實現一鍵授權登入

應用部署

  1. Docker實戰 | 第一篇:Linux 安裝 Docker

  2. Docker實戰 | 第二篇:Docker部署nacos-server:1.4.0

  3. Docker實戰 | 第三篇:IDEA 整合 Docker 外掛實現一鍵遠端部署 SpringBoot 應用,無需三方依賴,開源微服務全棧有來商城線上部署方式

  4. Docker實戰 | 第四篇:Docker安裝Nginx,實現基於vue-element-admin框架構建的專案線上部署

二. 驗證碼授權模式

1. 原理

驗證碼授權模式是在密碼模式基礎新增個驗證碼校驗,如果你有 不管功夫怎樣,能打贏你的就是好功夫 這樣的心態完全可以使用過濾器實現,但如果想不開的話那就試下擴充套件吧。

因為是基於密碼授權模式的擴充套件,就先了解密碼授權模式的流程吧。因為其他幾種授權模式和密碼模式實現原理都是一樣,弄明白密碼授權模式之後其他授權模式包括如何去擴充套件都是輕車熟路。

密碼模式流程: 根據請求引數 grant_type 的值 password 匹配到授權者 ResourceOwnerPasswordTokenGraner ,授權者委託給認證提供者管理器 ProviderManager,根據 token 型別匹配到提供者 DaoAuthenticationProviderProvider 從資料庫獲取使用者認證資訊和客戶端請求傳值的使用者資訊進行認證密碼判讀,驗證通過之後返回token給客戶端。

下面密碼授權模式時序圖貼出關鍵類和方法,斷點走幾遍流程就應該知道流程。

驗證碼授權模式時序圖如下,仔細比對下和密碼授權模式的區別。

比較可知兩者的區別基本就是授權者 Granter 的區別,後續的 Provider 獲取使用者認證資訊和密碼判斷完全一致,具體新增的驗證碼模式授權者 CaptchaTokenGranter 和密碼模式的授權者 ResourceOwnerPasswordTokenGraner 區別在於前者的 getOAuth2Authentication() 方法獲取認證資訊新增了校驗驗證碼的邏輯,具體的程式碼實現在實戰裡交待。

2. 實戰

驗證碼授權模式涉及Spring Security OAuth2擴充套件驗證碼授權模式、後臺生成驗證碼和前端登入加入驗證碼三部分,涉及到前後端的東西,針對自己需要選擇關注點即可。

2.1 驗證碼授權模式擴充套件

從原理得知只需重寫 Granter 為其新增校驗驗證碼的能力,所以複製密碼模式的授權者 ResourceOwnerPasswordTokenGranter 然後重名為 CaptchaTokenGranter,稍加改動成為驗證碼模式的授權者。

CaptchaTokenGranter
/**
 * 驗證碼授權模式 授權者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class CaptchaTokenGranter extends AbstractTokenGranter {

    /**
     * 宣告授權者 CaptchaTokenGranter 支援授權模式 captcha
     * 根據介面傳值 grant_type = captcha 的值匹配到此授權者
     * 匹配邏輯詳見下面的兩個方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "captcha";
    private final AuthenticationManager authenticationManager;
    private StringRedisTemplate redisTemplate;

    public CaptchaTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
                               StringRedisTemplate redisTemplate
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        // 驗證碼校驗邏輯
        String validateCode = parameters.get("validateCode");
        String uuid = parameters.get("uuid");

        Assert.isTrue(StrUtil.isNotBlank(validateCode), "驗證碼不能為空");
        String validateCodeKey = AuthConstants.VALIDATE_CODE_PREFIX + uuid;
        
        // 從快取取出正確的驗證碼和使用者輸入的驗證碼比對
        String correctValidateCode = redisTemplate.opsForValue().get(validateCodeKey);
        if (!validateCode.equals(correctValidateCode)) {
            throw new BizException("驗證碼不正確");
        } else {
            redisTemplate.delete(validateCodeKey);
        }

        String username = parameters.get("username");
        String password = parameters.get("password");

        // 移除後續無用引數
        parameters.remove("password");
        parameters.remove("validateCode");
        parameters.remove("uuid");

        // 和密碼模式一樣的邏輯
        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
    }
}

上面相對密碼模式的授權者做了兩處改動,總結如下:

  1. 修改 GRANT_TYPE 的值 passwordcaptcha;
  2. getOAuth2Authentication() 方法新增驗證碼校驗邏輯。
AuthorizationServerConfig

在 AuthorizationServerConfig 配置類重寫 TokenGranter 讓其支援新增的驗證碼模式授權者 CaptchaTokenGranter

image-20211010231930665

到此,Spring Security OAuth2 擴充套件驗證碼授權大功告成!!!

怎麼樣,簡不簡單?相信你有可能心存懷疑,那先做個測試吧。

管理前端的客戶端ID是 mall-admin-web ,在測試之前,先賦予客戶端支援驗證碼模式。

image-20211010225823266

在登入介面輸入錯誤的驗證碼和正確的驗證碼各一次看下效果,是不是能達到預期的效果,還有驗證碼如何生成和前端如何傳值放在後文說。

2.2 Spring WebFlux 整合驗證碼 Kaptcha

驗證碼生成的功能主要是生成一個隨機碼將其快取redis,返回redis的key標識(一般是uuid)和隨機碼的圖片給前端。因為沒有任何業務邏輯,故這裡直接放在閘道器,除了利用 WebFlux 效能優勢之外還能減少一次轉發。youlai-gateway 驗證碼相關程式碼結構圖如下:

image-20211010171329855

CaptchaHandler
@Component
@RequiredArgsConstructor
public class CaptchaHandler implements HandlerFunction<ServerResponse> {

    private final Producer producer;
    private final StringRedisTemplate redisTemplate;

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest) {
        // 生成驗證碼
        String capText = producer.createText();
        String capStr = capText.substring(0, capText.lastIndexOf("@"));
        String code = capText.substring(capText.lastIndexOf("@") + 1);
        BufferedImage image = producer.createImage(capStr);
        // 快取驗證碼至Redis
        String uuid = IdUtil.simpleUUID();
        redisTemplate.opsForValue().set(AuthConstants.VALIDATE_CODE_PREFIX + uuid, code, 60, TimeUnit.SECONDS);
        // 轉換流資訊寫出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", os);
        } catch (IOException e) {
            return Mono.error(e);
        }

        java.util.Map resultMap = new HashMap<String, String>();
        resultMap.put("uuid", uuid);
        resultMap.put("img", Base64.encode(os.toByteArray()));

        return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(Result.success(resultMap)));
    }
}
CaptchaConfig

屬性 kaptcha.textproducer.impl 需要指定你自己專案文字生成器 KaptchaTextCreator 的類路徑

// 驗證碼文字生成器 
properties.setProperty("kaptcha.textproducer.impl", "com.youlai.gateway.kaptcha.KaptchaTextCreator");
CaptchaRouter
@Configuration
public class CaptchaRouter {

    @Bean
    public RouterFunction<ServerResponse> routeFunction(CaptchaHandler captchaHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/captcha")
                        .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), captchaHandler::handle);
    }
}
驗證碼測試

修改 Nacos 閘道器配置檔案 youlai-gateway.yaml 白名單新增請求路徑 /captcha

訪問 http://localhost:9999/captcha 如下:

image-20211010190718964

2.3 前端登入接入驗證碼模式

登入頁面

登入表單新增驗證碼,完整程式碼地址:mall-admin-web

src/views/login/index.vue

 <el-form-item prop="validateCode">
    <span class="svg-container">
       <svg-icon icon-class="validCode"/>
     </span>
   <el-input
     v-model="loginForm.validateCode"
     auto-complete="off"
     placeholder="請輸入驗證碼"
     style="width: 65%"
     @keyup.enter.native="handleLogin"
   />
   <div class="validate-code">
     <img :src="captchaUrl" @click="getValidateCode" height="38px"/>
   </div>
 </el-form-item>

返回的圖片是base64 加密後的字串,所以新增字首 data:image/gif;base64,

// 獲取驗證碼
getValidateCode() {
  getCaptcha().then(response => {
	const {img, uuid} = response.data
	this.captchaUrl = "data:image/gif;base64," + img
	this.loginForm.uuid = uuid;
  })
}
介面請求

src/store/modules/user.js 設定請求引數

login({commit}, userInfo) {
  const {username, password, validateCode, uuid} = userInfo
  return new Promise((resolve, reject) => {
    login({  
      username: username,
      password: password,
      grant_type: 'captcha', // 授權模式指定為 captcha 驗證碼模式,原先為 password 密碼模式
      uuid: uuid, // 從Redis獲取正確驗證碼的標識
      validateCode: validateCode // 驗證碼
    }).then(response => {
      const {access_token, refresh_token, token_type} = response.data
      const token = token_type + " " + access_token
      commit('SET_TOKEN', token)
      setToken(token)
      setRefreshToken(refresh_token)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })

src/api/user.js 請求API設定請求頭部

export function login(params) {
  return request({
    url: '/youlai-auth/oauth/token',
    method: 'post',
    params: params,
    headers: {
      'Authorization': 'Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' // OAuth2客戶端資訊Base64加密,明文:mall-admin-web:123456
    }
  })
}

三. 手機簡訊驗證碼授權模式

1. 原理

手機簡訊驗證碼模式時序圖如下,變動的角色還是用綠色背景標識。可以看到擴充套件是對授權者 Granter 和認證提供者 Provider 做切入口。

手機簡訊驗證碼授權流程: 流程基本上和密碼模式一致,根據 grant_type 匹配授權者 SmsCodeTokenGranter , 委託給 ProviderManager 進行認證,根據 SmsCodeAuthenticationToken的匹配認證提供者 SmsCodeAuthenticationProvider 進行簡訊驗證碼校驗。

2. 實戰

2.1 手機簡訊驗證碼授權模式擴充套件

SmsCodeTokenGranter
/**
 * 手機驗證碼授權者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class SmsCodeTokenGranter extends AbstractTokenGranter {

    /**
     * 宣告授權者 CaptchaTokenGranter 支援授權模式 sms_code
     * 根據介面傳值 grant_type = sms_code 的值匹配到此授權者
     * 匹配邏輯詳見下面的兩個方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "sms_code";
    private final AuthenticationManager authenticationManager;

    public SmsCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        String mobile = parameters.get("mobile"); // 手機號
        String code = parameters.get("code"); // 簡訊驗證碼

        parameters.remove("code");

        Authentication userAuth = new SmsCodeAuthenticationToken(mobile, code);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + mobile);
        }
    }
}

SmsCodeAuthenticationProvider
/**
 * 簡訊驗證碼認證授權提供者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private MemberFeignClient memberFeignClient;
    private StringRedisTemplate redisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCredentials();

        String codeKey = AuthConstants.SMS_CODE_PREFIX + mobile;
        String correctCode = redisTemplate.opsForValue().get(codeKey);
        // 驗證碼比對
        if (StrUtil.isBlank(correctCode) || !code.equals(correctCode)) {
            throw new BizException("驗證碼不正確");
        } else {
            redisTemplate.delete(codeKey);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByMobile(mobile);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

AuthorizationServerConfig

在認證中心配置把 SmsCodeTokenGranter 新增到認證器的授權型別的集合中去。

image-20211013010613282

2.2 阿里雲免費簡訊申請

訪問 https://free.aliyun.com/product/cloudcommunication-free-trial?spm=5176.10695662.1128094.7.2a6b4bee30xtJx 申請阿里雲免費簡訊試用

image-20211012083759204

新增簽名,等待稽核通過

image-20211005223935872

簽名稽核通過之後就可以建立 AccessKey 訪問金鑰

image-20211006083311766

image-20211012201848000

新增模板, 國內訊息 → 模板管理 → 新增模板

image-20211006100631323

簽名稽核通過後得到 AccessKey 和 模板稽核通過得到模板CODE,接下來就可以進行專案整合了。

2.3 SpringBoot 整合阿里雲 SMS 簡訊

SpringBoot 整合 SMS 網上教程很多,這裡不畫蛇添足,接下來簡單說下 youlai-mall 整合阿里雲 SMS 簡訊。完整原始碼

按慣例把簡訊封裝成一個公共模組以便給其他需要簡訊的應用模組引用。

image-20211014225225501

youlai-auth 引入 common-sms 依賴

<dependencies> 
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>common-sms</artifactId>
    </dependency>
</dependencies>

其中 AliyunSmsProperties 需要的屬性需要配置在 Nacos 的配置中心檔案 youlai-auth.yaml

# 阿里雲簡訊配置
aliyun:
  sms:
    accessKeyId: LTAI5tSxxxxxxNcD6diBJLyR
    accessKeySecret: SoOWRqpjtSxxxxxxM8QZ2PZiMTJOVC
    domain: dysmsapi.aliyuncs.com 
    regionId: cn-shanghai
    templateCode: SMS_225xxx770
    signName: 有來技術

傳送簡訊驗證碼介面

@Api(tags = "簡訊驗證碼")
@RestController
@RequestMapping("/sms-code")
@RequiredArgsConstructor
public class SmsCodeController {

    private final AliyunSmsService aliyunSmsService;

    @ApiOperation(value = "傳送簡訊驗證碼")
    @ApiImplicitParam(name = "phoneNumber", example = "17621590365", value = "手機號", required = true)
    @PostMapping
    public Result sendSmsCode(String phoneNumber)  {
        boolean result = aliyunSmsService.sendSmsCode(phoneNumber);
        return Result.judge(result);
    }
}

2.4 移動端接入簡訊驗證碼授權模式

有來移動端 mall-app 使用 uni-app 跨平臺應用的前端框架。因為一直以來有來商城都是以微信小程式的一個端呈現,所以 uni-app 的強大之處沒法體現。藉著這次給 mall-app 擴充套件手機簡訊驗證碼的授權模式的機會,為 H5、Android和IOS 新增手機簡訊驗證碼的登入介面。

先看下 mall-app 登入介面 在H5/Android/IOS 和 微信小程式的不同呈現效果。

H5/Android/IOS 登入介面 微信小程式 登入介面
image-20211015003635666 image-20211015003730144

登入頁面 /pages/login/login.vue 在不同的平臺有不同的呈現實現原理是通過 #ifdef MP #ifndef MP 條件編譯指令實現的,其中 #ifdef MP 是在小程式平臺編譯生效,而 #ifdef MP 是非小程式平臺編譯生效。

在開發編譯時,當在 HBuilderX 工具欄點選執行選擇不同的平臺會有不同的頁面呈現。

  1. 執行 → 執行到內建瀏覽器 → 手機簡訊驗證碼登入介面;
  2. 執行 → 執行到小程式模擬器 → 微信開發者工具 → 小程式授權登入介面;

說到接入 Spring Security OAuth2 擴充套件的手機簡訊驗證碼,重要的還是看如何傳參。在 mall-app/api/user.js 程式碼:

// H5/Android/IOS 手機簡訊驗證碼登入
// #ifndef MP
export function login( mobile,code) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			mobile: mobile,
			code: code,
			grant_type: 'sms_code'
		},
		headers: {
			'Authorization': 'Basic bWFsbC1hcHA6MTIzNDU2' // 客戶端資訊Base64加密,明文:mall-app:123456
		}
	})
}
// #endif

賦予mall-app 客戶端支援 sms_code 模式

image-20211015010521830

3. 測試

到此H5/Android/IOS移動端接入 Spring Security OAuth2 擴充套件的手機簡訊驗證碼授權模式已經完成。接下來擴充套件的授權模式是針對當下最火的微信小程式移動端的授權登入。

四. 微信授權模式

1. 原理

微信小程式登入授權流程圖如下,我們所扮演的角色是 開發者伺服器,主要的工作是接收小程式端的 code 從微信伺服器獲取 openidsession_key 後在開發者伺服器生成會話(token)返回給小程式,後續小程式攜帶token和開發者伺服器進行互動,也就沒有微信伺服器啥事了。

img

Spring Security OAuth2 微信授權擴充套件和上面的手機簡訊驗證碼原理一樣,新增授權者 WechatTokenGranter 構建 WechatAuthenticationToken , 匹配到認證提供者 WechatAuthenticationProvider ,在其 authenticate 方法完成認證授權邏輯。

2. 實戰

2.1 微信授權模式擴充套件

WechatTokenGranter

WechatTokenGranter 微信授權者接收 codeencryptedDataiv 構建 WechatAuthenticationToken

/**
 *  微信授權者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class WechatTokenGranter extends AbstractTokenGranter {

    /**
     * 宣告授權者 CaptchaTokenGranter 支援授權模式 wechat
     * 根據介面傳值 grant_type = wechat 的值匹配到此授權者
     * 匹配邏輯詳見下面的兩個方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "wechat";
    private final AuthenticationManager authenticationManager;

    public WechatTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String code = parameters.get("code");
        String encryptedData = parameters.get("encryptedData");
        String iv = parameters.get("iv");

        parameters.remove("code");
        parameters.remove("encryptedData");
        parameters.remove("iv");

        Authentication userAuth = new WechatAuthenticationToken(code, encryptedData,iv); // 未認證狀態
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth); // 認證中
        } catch (Exception e) {
            throw new InvalidGrantException(e.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) { // 認證成功
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else { // 認證失敗
            throw new InvalidGrantException("Could not authenticate code: " + code);
        }
    }
}
WechatAuthenticationProvider

最終在微信認證提供者的 authenticate() 方法裡完成認證邏輯,成功返回token。

/**
 * 微信認證提供者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class WechatAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private WxMaService wxMaService;
    private MemberFeignClient memberFeignClient;

    /**
     * 微信認證
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        WechatAuthenticationToken authenticationToken = (WechatAuthenticationToken) authentication;
        String code = (String) authenticationToken.getPrincipal();

        WxMaJscode2SessionResult sessionInfo = null;
        try {
            sessionInfo = wxMaService.getUserService().getSessionInfo(code);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        String openid = sessionInfo.getOpenid();
        Result<MemberAuthDTO> memberAuthResult = memberFeignClient.loadUserByOpenId(openid);
        // 微信使用者不存在,註冊成為新會員
        if (memberAuthResult != null && ResultCode.USER_NOT_EXIST.getCode().equals(memberAuthResult.getCode())) {

            String sessionKey = sessionInfo.getSessionKey();
            String encryptedData = authenticationToken.getEncryptedData();
            String iv = authenticationToken.getIv();
            // 解密 encryptedData 獲取使用者資訊
            WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, iv);

            UmsMember member = new UmsMember();
            BeanUtil.copyProperties(userInfo, member);
            member.setOpenid(openid);
            member.setStatus(GlobalConstants.STATUS_YES);
            memberFeignClient.add(member);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByOpenId(openid);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return WechatAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

2.2 微信小程式接入微信授權模式

同樣是在 mall-app 的介面檔案中 /api/user.js,先讓我們看下小程式端如何傳值?

// 小程式授權登入
// #ifdef MP
export function login(code, encryptedData,iv) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			code: code,
			encryptedData: encryptedData,
			iv:iv,
			grant_type: 'wechat'
		},
		headers: {
			'Authorization': 'Basic bWFsbC13ZWFwcDoxMjM0NTY=' // 客戶端資訊Base64加密,明文:mall-weapp:123456
		}
	})
}
// #endif

設定 OAuth2 客戶端支援 wechat 授權模式

image-20211016124619386

3. 測試

到此微信授權擴充套件完成,實際業務場景常用的3種授權模式也就告一段落。

但是如果你對 Spring Security OAuth2 有些瞭解的話,你會有疑問這些擴充套件的模式對應的重新整理模式需不需要做什麼調整呢?

如果擴充套件只是針對一種使用者體系以及一種認證方式(使用者名稱/手機號/openid)的話,比如驗證碼 模式的擴充套件,就不需要對重新整理模式做調整。

但是如果是多使用者體系或者多種認證方式,youlai-mall 就是多使用者體系以及多種認證方式,這時你必須做些調整來適配,不過改動不大,具體為什麼調整和如何調整下文細說。

五. 多使用者體系重新整理模式

1. 原理

重新整理模式 時序圖如下,相較於密碼模式還只是 GranterProvider的變動。

著重說一下重新整理模式的認證提供者 PreAuthenticatedAuthenticationProvider ,其 authenticate() 認證方法只做使用者狀態校驗,check() 方法呼叫 AccountStatusUserDetailsChecker#check(UserDetails)。

image-20211016232048927

注意 下this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken)authentication);preAuthenticatedUserDetailsService 使用者服務。

在沒有進行授權模式擴充套件的時候,是下面這樣設定的

image-20211016232139373

然後在 AuthorizationServerEndpointsConfigurer#addUserDetailsService(DefaultTokenServices,UserDetailsService) 構造 PreAuthenticatedAuthenticationProvider 裡設定了 UserDetailService使用者服務。

image-20211016231951555

這樣在多使用者體系認證下問題可想而知,使用者分別有系統使用者和會員使用者,這裡固定成一個使用者服務肯定是行不通的,擴充套件授權模式建立 Provider 時可以指定具體的使用者服務 UserDetailService,就如下面這樣:

image-20211016232510221

你可以為每個授權模式擴充套件新增對應的重新整理模式,但是這樣的話比較麻煩,本文的實現方案核心圖的是簡單有效,所以這裡使用的另一種方案,重新設定PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 屬性,讓其有判斷選擇使用者體系和認證方式的能力。

2. 實戰

首先我們清楚一個 OAuth2 客戶端基本對應的是一個使用者體系,比如 youlai-mall 專案的客戶端和使用者體系對應關係如下表:

OAuth2 客戶端名稱 OAuth2 客戶端ID 使用者體系
管理系統 mall-admin-web 系統使用者
H5/Android/IOS 移動端 mall-app 商城會員
小程式端 mall-weapp 商城會員

那就有一個很簡單有效的思路,可以在系統內部維護一個如上表的對映關係 Map,然後根據傳遞的客戶端ID去選擇使用者體系。

就這?當然不是,還有個點你必須要考慮到,舉個例子雖然移動端的使用者體系是會員使用者,但是它可能有多種認證方式呀,比如可以同時支援手機簡訊驗證碼和使用者名稱密碼甚至更多的認證方式。

而 Spring Security OAuth2 預設的 UserDetailsService 介面只有一個 loadUserByUsername() 方法,很顯然是做不到會員體系支援多種認證方式的。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

所以需要在 UserDetailsService 的實現類新增認證方式,然後在執行時將 UserDetailsService 轉為具體的實現類,具體可看下有來專案的 MemberUserDetailsServiceImpl 的實現,同時支援手機號和三方標識 openid 獲取使用者認證資訊,即兩種不同的認證方式。

/**
 * 商城會員使用者認證服務
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 */
@Service("memberUserDetailsService")
@RequiredArgsConstructor
public class MemberUserDetailsServiceImpl implements UserDetailsService {

    private final MemberFeignClient memberFeignClient;

    @Override
    public UserDetails loadUserByUsername(String username) {
        return null;
    }

    /**
     * 手機號碼認證方式
     *
     * @param mobile
     * @return
     */
    public UserDetails loadUserByMobile(String mobile) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.MOBILE.getValue());   // 認證方式:OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("該賬戶已被禁用!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("該賬號已被鎖定!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("該賬號已過期!");
        }
        return userDetails;
    }


    /**
     * openid 認證方式
     *
     * @param openId
     * @return
     */
    public UserDetails loadUserByOpenId(String openId) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByOpenId(openId);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.OPENID.getValue());   // 認證方式:OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("該賬戶已被禁用!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("該賬號已被鎖定!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("該賬號已過期!");
        }
        return userDetails;
    }
}

新增的 PreAuthenticatedUserDetailsService 可根據客戶端和認證方式選擇UserDetailService 和方法獲取使用者資訊 UserDetail

/**
 * 重新整理token再次認證 UserDetailsService
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/10/2
 */
@NoArgsConstructor
public class PreAuthenticatedUserDetailsService<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {

    /**
     * 客戶端ID和使用者服務 UserDetailService 的對映
     *
     * @see com.youlai.auth.security.config.AuthorizationServerConfig#tokenServices(AuthorizationServerEndpointsConfigurer)
     */
    private Map<String, UserDetailsService> userDetailsServiceMap;

    public PreAuthenticatedUserDetailsService(Map<String, UserDetailsService> userDetailsServiceMap) {
        Assert.notNull(userDetailsServiceMap, "userDetailsService cannot be null.");
        this.userDetailsServiceMap = userDetailsServiceMap;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsServiceMap, "UserDetailsService must be set");
    }

    /**
     * 重寫PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 屬性,可根據客戶端和認證方式選擇使用者服務 UserDetailService 獲取使用者資訊 UserDetail
     *
     * @param authentication
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
        String clientId = RequestUtils.getOAuth2ClientId();
        // 獲取認證方式,預設是使用者名稱 username
        AuthenticationMethodEnum authenticationMethodEnum = AuthenticationMethodEnum.getByValue(RequestUtils.getAuthenticationMethod());
        UserDetailsService userDetailsService = userDetailsServiceMap.get(clientId);
        if (clientId.equals(SecurityConstants.APP_CLIENT_ID)) {
            // 移動端的使用者體系是會員,認證方式是通過手機號 mobile 認證
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case MOBILE:
                    return memberUserDetailsService.loadUserByMobile(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.WEAPP_CLIENT_ID)) {
            // 小程式的使用者體系是會員,認證方式是通過微信三方標識 openid 認證
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case OPENID:
                    return memberUserDetailsService.loadUserByOpenId(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.ADMIN_CLIENT_ID)) {
            // 管理系統的使用者體系是系統使用者,認證方式通過使用者名稱 username 認證
            switch (authenticationMethodEnum) {
                default:
                    return userDetailsService.loadUserByUsername(authentication.getName());
            }
        } else {
            return userDetailsService.loadUserByUsername(authentication.getName());
        }
    }
}

AuthorizationServerConfig 配置重新設定 PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 屬性值

    /**
     * 配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token增強
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        // 獲取原有預設授權模式(授權碼模式、密碼模式、客戶端模式、簡化模式)的授權者
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));

        // 新增驗證碼授權模式授權者
        granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager, stringRedisTemplate
        ));

        // 新增手機簡訊驗證碼授權模式的授權者
        granterList.add(new SmsCodeTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        // 新增微信授權模式的授權者
        granterList.add(new WechatTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
                /** refresh token有兩種使用方式:重複使用(true)、非重複使用(false),預設為true
                 *  1 重複使用:access token過期重新整理時, refresh token過期時間未改變,仍以初次生成的時間為準
                 *  2 非重複使用:access token過期重新整理時, refresh token過期時間延續,在refresh token有效期內重新整理便永不失效達到無需再次登入的目的
                 */
                .reuseRefreshTokens(true)
                .tokenServices(tokenServices(endpoints))
        ;
    }


    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // 多使用者體系下,重新整理token再次認證客戶端ID和 UserDetailService 的對映Map
        Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
        clientUserDetailsServiceMap.put(SecurityConstants.ADMIN_CLIENT_ID, sysUserDetailsService); // 管理系統客戶端
        clientUserDetailsServiceMap.put(SecurityConstants.APP_CLIENT_ID, memberUserDetailsService); // Android/IOS/H5 移動客戶端
        clientUserDetailsServiceMap.put(SecurityConstants.WEAPP_CLIENT_ID, memberUserDetailsService); // 微信小程式客戶端

        // 重新設定PreAuthenticatedAuthenticationProvider#preAuthenticatedUserDetailsService 能夠根據客戶端ID和認證方式區分使用者體系獲取認證使用者資訊
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
        return tokenServices;
    }

核心程式碼基本都在上面,在完成以上的調整之後重新整理模式就可以了,接下來對新擴充套件的授權模式對應的重新整理模式進行逐一測試。

3. 測試

3.1 Postman 匯入 cURL 操作說明

下面所有的測試都會把 cURL 貼出來,至於為什麼強調這個?原來以為我把用 Postman 測試 Spring Security OAuth2 獲取 token 的完整請求截圖放入專案說明文件 README.md 這樣就不會再有人問登入介面 403 報錯,但事實反饋確實自己挺失望,以致於後來再有這樣的問題基本上選擇沉默了,希望大家換位思考理解下。所以這次想到的方案是把介面資訊以 cURL 的形式貼出來,然後直接匯入 Postman 測試。

下面是有來專案獲取 token 的 cURL

curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

進入 Postman 選擇 File → Import → Raw text 把上面的 cURL 匯入

postmancurl

3.2 密碼模式測試

密碼模式的測試使用的客戶端資訊, 客戶端ID:客戶端金鑰: mall-admin-web:123456 ----- Base64線上編碼 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

如果你要更改客戶端,請在下方介面的請求頭 Authorization 更換客戶端資訊即可,不然會報 403 提示,因為你的客戶端資訊不正確認證不成功禁止訪問。

有些人會問現在有來專案沒有自定義客戶端認證異常的處理,其實在我之前的文章有提供解決方案 https://www.cnblogs.com/haoxianrui/p/14028366.html#3-客戶端認證異常,有需要的可以根據文章調整。至於為什麼專案中沒有使用方案,首先覺得實現比較複雜,如果你有好的解決方案歡迎提出,另外這種客戶端資訊錯誤作為一個開發人員來說你是完全可以規避的。

獲取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

image-20211017113856479

重新整理token

refresh_token 需要替換,在第一步獲取 token 返回的 refresh_token

curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJmYzdiOGNhZi1iNmI4LTRlZTEtOGE4OC0yYzdmZTcxNTA0YjEiLCJleHAiOjE2MzQ0NDg5NDIsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiOGU3ZWE5MjAtOGQ0Ni00NmFlLWI3ODYtZTc3ZjAxY2Y5ZjIyIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.I_9uLpr7WUeb-JNSBr17Ya59qP3a8EFSps3MwqpTS-mlDldx-HDsJM41Pl11-b_99_yhl_h-FRhIYpGaOqP4p7428z_LQmlpBrebx9TVcSk_gVbDPjN3Q2glxaupvCGmAuRNWby0Aam-On2wO8RkKKhH0arI2nf4rseu18WN0-cqxJuYn10hyQ-T7n5n3zjnx92nMyqESWqfPqsy8_eie-can4113PBHhnqs9QI1SQ-1Z_AtZLgAb1FzaV2JuTqqbPlVULM-uaQnIoe0zNq5R-TYoUJ2cQNkP4YOR4e9TP26iSPLNlcsg59TFHi0UhrZiZqvS3i5nUkqV0jpzvYVrg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' 

image-20211017114259094

3.3 驗證碼模式測試

驗證碼模式的測試使用客戶端的資訊, 客戶端ID:客戶端金鑰: mall-admin-web:123456 ----- Base64線上編碼 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

獲取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=captcha&uuid=11add22b38e74a57bade0bf628a70645&validateCode=1' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2

image-20211017155156990

重新整理token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJiMTU5ZGU2Ni1iYmY5LTRmOWEtYTg1MC1kMjk1MDJiYTNjY2IiLCJleHAiOjE2MzQ0NjQxNjUsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiN2MwNDk2YzgtMTRjMC00MWJhLTk2OTUtYTk2ZGYwODQ1NGMxIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.j3n1FrMEIRkb_-3YhoDdPA4qBofzjD4y6HWdhCRdIjWU3D1La9ee_guhdeEEL49sfdHQSek_T4funyUCegTCdxfowzh3JghtCXFyRdxSWxjgJalgSIGVcOSEePxADwf2biHB3m6WzpOT9FxEdBavT7mfdQRjfc276uL7zzi5blKc4pUzX9l1AvReMP7azT_6soBNi-nid5maUCpMx_w9AVUvjVl4L7QMCO22zEogs2SlpMpggAITMv3QKYYTZ3vzxL2oNR_r-9qXqN7W6DxGqQc1gIqXADX1oqsXzD4AaAtLqOslP8FM6HiOzzZVd1kmv1cPHzVzabx6vYUZFA1PMg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

image-20211017155419436

3.4 手機簡訊驗證碼測試

手機簡訊驗證碼模式測試使用的客戶端的資訊, 客戶端ID:客戶端金鑰: mall-app:123456 ----- Base64線上編碼 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

獲取 token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?mobile=17621590365&code=666666&grant_type=sms_code' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

image-20211017160550739

重新整理token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGlvbk1ldGhvZCI6Im1vYmlsZSIsInVzZXJfbmFtZSI6IjE3NjIxNTkwMzY1Iiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBlZGMyZjI0LWFiNWUtNDkxYy1iYjAyLTdlOWJkN2U5M2Y0MiIsImV4cCI6MTYzNDQ2NTEzMCwidXNlcklkIjo1OSwianRpIjoiZjcyMWZhZjAtZTczMS00MmUxLTgxYjAtMjg4NDEwZjQzODA0IiwiY2xpZW50X2lkIjoibWFsbC1hcHAiLCJ1c2VybmFtZSI6IjE3NjIxNTkwMzY1In0.RdtJiNhk3OheoUcpUtM9JBgwLfSt1k3FhEvgMYeDSFwf28TeS_SF2LY7vzOrbJfYQZuaMzvMfoSljeDuQoBr38Ebh2LogbZClaDY72TO9P88DAW-1l2Rjm1XYFMEzCZYweDehT2tJU6eOwN8GZ40dzcCnqjZwgCKgoIdJksxMB6n96Kfmxw_Z3TUny5j2mdDZB79bwWci86jev6y-RUTjbZWRu1vH4MVJ0hCOCRARoem1jlkW6nnkzhE84OasDI9RCg5jsA_ZNs3x-rFNnRY7T5gQOAOwPvJKVcXww35BGYZGHCHqQb6QEbxul6Pg1rLjFU6YgsSO1Xq_cWVOt0Nvg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

image-20211017160927307

3.5 微信授權模式測試

微信授權模式測試使用的客戶端的資訊, 客戶端ID:客戶端金鑰: mall-weapp:123456 ----- Base64線上編碼 → bWFsbC13ZWFwcDoxMjM0NTY=

獲取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?code=063hEOFa1N1dWB0XpRIa1WvNw74hEOF-&encryptedData=1qmFeCKbTxZyCdzctu37sX+jOnM9dZG9lKyD3v6FhA5sCEtDwaF/wqyVR70QVrqt7bGVH+Kb+PBsFJlBXUdjnFGlrwmPqgNusI4f5eA8SvZgopvmlzJhXwe+OjLCQooeGnSkcnUrUuMA/G4ZYWFeljaHhxJq/75APWs4HyLANfbeLp50qI9xrRJVUXlTqdqJ0ub38ZxWVvWZMqY8FaskAiZpxzrF30eXu93BCpDavRCVzlSfv6LFJmmvEGVOKr4Wap9ND82N3sDMyArRsdhdhmoWIYBbRs+iLbKcS4WyOhpmaQr4fhhOuxO+zSAa7W+eNmCH2Id6Pgpvhl6ureNNzEb0cQLoksP6oakPmv/yEiw5fnW6Oi9jJbxzlMyORN3/atHgBl6zLIgS9UMhFE+42Vp5B3L8jLly4+B4NpNgol+khXoh+ycUXSRPV4bUuriv&iv=j+brWSrqRW+d4lAjRWW4RA==&grant_type=wechat' \
--header 'Authorization: Basic bWFsbC13ZWFwcDoxMjM0NTY='

image-20211017162304883

重新整理token

image-20211017163023128

六. 總結

本篇基於 Spring Security OAuth2 擴充套件了實際開發常用的 驗證碼模式手機簡訊驗證碼模式微信授權模式並分別應用至有來商城的管理前端移動應用端和微信小程式端,同時稍調整重新整理模式使其能夠適配擴充套件的幾種模式以及多使用者體系。通過授權模式的擴充套件揭露 Spring Security OAuth2 的認證流程和底層原理,相信對流程和原理有個清晰的思路之後,不同的認證需求都可以做到得心應手。最後還是感嘆下 Spring 框架的魅力,就是你能感受到它在功能的實現的基礎上會給你留個擴充套件的入口,而不是讓你想著去改它的原始碼去實現。最後希望大家都能收穫些東西吧,雖然我們這也不圖啥,寫這些說實話對自己提升也不大,但畢竟是花了半個多月時間寫的這篇文章,算是自己的一份心血,也不希望白費了。

七. 聯絡資訊

有興趣進交流群的同學加我微信,備註 有來 即可,純屬學習交流群,無任何利益。另外如果有興趣加入開源專案 youlai-mall 開發的歡迎私信我,或者能給專案提交PR的我聯絡您。

【有來小店】微信小程式體驗碼 進交流群加我,備註“有來”即可

相關文章