Spring Security OAuth2集成短信驗(yàn)證碼登錄以及第三方登錄
前言
基于SpringCloud做微服務(wù)架構(gòu)分布式系統(tǒng)時(shí),OAuth2.0作為認(rèn)證的業(yè)內(nèi)標(biāo)準(zhǔn),Spring Security OAuth2也提供了全套的解決方案來支持在Spring Cloud/Spring Boot環(huán)境下使用OAuth2.0,提供了開箱即用的組件。但是在開發(fā)過程中我們會(huì)發(fā)現(xiàn)由于Spring Security OAuth2的組件特別全面,這樣就導(dǎo)致了擴(kuò)展很不方便或者說是不太容易直指定擴(kuò)展的方案,例如:
- 圖片驗(yàn)證碼登錄
- 短信驗(yàn)證碼登錄
- 微信小程序登錄
- 第三方系統(tǒng)登錄
- CAS單點(diǎn)登錄
在面對(duì)這些場(chǎng)景的時(shí)候,預(yù)計(jì)很多對(duì)Spring Security OAuth2不熟悉的人恐怕會(huì)無從下手?;谏鲜龅膱?chǎng)景要求,如何優(yōu)雅的集成短信驗(yàn)證碼登錄及第三方登錄,怎么樣才算是優(yōu)雅集成呢?有以下要求:
- 不侵入Spring Security OAuth2的原有代碼
- 對(duì)于不同的登錄方式不擴(kuò)展新的端點(diǎn),使用/oauth/token可以適配所有的登錄方式
- 可以對(duì)所有登錄方式進(jìn)行兼容,抽象一套模型只要簡(jiǎn)單的開發(fā)就可以集成登錄
基于上述的設(shè)計(jì)要求,接下來將會(huì)在文章種詳細(xì)介紹如何開發(fā)一套集成登錄認(rèn)證組件開滿足上述要求。
閱讀本篇文章您需要了解OAuth2.0認(rèn)證體系、SpringBoot、SpringSecurity以及Spring Cloud等相關(guān)知識(shí)
思路
我們來看下Spring Security OAuth2的認(rèn)證流程:

這個(gè)流程當(dāng)中,切入點(diǎn)不多,集成登錄的思路如下:
- 在進(jìn)入流程之前先進(jìn)行攔截,設(shè)置集成認(rèn)證的類型,例如:短信驗(yàn)證碼、圖片驗(yàn)證碼等信息。
- 在攔截的通知進(jìn)行預(yù)處理,預(yù)處理的場(chǎng)景有很多,比如驗(yàn)證短信驗(yàn)證碼是否匹配、圖片驗(yàn)證碼是否匹配、是否是登錄IP白名單等處理
- 在UserDetailService.loadUserByUsername方法中,根據(jù)之前設(shè)置的集成認(rèn)證類型去獲取用戶信息,例如:通過手機(jī)號(hào)碼獲取用戶、通過微信小程序OPENID獲取用戶等等
接入這個(gè)流程之后,基本上就可以優(yōu)雅集成第三方登錄。
實(shí)現(xiàn)
介紹完思路之后,下面通過代碼來展示如何實(shí)現(xiàn):
第一步,定義攔截器攔截登錄的請(qǐng)求
/**
* @author LIQIU
* @date 2018-3-30
**/
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final String AUTH_TYPE_PARM_NAME = "auth_type";
private static final String OAUTH_TOKEN_URL = "/oauth/token";
private Collection<IntegrationAuthenticator> authenticators;
private ApplicationContext applicationContext;
private RequestMatcher requestMatcher;
public IntegrationAuthenticationFilter(){
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if(requestMatcher.matches(request)){
//設(shè)置集成登錄信息
IntegrationAuthentication integrationAuthentication = new IntegrationAuthentication();
integrationAuthentication.setAuthType(request.getParameter(AUTH_TYPE_PARM_NAME));
integrationAuthentication.setAuthParameters(request.getParameterMap());
IntegrationAuthenticationContext.set(integrationAuthentication);
try{
//預(yù)處理
this.prepare(integrationAuthentication);
filterChain.doFilter(request,response);
//后置處理
this.complete(integrationAuthentication);
}finally {
IntegrationAuthenticationContext.clear();
}
}else{
filterChain.doFilter(request,response);
}
}
/**
* 進(jìn)行預(yù)處理
* @param integrationAuthentication
*/
private void prepare(IntegrationAuthentication integrationAuthentication) {
//延遲加載認(rèn)證器
if(this.authenticators == null){
synchronized (this){
Map<String,IntegrationAuthenticator> integrationAuthenticatorMap = applicationContext.getBeansOfType(IntegrationAuthenticator.class);
if(integrationAuthenticatorMap != null){
this.authenticators = integrationAuthenticatorMap.values();
}
}
}
if(this.authenticators == null){
this.authenticators = new ArrayList<>();
}
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(integrationAuthentication)){
authenticator.prepare(integrationAuthentication);
}
}
}
/**
* 后置處理
* @param integrationAuthentication
*/
private void complete(IntegrationAuthentication integrationAuthentication){
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(integrationAuthentication)){
authenticator.complete(integrationAuthentication);
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
在這個(gè)類種主要完成2部分工作:1、根據(jù)參數(shù)獲取當(dāng)前的是認(rèn)證類型,2、根據(jù)不同的認(rèn)證類型調(diào)用不同的IntegrationAuthenticator.prepar進(jìn)行預(yù)處理
第二步,將攔截器放入到攔截鏈條中
/**
* @author LIQIU
* @date 2018-3-7
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private IntegrationUserDetailsService integrationUserDetailsService;
@Autowired
private WebResponseExceptionTranslator webResponseExceptionTranslator;
@Autowired
private IntegrationAuthenticationFilter integrationAuthenticationFilter;
@Autowired
private DatabaseCachableClientDetailsService redisClientDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// TODO persist clients details
clients.withClientDetails(redisClientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
// .accessTokenConverter(jwtAccessTokenConverter())
.authenticationManager(authenticationManager)
.exceptionTranslator(webResponseExceptionTranslator)
.reuseRefreshTokens(false)
.userDetailsService(integrationUserDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
.addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cola-cloud");
return jwtAccessTokenConverter;
}
}
通過調(diào)用security. .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);方法,將攔截器放入到認(rèn)證鏈條中。
第三步,根據(jù)認(rèn)證類型來處理用戶信息
@Service
public class IntegrationUserDetailsService implements UserDetailsService {
@Autowired
private UpmClient upmClient;
private List<IntegrationAuthenticator> authenticators;
@Autowired(required = false)
public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
this.authenticators = authenticators;
}
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
IntegrationAuthentication integrationAuthentication = IntegrationAuthenticationContext.get();
//判斷是否是集成登錄
if (integrationAuthentication == null) {
integrationAuthentication = new IntegrationAuthentication();
}
integrationAuthentication.setUsername(username);
UserVO userVO = this.authenticate(integrationAuthentication);
if(userVO == null){
throw new UsernameNotFoundException("用戶名或密碼錯(cuò)誤");
}
User user = new User();
BeanUtils.copyProperties(userVO, user);
this.setAuthorize(user);
return user;
}
/**
* 設(shè)置授權(quán)信息
*
* @param user
*/
public void setAuthorize(User user) {
Authorize authorize = this.upmClient.getAuthorize(user.getId());
user.setRoles(authorize.getRoles());
user.setResources(authorize.getResources());
}
private UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
if (this.authenticators != null) {
for (IntegrationAuthenticator authenticator : authenticators) {
if (authenticator.support(integrationAuthentication)) {
return authenticator.authenticate(integrationAuthentication);
}
}
}
return null;
}
}
這里實(shí)現(xiàn)了一個(gè)IntegrationUserDetailsService ,在loadUserByUsername方法中會(huì)調(diào)用authenticate方法,在authenticate方法中會(huì)當(dāng)前上下文種的認(rèn)證類型調(diào)用不同的IntegrationAuthenticator 來獲取用戶信息,接下來來看下默認(rèn)的用戶名密碼是如何處理的:
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {
@Autowired
private UcClient ucClient;
@Override
public UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
return ucClient.findUserByUsername(integrationAuthentication.getUsername());
}
@Override
public void prepare(IntegrationAuthentication integrationAuthentication) {
}
@Override
public boolean support(IntegrationAuthentication integrationAuthentication) {
return StringUtils.isEmpty(integrationAuthentication.getAuthType());
}
}
UsernamePasswordAuthenticator只會(huì)處理沒有指定的認(rèn)證類型即是默認(rèn)的認(rèn)證類型,這個(gè)類中主要是通過用戶名獲取密碼。接下來來看下圖片驗(yàn)證碼登錄如何處理的:
/**
* 集成驗(yàn)證碼認(rèn)證
* @author LIQIU
* @date 2018-3-31
**/
@Component
public class VerificationCodeIntegrationAuthenticator extends UsernamePasswordAuthenticator {
private final static String VERIFICATION_CODE_AUTH_TYPE = "vc";
@Autowired
private VccClient vccClient;
@Override
public void prepare(IntegrationAuthentication integrationAuthentication) {
String vcToken = integrationAuthentication.getAuthParameter("vc_token");
String vcCode = integrationAuthentication.getAuthParameter("vc_code");
//驗(yàn)證驗(yàn)證碼
Result<Boolean> result = vccClient.validate(vcToken, vcCode, null);
if (!result.getData()) {
throw new OAuth2Exception("驗(yàn)證碼錯(cuò)誤");
}
}
@Override
public boolean support(IntegrationAuthentication integrationAuthentication) {
return VERIFICATION_CODE_AUTH_TYPE.equals(integrationAuthentication.getAuthType());
}
}
VerificationCodeIntegrationAuthenticator繼承UsernamePasswordAuthenticator,因?yàn)槠渲皇切枰趐repare方法中驗(yàn)證驗(yàn)證碼是否正確,獲取用戶還是用過用戶名密碼的方式獲取。但是需要認(rèn)證類型為"vc"才會(huì)處理
接下來來看下短信驗(yàn)證碼登錄是如何處理的:
@Component
public class SmsIntegrationAuthenticator extends AbstractPreparableIntegrationAuthenticator implements ApplicationEventPublisherAware {
@Autowired
private UcClient ucClient;
@Autowired
private VccClient vccClient;
@Autowired
private PasswordEncoder passwordEncoder;
private ApplicationEventPublisher applicationEventPublisher;
private final static String SMS_AUTH_TYPE = "sms";
@Override
public UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
//獲取密碼,實(shí)際值是驗(yàn)證碼
String password = integrationAuthentication.getAuthParameter("password");
//獲取用戶名,實(shí)際值是手機(jī)號(hào)
String username = integrationAuthentication.getUsername();
//發(fā)布事件,可以監(jiān)聽事件進(jìn)行自動(dòng)注冊(cè)用戶
this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication));
//通過手機(jī)號(hào)碼查詢用戶
UserVO userVo = this.ucClient.findUserByPhoneNumber(username);
if (userVo != null) {
//將密碼設(shè)置為驗(yàn)證碼
userVo.setPassword(passwordEncoder.encode(password));
//發(fā)布事件,可以監(jiān)聽事件進(jìn)行消息通知
this.applicationEventPublisher.publishEvent(new SmsAuthenticateSuccessEvent(integrationAuthentication));
}
return userVo;
}
@Override
public void prepare(IntegrationAuthentication integrationAuthentication) {
String smsToken = integrationAuthentication.getAuthParameter("sms_token");
String smsCode = integrationAuthentication.getAuthParameter("password");
String username = integrationAuthentication.getAuthParameter("username");
Result<Boolean> result = vccClient.validate(smsToken, smsCode, username);
if (!result.getData()) {
throw new OAuth2Exception("驗(yàn)證碼錯(cuò)誤或已過期");
}
}
@Override
public boolean support(IntegrationAuthentication integrationAuthentication) {
return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType());
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
SmsIntegrationAuthenticator會(huì)對(duì)登錄的短信驗(yàn)證碼進(jìn)行預(yù)處理,判斷其是否非法,如果是非法的則直接中斷登錄。如果通過預(yù)處理則在獲取用戶信息的時(shí)候通過手機(jī)號(hào)去獲取用戶信息,并將密碼重置,以通過后續(xù)的密碼校驗(yàn)。
總結(jié)
在這個(gè)解決方案中,主要是使用責(zé)任鏈和適配器的設(shè)計(jì)模式來解決集成登錄的問題,提高了可擴(kuò)展性,并對(duì)spring的源碼無污染。如果還要繼承其他的登錄,只需要實(shí)現(xiàn)自定義的IntegrationAuthenticator就可以。
項(xiàng)目地址:https://gitee.com/leecho/cola-cloud
本地下載:cola-cloud_jb51.rar
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Spring Security 實(shí)現(xiàn)多種登錄方式(常規(guī)方式外的郵件、手機(jī)驗(yàn)證碼登錄)
- Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
- Spring Security登錄添加驗(yàn)證碼的實(shí)現(xiàn)過程
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例
- 使用Spring Security集成手機(jī)驗(yàn)證碼登錄功能實(shí)現(xiàn)
相關(guān)文章
Spring?Boot?教程之創(chuàng)建項(xiàng)目的三種方式
這篇文章主要分享了Spring?Boot?教程之創(chuàng)建項(xiàng)目的三種方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-05-05
spring-boot.version2.6升級(jí)到2.7.18后security報(bào)錯(cuò)問題
這篇文章主要介紹了spring-boot.version2.6升級(jí)到2.7.18后security報(bào)錯(cuò)問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Java返回文件時(shí)為圖片或pdf等設(shè)置在線預(yù)覽或下載功能
這篇文章主要介紹了Java返回文件時(shí)為圖片或pdf等設(shè)置在線預(yù)覽或下載功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
Spring?Boot實(shí)現(xiàn)分布式任務(wù)調(diào)度的步驟
Spring?Boot提供了一些工具和框架,可以幫助我們輕松地實(shí)現(xiàn)分布式任務(wù)調(diào)度,在本文中我們將介紹如何使用Spring?Boot、Spring?Cloud、Quartz和Redis來實(shí)現(xiàn)分布式任務(wù)調(diào)度,感興趣的朋友跟隨小編一起看看吧2023-06-06
使用SpringBoot集成Thymeleaf和Flying?Saucer實(shí)現(xiàn)PDF導(dǎo)出
在?Spring?Boot?項(xiàng)目中,生成?PDF?報(bào)表或發(fā)票是常見需求,本文將介紹如何使用?Spring?Boot?集成?Thymeleaf?模板引擎和?Flying?Saucer?實(shí)現(xiàn)?PDF?導(dǎo)出,并提供詳細(xì)的代碼實(shí)現(xiàn)和常見問題解決方案,需要的朋友可以參考下2024-11-11
SpringMVC 跨重定向請(qǐng)求傳遞數(shù)據(jù)的方法實(shí)現(xiàn)
這篇文章主要介紹了SpringMVC 跨重定向請(qǐng)求傳遞數(shù)據(jù)的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
Java實(shí)現(xiàn)儲(chǔ)存對(duì)象并按對(duì)象某屬性排序的幾種方法示例
這篇文章主要介紹了Java實(shí)現(xiàn)儲(chǔ)存對(duì)象并按對(duì)象某屬性排序的幾種方法,結(jié)合實(shí)例形式詳細(xì)分析了Java儲(chǔ)存對(duì)象并按對(duì)象某屬性排序的具體實(shí)現(xiàn)方法與操作注意事項(xiàng),需要的朋友可以參考下2020-05-05
springboot+mybatis攔截器方法實(shí)現(xiàn)水平分表操作
這篇文章主要介紹了springboot+mybatis攔截器方法實(shí)現(xiàn)水平分表操作,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08

