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

一. 前言

【APP 移動端】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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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的我聯繫您。

【有來小店】微信小程序體驗碼 進交流羣加我,備註“有來”即可
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章