Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战(二)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战(二)

三. 手机短信验证码授权模式

1. 原理

手机短信验证码模式时序图如下,变动的角色还是用绿色背景标识。可以看到扩展是对授权者 Granter 和认证提供者 Provider 做切入口。

微信图片_20230710084932.png



手机短信验证码授权流程: 流程基本上和密码模式一致,根据 grant_type 匹配授权者 SmsCodeTokenGranter , 委托给 ProviderManager 进行认证,根据 SmsCodeAuthenticationToken的匹配认证提供者 SmsCodeAuthenticationProvider 进行短信验证码校验。


2. 实战

2.1 手机短信验证码授权模式扩展

SmsCodeTokenGranter

/**

* 手机验证码授权者

*

* @author xianrui

* @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 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 xianrui

* @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 添加到认证器的授权类型的集合中去。

微信图片_20230710085013.png


2.2 阿里云免费短信申请

访问 https://free.aliyun.com/product/cloudcommunication-free-trial?spm=5176.10695662.1128094.7.2a6b4bee30xtJx 申请阿里云免费短信试用


微信图片_20230710085029.png


添加签名,等待审核通过

微信图片_20230710085035.png


签名审核通过之后就可以创建 AccessKey 访问密钥

微信图片_20230710085052.png 微信图片_20230710085056.png




添加模板, 国内消息 → 模板管理 → 添加模板

微信图片_20230710085059.png



签名审核通过后得到 AccessKey 和 模板审核通过得到模板CODE,接下来就可以进行项目整合了。


2.3 SpringBoot 整合阿里云 SMS 短信

SpringBoot 整合 SMS 网上教程很多,这里不画蛇添足,接下来简单说下 youlai-mall 整合阿里云 SMS 短信。完整源码


按惯例把短信封装成一个公共模块以便给其他需要短信的应用模块引用。

微信图片_20230710085130.png



youlai-auth 引入 common-sms 依赖


   

       com.youlai

       common-sms

   



其中 AliyunSmsProperties 需要的属性需要配置在 Nacos 的配置中心文件 youlai-auth.yaml


# 阿里云短信配置

aliyun:

 sms:

   accessKeyId: xxxxxx

   accessKeySecret: xxxxxxx

   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 登录界面

微信小程序 登录界面

微信图片_20230710085146.png

微信图片_20230710085150.png



登录页面 /pages/login/login.vue 在不同的平台有不同的呈现实现原理是通过 #ifdef MP 和 #ifndef MP 条件编译指令实现的,其中 #ifdef MP 是在小程序平台编译生效,而 #ifdef MP 是非小程序平台编译生效。


在开发编译时,当在 HBuilderX 工具栏点击运行选择不同的平台会有不同的页面呈现。


运行 → 运行到内置浏览器 → 手机短信验证码登录界面;

运行 → 运行到小程序模拟器 → 微信开发者工具 → 小程序授权登录界面;

说到接入 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 模式




3. 测试

微信图片_20230710085308.gif


到此H5/Android/IOS移动端接入 Spring Security OAuth2 扩展的手机短信验证码授权模式已经完成。接下来扩展的授权模式是针对当下最火的微信小程序移动端的授权登录。


四. 微信授权模式

1. 原理

微信小程序登录授权流程图如下,我们所扮演的角色是 开发者服务器,主要的工作是接收小程序端的 code 从微信服务器获取 openid 和 session_key 后在开发者服务器生成会话(token)返回给小程序,后续小程序携带token和开发者服务器进行交互,也就没有微信服务器啥事了。


微信图片_20230710085330.jpg


Spring Security OAuth2 微信授权扩展和上面的手机短信验证码原理一样,添加授权者 WechatTokenGranter 构建 WechatAuthenticationToken , 匹配到认证提供者 WechatAuthenticationProvider ,在其 authenticate 方法完成认证授权逻辑。


微信图片_20230710085333.png


2. 实战

2.1 微信授权模式扩展

WechatTokenGranter

WechatTokenGranter 微信授权者接收 code 、encryptedData 、iv 构建 WechatAuthenticationToken


/**

*  微信授权者

*

* @author xianrui

* @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 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 xianrui

* @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 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 授权模式


微信图片_20230710085356.png


3. 测试

微信图片_20230710085359.gif


到此微信授权扩展完成,实际业务场景常用的3种授权模式也就告一段落。


但是如果你对 Spring Security OAuth2 有些了解的话,你会有疑问这些扩展的模式对应的刷新模式需不需要做什么调整呢?


如果扩展只是针对一种用户体系以及一种认证方式(用户名/手机号/openid)的话,比如验证码 模式的扩展,就不需要对刷新模式做调整。


但是如果是多用户体系或者多种认证方式,youlai-mall 就是多用户体系以及多种认证方式,这时你必须做些调整来适配,不过改动不大,具体为什么调整和如何调整下文细说。


五. 多用户体系刷新模式

1. 原理

刷新模式 时序图如下,相较于密码模式还只是 Granter 和 Provider的变动。

微信图片_20230710085423.png


着重说一下刷新模式的认证提供者 PreAuthenticatedAuthenticationProvider ,其 authenticate() 认证方法只做用户状态校验,check() 方法调用 AccountStatusUserDetailsChecker#check(UserDetails)。

微信图片_20230710085427.png



注意 下this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken)authentication); 的 preAuthenticatedUserDetailsService 用户服务。


在没有进行授权模式扩展的时候,是下面这样设置的

微信图片_20230710085429.png



然后在 AuthorizationServerEndpointsConfigurer#addUserDetailsService(DefaultTokenServices,UserDetailsService) 构造 PreAuthenticatedAuthenticationProvider 里设置了 UserDetailService用户服务。

微信图片_20230710085519.png



这样在多用户体系认证下问题可想而知,用户分别有系统用户和会员用户,这里固定成一个用户服务肯定是行不通的,扩展授权模式创建 Provider 时可以指定具体的用户服务 UserDetailService,就如下面这样:

微信图片_20230710085521.png



你可以为每个授权模式扩展新增对应的刷新模式,但是这样的话比较麻烦,本文的实现方案核心图的是简单有效,所以这里使用的另一种方案,重新设置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;

}

1

2

3

所以需要在 UserDetailsService 的实现类新增认证方式,然后在运行时将 UserDetailsService 转为具体的实现类,具体可看下有来项目的 MemberUserDetailsServiceImpl 的实现,同时支持手机号和三方标识 openid 获取用户认证信息,即两种不同的认证方式。


/**

* 商城会员用户认证服务

*

* @author xianrui

*/

