Preface
When using SpringCloud to build a distributed system with microservice architecture, OAuth2.0 is the industry standard for certification. Spring Security OAuth2 also provides a complete set of solutions to support the use of OAuth2.0 in the Spring Cloud/Spring Boot environment, providing out-of-the-box components. However, during the development process, we will find that because the components of Spring Security OAuth2 are particularly comprehensive, this makes it very inconvenient to extend or is not easy to directly specify the extension solution, such as:
When facing these scenarios, it is expected that many people who are not familiar with Spring Security OAuth2 will not be able to start. Based on the above scenario requirements, how to elegantly integrate SMS verification code login and third-party login, and how to be considered elegantly integrated? There are the following requirements:
Based on the above design requirements, we will introduce in detail how to develop a set of integrated login authentication components to meet the above requirements in the article.
Read this article you need to know about OAuth2.0 certification system, SpringBoot, SpringSecurity, Spring Cloud and other related knowledge
Ideas
Let’s take a look at the authentication process of Spring Security OAuth2:
In this process, there are not many entry points, and the idea of integrated login is as follows:
After accessing this process, you can basically integrate third-party login elegantly.
accomplish
After introducing the ideas, the following code shows how to implement it:
The first step is to define the interceptor to intercept login requests
/** * @author LIQIU * @date 2018-3-30 **/@Componentpublic 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)){ //Set integrated login information IntegrationAuthentication integrationAuthentication = new IntegrationAuthentication(); integrationAuthentication.setAuthType(request.getParameter(AUTH_TYPE_PARM_NAME)); integrationAuthentication.setAuthParameters(request.getParameterMap()); IntegrationAuthenticationContext.set(integrationAuthentication); try{ //Preprocessing this.prepare(integrationAuthentication); filterChain.doFilter(request,response); //Post-processing this.complete(integrationAuthentication); } finally { IntegrationAuthenticationContext.clear(); } }else{ filterChain.doFilter(request,response); } } /** * Preprocessing* @param integrationAuthentication */ private void prepare(IntegrationAuthentication integrationAuthentication) { // Lazy loading authenticator 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); } } } /** * Post-processing* @param integrationAuthentication */ private void complete(IntegrationAuthentication integrationAuthentication){ for (IntegrationAuthenticator authentication: authentications) { if(authenticator.support(integrationAuthentication)){ authentication.complete(integrationAuthentication); } } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}In this class, two parts of the work are mainly completed: 1. Obtain the current authentication type according to the parameters, 2. Call different IntegrationAuthenticator.prepar according to different authentication types for preprocessing
Step 2: Put the interceptor into the intercept chain
/** * @author LIQIU * @date 2018-3-7 **/@Configuration@EnableAuthorizationServerpublic 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; }}Put the interceptor into the authentication chain by calling the security. .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter); method.
Step 3: Process user information according to the authentication type
@Servicepublic 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(); //Judge whether it is an integrated login if (integrationAuthentication == null) { integrationAuthentication = new IntegrationAuthentication(); } integrationAuthentication.setUsername(username); UserVO userVO = this.authenticate(integrationAuthentication); if(userVO == null){ throw new UsernameNotFoundException("Username or password error"); } User user = new User(); BeanUtils.copyProperties(userVO, user); this.setAuthorize(user); return user; } /** * Set authorization information* * @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; }}Here is an IntegrationUserDetailsService. The authenticate method will be called in the loadUserByUsername method. In the authenticate method, the current context authentication type will call different IntegrationAuthenticator to obtain user information. Let’s take a look at how the default username and password are handled:
@Component@Primarypublic class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator { @Autowired private UcClient ucClient; @Override public UserVO authentication(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 will only handle the default authentication type without specified authentication type. This class mainly obtains passwords through the username. Next, let’s take a look at how to handle the image verification code login:
/** * Integrated verification code authentication* @author LIQIU * @date 2018-3-31 **/@Componentpublic 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"); //Verification verification codeResult<Boolean> result = vccClient.validate(vcToken, vcCode, null); if (!result.getData()) { throw new OAuth2Exception("Verification code error"); } } @Override public boolean support(IntegrationAuthentication integrationAuthentication) { return VERIFICATION_CODE_AUTH_TYPE.equals(integrationAuthentication.getAuthType()); }}VerificationCodeIntegrationAuthenticator inherits UsernamePasswordAuthenticator because it only needs to verify whether the verification code is correct in the prepare method, and whether the user has obtained it by using the username and password. However, the authentication type is "vc" before it can be processed. Let's take a look at how the SMS verification code login is handled:
@Componentpublic 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 authentication(IntegrationAuthentication integrationAuthentication) { //Get password, the actual value is the verification code String password = integrationAuthentication.getAuthParameter("password"); //Get username, the actual value is the mobile phone number String username = integrationAuthentication.getUsername(); //Publish events, you can listen to events to automatically register the user this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication)); //Query user through mobile phone number UserVO userVo = this.ucClient.findUserByPhoneNumber(username); if (userVo != null) { //Set the password as the verification code userVo.setPassword(passwordEncoder.encode(password)); // Publish events, you can listen to events for message notification 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("Verification code error or expired"); } } @Override public boolean support(IntegrationAuthentication integrationAuthentication) { return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType()); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; }}SmsIntegrationAuthenticator will preprocess the logged-in SMS verification code to determine whether it is illegal. If it is illegal, it will directly interrupt the login. If preprocessing is passed, the user information will be obtained through the mobile phone number when obtaining the user information, and the password will be reset to pass subsequent password verification.
Summarize
In this solution, the main use of the responsibility chain and adapter design pattern to solve the problem of integrated login, improves scalability, and does not pollute the source code of spring. If you want to inherit other logins, you only need to implement a custom IntegrationAuthenticator.
Project address: https://gitee.com/leecho/cola-cloud
Local download: cola-cloud_jb51.rar
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.