摘要:前言基于做微服務架構分布式系統(tǒng)時,作為認證的業(yè)內標準,也提供了全套的解決方案來支持在環(huán)境下使用,提供了開箱即用的組件。
前言
基于SpringCloud做微服務架構分布式系統(tǒng)時,OAuth2.0作為認證的業(yè)內標準,Spring Security OAuth2也提供了全套的解決方案來支持在Spring Cloud/Spring Boot環(huán)境下使用OAuth2.0,提供了開箱即用的組件。但是在開發(fā)過程中我們會發(fā)現(xiàn)由于Spring Security OAuth2的組件特別全面,這樣就導致了擴展很不方便或者說是不太容易直指定擴展的方案,例如:
圖片驗證碼登錄
短信驗證碼登錄
微信小程序登錄
第三方系統(tǒng)登錄
CAS單點登錄
在面對這些場景的時候,預計很多對Spring Security OAuth2不熟悉的人恐怕會無從下手。基于上述的場景要求,如何優(yōu)雅的集成短信驗證碼登錄及第三方登錄,怎么樣才算是優(yōu)雅集成呢?有以下要求:
不侵入Spring Security OAuth2的原有代碼
對于不同的登錄方式不擴展新的端點,使用/oauth/token可以適配所有的登錄方式
可以對所有登錄方式進行兼容,抽象一套模型只要簡單的開發(fā)就可以集成登錄
基于上述的設計要求,接下來將會在文章種詳細介紹如何開發(fā)一套集成登錄認證組件開滿足上述要求。
閱讀本篇文章您需要了解OAuth2.0認證體系、SpringBoot、SpringSecurity以及Spring Cloud等相關知識思路
我們來看下Spring Security OAuth2的認證流程:
這個流程當中,切入點不多,集成登錄的思路如下:
在進入流程之前先進行攔截,設置集成認證的類型,例如:短信驗證碼、圖片驗證碼等信息。
在攔截的通知進行預處理,預處理的場景有很多,比如驗證短信驗證碼是否匹配、圖片驗證碼是否匹配、是否是登錄IP白名單等處理
在UserDetailService.loadUserByUsername方法中,根據(jù)之前設置的集成認證類型去獲取用戶信息,例如:通過手機號碼獲取用戶、通過微信小程序OPENID獲取用戶等等
接入這個流程之后,基本上就可以優(yōu)雅集成第三方登錄。
實現(xiàn)介紹完思路之后,下面通過代碼來展示如何實現(xiàn):
第一步,定義攔截器攔截登錄的請求/** * @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 Collectionauthenticators; 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)){ //設置集成登錄信息 IntegrationAuthentication integrationAuthentication = new IntegrationAuthentication(); integrationAuthentication.setAuthType(request.getParameter(AUTH_TYPE_PARM_NAME)); integrationAuthentication.setAuthParameters(request.getParameterMap()); IntegrationAuthenticationContext.set(integrationAuthentication); try{ //預處理 this.prepare(integrationAuthentication); filterChain.doFilter(request,response); //后置處理 this.complete(integrationAuthentication); }finally { IntegrationAuthenticationContext.clear(); } }else{ filterChain.doFilter(request,response); } } /** * 進行預處理 * @param integrationAuthentication */ private void prepare(IntegrationAuthentication integrationAuthentication) { //延遲加載認證器 if(this.authenticators == null){ synchronized (this){ Map 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; } }
在這個類種主要完成2部分工作:1、根據(jù)參數(shù)獲取當前的是認證類型,2、根據(jù)不同的認證類型調用不同的IntegrationAuthenticator.prepar進行預處理
第二步,將攔截器放入到攔截鏈條中/** * @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; } }
通過調用security. .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);方法,將攔截器放入到認證鏈條中。
第三步,根據(jù)認證類型來處理用戶信息@Service public class IntegrationUserDetailsService implements UserDetailsService { @Autowired private UpmClient upmClient; private Listauthenticators; @Autowired(required = false) public void setIntegrationAuthenticators(List 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("用戶名或密碼錯誤"); } User user = new User(); BeanUtils.copyProperties(userVO, user); this.setAuthorize(user); return user; } /** * 設置授權信息 * * @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; } }
這里實現(xiàn)了一個IntegrationUserDetailsService ,在loadUserByUsername方法中會調用authenticate方法,在authenticate方法中會當前上下文種的認證類型調用不同的IntegrationAuthenticator 來獲取用戶信息,接下來來看下默認的用戶名密碼是如何處理的:
@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只會處理沒有指定的認證類型即是默認的認證類型,這個類中主要是通過用戶名獲取密碼。接下來來看下圖片驗證碼登錄如何處理的:
/** * 集成驗證碼認證 * @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"); //驗證驗證碼 Resultresult = vccClient.validate(vcToken, vcCode, null); if (!result.getData()) { throw new OAuth2Exception("驗證碼錯誤"); } } @Override public boolean support(IntegrationAuthentication integrationAuthentication) { return VERIFICATION_CODE_AUTH_TYPE.equals(integrationAuthentication.getAuthType()); } }
VerificationCodeIntegrationAuthenticator繼承UsernamePasswordAuthenticator,因為其只是需要在prepare方法中驗證驗證碼是否正確,獲取用戶還是用過用戶名密碼的方式獲取。但是需要認證類型為"vc"才會處理
接下來來看下短信驗證碼登錄是如何處理的:
@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) { //獲取密碼,實際值是驗證碼 String password = integrationAuthentication.getAuthParameter("password"); //獲取用戶名,實際值是手機號 String username = integrationAuthentication.getUsername(); //發(fā)布事件,可以監(jiān)聽事件進行自動注冊用戶 this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication)); //通過手機號碼查詢用戶 UserVO userVo = this.ucClient.findUserByPhoneNumber(username); if (userVo != null) { //將密碼設置為驗證碼 userVo.setPassword(passwordEncoder.encode(password)); //發(fā)布事件,可以監(jiā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"); Resultresult = vccClient.validate(smsToken, smsCode, username); if (!result.getData()) { throw new OAuth2Exception("驗證碼錯誤或已過期"); } } @Override public boolean support(IntegrationAuthentication integrationAuthentication) { return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType()); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }
SmsIntegrationAuthenticator會對登錄的短信驗證碼進行預處理,判斷其是否非法,如果是非法的則直接中斷登錄。如果通過預處理則在獲取用戶信息的時候通過手機號去獲取用戶信息,并將密碼重置,以通過后續(xù)的密碼校驗。
總結在這個解決方案中,主要是使用責任鏈和適配器的設計模式來解決集成登錄的問題,提高了可擴展性,并對spring的源碼無污染。如果還要繼承其他的登錄,只需要實現(xiàn)自定義的IntegrationAuthenticator就可以。
項目地址:https://gitee.com/leecho/cola...
大家有好的建議和想法可以一起溝通交流。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69060.html
摘要:驗證碼的發(fā)放校驗邏輯比較簡單,方法后通過全局判斷請求中是否和手機號匹配集合,重點邏輯是令牌的參數(shù) spring security oauth2 登錄過程詳解 ? showImg(https://segmentfault.com/img/remote/1460000012811024); ? 定義手機號登錄令牌 /** * @author lengleng * @date 2018/...
摘要:我們以微信為例,首先我們發(fā)送一個請求,因為你已經(jīng)登錄了,所以后臺可以獲取當前是誰,然后就獲取到請求的鏈接,最后就是跳轉到這個鏈接上面去。 1、準備工作 申請QQ、微信相關AppId和AppSecret,這些大家自己到QQ互聯(lián)和微信開發(fā)平臺 去申請吧 還有java后臺要引入相關的jar包,如下: org.springframework.security....
摘要:前言現(xiàn)在的好多項目都是基于移動端以及前后端分離的項目,之前基于的前后端放到一起的項目已經(jīng)慢慢失寵并淡出我們視線,尤其是當基于的微服務架構以及單頁面應用流行起來后,情況更甚。使用生成是什么請自行百度。 1、前言 現(xiàn)在的好多項目都是基于APP移動端以及前后端分離的項目,之前基于Session的前后端放到一起的項目已經(jīng)慢慢失寵并淡出我們視線,尤其是當基于SpringCloud的微服務架構以及...
摘要:框架具有輕便,開源的優(yōu)點,所以本譯見構建用戶管理微服務五使用令牌和來實現(xiàn)身份驗證往期譯見系列文章在賬號分享中持續(xù)連載,敬請查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務邏輯數(shù)據(jù)訪問層和前端控制器但是忽略了對身份進行驗證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:指南無論你正在構建什么,這些指南都旨在讓你盡快提高工作效率使用團隊推薦的最新項目版本和技術。使用進行消息傳遞了解如何將用作消息代理。安全架構的主題指南,這些位如何組合以及它們如何與交互。使用的主題指南以及如何為應用程序創(chuàng)建容器鏡像。 Spring 指南 無論你正在構建什么,這些指南都旨在讓你盡快提高工作效率 — 使用Spring團隊推薦的最新Spring項目版本和技術。 入門指南 這些...
閱讀 2241·2021-11-23 09:51
閱讀 1073·2021-11-22 15:35
閱讀 4831·2021-11-22 09:34
閱讀 1597·2021-10-08 10:13
閱讀 3018·2021-07-22 17:35
閱讀 2519·2019-08-30 15:56
閱讀 3079·2019-08-29 18:44
閱讀 3089·2019-08-29 15:32