@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 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 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 xianrui

* @date 2021/10/2

*/

@NoArgsConstructor

public class PreAuthenticatedUserDetailsService implements AuthenticationUserDetailsService, InitializingBean {


   /**

    * 客户端ID和用户服务 UserDetailService 的映射

    *

    * @see com.youlai.auth.security.config.AuthorizationServerConfig#tokenServices(AuthorizationServerEndpointsConfigurer)

    */

   private Map userDetailsServiceMap;


   public PreAuthenticatedUserDetailsService(Map 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 tokenEnhancers = new ArrayList<>();

       tokenEnhancers.add(tokenEnhancer());

       tokenEnhancers.add(jwtAccessTokenConverter());

       tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);


       // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者

       List 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 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 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'

1

2

进入 Postman 选择 File → Import → Raw text 把上面的 cURL 导入

微信图片_20230710085550.gif



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'

1

2

微信图片_20230710085554.png


刷新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'

1

2



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

1

2

微信图片_20230710085626.png


刷新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'

1

2

微信图片_20230710085629.png


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'

1

2

微信图片_20230710085653.png


刷新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'

1

2

微信图片_20230710085656.png


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='

1

2


微信图片_20230710085717.png

刷新token

微信图片_20230710085721.png


六. 总结

本篇基于 Spring Security OAuth2 扩展了实际开发常用的 验证码模式 、手机短信验证码模式和微信授权模式并分别应用至有来商城的管理前端、移动应用端和微信小程序端,同时稍调整刷新模式使其能够适配扩展的几种模式以及多用户体系。通过授权模式的扩展揭露 Spring Security OAuth2 的认证流程和底层原理,相信对流程和原理有个清晰的思路之后,不同的认证需求都可以做到得心应手。最后还是感叹下 Spring 框架的魅力,就是你能感受到它在功能的实现的基础上会给你留个扩展的入口,而不是让你想着去改它的源码去实现。最后希望大家都能收获些东西吧,虽然咱这也不图啥,写这些说实话对自己提升也不大,但毕竟是花了半个多月时间写的这篇文章,算是自己的一份心血,也不希望白费了。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
Java 开发者 微服务
从单体到微服务:如何借助 Spring Cloud 实现架构转型
**Spring Cloud** 是一套基于 Spring 框架的**微服务架构解决方案**,它提供了一系列的工具和组件,帮助开发者快速构建分布式系统,尤其是微服务架构。
294 69
从单体到微服务:如何借助 Spring Cloud 实现架构转型
|
1月前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
94 16
|
8天前
|
传感器 监控 安全
智慧工地云平台的技术架构解析:微服务+Spring Cloud如何支撑海量数据?
慧工地解决方案依托AI、物联网和BIM技术,实现对施工现场的全方位、立体化管理。通过规范施工、减少安全隐患、节省人力、降低运营成本,提升工地管理的安全性、效率和精益度。该方案适用于大型建筑、基础设施、房地产开发等场景,具备微服务架构、大数据与AI分析、物联网设备联网、多端协同等创新点,推动建筑行业向数字化、智能化转型。未来将融合5G、区块链等技术,助力智慧城市建设。
|
1月前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
6月前
|
Java UED Sentinel
微服务守护神:Spring Cloud Sentinel,让你的系统在流量洪峰中稳如磐石!
【8月更文挑战第29天】Spring Cloud Sentinel结合了阿里巴巴Sentinel的流控、降级、熔断和热点规则等特性,为微服务架构下的应用提供了一套完整的流量控制解决方案。它能够有效应对突发流量,保护服务稳定性,避免雪崩效应,确保系统在高并发下健康运行。通过简单的配置和注解即可实现高效流量控制,适用于高并发场景、依赖服务不稳定及资源保护等多种情况,显著提升系统健壮性和用户体验。
122 1
|
2月前
|
运维 监控 Java
为何内存不够用?微服务改造启动多个Spring Boot的陷阱与解决方案
本文记录并复盘了生产环境中Spring Boot应用内存占用过高的问题及解决过程。系统上线初期运行正常,但随着业务量上升,多个Spring Boot应用共占用了64G内存中的大部分,导致应用假死。通过jps和jmap工具排查发现,原因是运维人员未设置JVM参数,导致默认配置下每个应用占用近12G内存。最终通过调整JVM参数、优化堆内存大小等措施解决了问题。建议在生产环境中合理设置JVM参数,避免资源浪费和性能问题。
128 3
|
6月前
|
Cloud Native Java Nacos
微服务时代的新宠儿!Spring Cloud Nacos实战指南,带你玩转服务发现与配置管理,拥抱云原生潮流!
【8月更文挑战第29天】Spring Cloud Nacos作为微服务架构中的新兴之星,凭借其轻量、高效的特点,迅速成为服务发现、配置管理和治理的首选方案。Nacos(命名和配置服务)由阿里巴巴开源,为云原生应用提供了动态服务发现及配置管理等功能,简化了服务间的调用与依赖管理。本文将指导你通过五个步骤在Spring Boot项目中集成Nacos,实现服务注册、发现及配置动态管理,从而轻松搭建出高效的微服务环境。
347 0
|
5月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
255 5
|
5月前
|
Java API 对象存储
微服务魔法启动!Spring Cloud与Netflix OSS联手,零基础也能创造服务奇迹!
这段内容介绍了如何使用Spring Cloud和Netflix OSS构建微服务架构。首先,基于Spring Boot创建项目并添加Spring Cloud依赖项。接着配置Eureka服务器实现服务发现,然后创建REST控制器作为API入口。为提高服务稳定性,利用Hystrix实现断路器模式。最后,在启动类中启用Eureka客户端功能。此外,还可集成其他Netflix OSS组件以增强系统功能。通过这些步骤,开发者可以更高效地构建稳定且可扩展的微服务系统。
87 1
|
5月前
|
缓存 Java 应用服务中间件
随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架
【9月更文挑战第6天】随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架。Nginx作为高性能的HTTP反向代理服务器,常用于前端负载均衡,提升应用的可用性和响应速度。本文详细介绍如何通过合理配置实现Spring Boot与Nginx的高效协同工作,包括负载均衡策略、静态资源缓存、数据压缩传输及Spring Boot内部优化(如线程池配置、缓存策略等)。通过这些方法,开发者可以显著提升系统的整体性能,打造高性能、高可用的Web应用。
91 2

热门文章

最新文章