基于SpringCloud作微服务架构分布式系统时,OAuth2.0做为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring Boot环境下使用OAuth2.0,提供了开箱即用的组件。可是在开发过程当中咱们会发现因为Spring Security OAuth2的组件特别全面,这样就致使了扩展很不方便或者说是不太容易直指定扩展的方案,例如:git
在面对这些场景的时候,预计不少对Spring Security OAuth2不熟悉的人恐怕会无从下手。基于上述的场景要求,如何优雅的集成短信验证码登陆及第三方登陆,怎么样才算是优雅集成呢?有如下要求:web
基于上述的设计要求,接下来将会在文章种详细介绍如何开发一套集成登陆认证组件开知足上述要求。redis
阅读本篇文章您须要了解OAuth2.0认证体系、SpringBoot、SpringSecurity以及Spring Cloud等相关知识
咱们来看下Spring Security OAuth2的认证流程:spring
这个流程当中,切入点很少,集成登陆的思路以下:小程序
接入这个流程以后,基本上就能够优雅集成第三方登陆。微信小程序
介绍完思路以后,下面经过代码来展现如何实现:设计模式
/** * @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)){ //设置集成登陆信息 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<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; } }
在这个类种主要完成2部分工做:一、根据参数获取当前的是认证类型,二、根据不一样的认证类型调用不一样的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);方法,将拦截器放入到认证链条中。架构
@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("用户名或密码错误"); } 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; } }
这里实现了一个IntegrationUserDetailsService ,在loadUserByUsername方法中会调用authenticate方法,在authenticate方法中会当前上下文种的认证类型调用不一样的IntegrationAuthenticator 来获取用户信息,接下来来看下默认的用户名密码是如何处理的:app
@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"); //验证验证码 Result<Boolean> result = 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(); //发布事件,能够监听事件进行自动注册用户 this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication)); //经过手机号码查询用户 UserVO userVo = this.ucClient.findUserByPhoneNumber(username); if (userVo != null) { //将密码设置为验证码 userVo.setPassword(passwordEncoder.encode(password)); //发布事件,能够监听事件进行消息通知 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("验证码错误或已过时"); } } @Override public boolean support(IntegrationAuthentication integrationAuthentication) { return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType()); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }
SmsIntegrationAuthenticator会对登陆的短信验证码进行预处理,判断其是否非法,若是是非法的则直接中断登陆。若是经过预处理则在获取用户信息的时候经过手机号去获取用户信息,并将密码重置,以经过后续的密码校验。
在这个解决方案中,主要是使用责任链和适配器的设计模式来解决集成登陆的问题,提升了可扩展性,并对spring的源码无污染。若是还要继承其余的登陆,只须要实现自定义的IntegrationAuthenticator就能够。
项目地址:https://gitee.com/leecho/cola...你们有好的建议和想法能够一块儿沟通交流。