认证服务用于一致管理用户信息,用于解决各个服务各自维护一套用户体系导致用户信息无法一致的问题。依托于spring-security + OAuth2的结构规划,维护两套对外认证方法:
  • 一:经典的OAuth2授权码形式,针对非内部项目,运用授权码形式维护用户信息安全性,获取用户信息也有呼应的权限控制。
  • 二:内部运用的认证方法,运用账号密码/手机号验证码登录,依托spring-security结构拓宽完成。 本次首要介绍第二种内部运用认证方法,第一种方法现已在上一篇博客介绍。 本文涉及到spring-security结构的扩展知识,可能会一笔带过,各位看官能够在spring-security官网检查扩展点。

一、内部运用认证流程

统一认证服务内部设计
能够发现,这儿认证和外部接入的恳求方法是相同的,差异点是登录直接运用认证服务提供的登录页面或接口,而无需用户授权页。

二、认证服务的内部编码规划

内部认证方法依托于spring-security结构扩展,现在扩展出手机号验证码登录。详细完成如下:

2.1、创立认证Filter节点

创立一个Filter节点用于阻拦登录恳求。

  • 这儿装备DelegatingAuthenticationConverte用于装备不同的Token类型转化器。
@Slf4j
public class UserAuthenticationFilter extends AbstractAuthenticationProcessingFilter implements Ordered {
    public static final String DEFAULT_LOGIN_PATH = "/auth/login";
    public static final String USER_AUTH_HEADER_KEY = "A_SPID"; // auth service permit id
    private final String loginPath = DEFAULT_LOGIN_PATH;
    @Setter @Getter private AuthenticationConverter authenticationConverter = new DelegatingAuthenticationConverter(Arrays.asList(new DefaultAuthenticationConverter()));
/**
	... 中心省掉结构器
**/
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
		/*
			这儿解析恳求信息,结构Token,依据不同的Token类型执行不同的认证流程
		*/
        Authentication requestAuthentication = authenticationConverter.convert(request);
        if(log.isDebugEnabled()) {
            log.debug("[DEBUG]UserAuthenticationFilter -> requestAuthentication[{}]", requestAuthentication);
        }
		 /*
			 开端认证
		 */
        return getAuthenticationManager().authenticate(requestAuthentication);
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

2.2、创立AuthenticationConverter转化器

AuthenticationConverter转化器用于将HttpServletRequest转化成Authentication对象,认证责任链会依据详细的Authentication完成来确定Support类,详细如下:

统一认证服务内部设计

故咱们拓宽一下这个convert用于支撑手机号登录,扩展代码如下:

public class MobileAuthenticationConverter extends AbstractAuthenticationConverter {
    @Setter private String mobileKey = "mobile";
    @Setter private String smsCodeKey = "smsCode";
    private static final ParameterizedTypeReference<MobileLogin> STRING_OBJECT_MAP =
        new ParameterizedTypeReference<MobileLogin>() {};
    @Override
    public Authentication convert(HttpServletRequest request) {
		// 支撑form
        String mobile = findMobileValue(request);
        String smsCode = findSmsCodeValue(request);
        if(StringUtils.isBlank(mobile)) {
            try {
	            // 支撑json
                MobileLogin mobileLogin = parsePostJson(request);
                mobile = mobileLogin.getMobile();
                smsCode = mobileLogin.getSmsCode();
            } catch (IOException e) {
                return null;
            }
        }
        if(StringUtils.isBlank(mobile)) {
            // 这儿如果是空,则代表判别下一个Convert
            return null;
        }
        return new MobileAuthenticationToken(mobile, smsCode);
    }
    public MobileLogin parsePostJson(HttpServletRequest request) throws IOException {
        GenericHttpMessageConverter<Object> jsonHttpMessageConverter = HttpMessageConverters.getJsonMessageConverter();
        MobileLogin mobileLogin = (MobileLogin) jsonHttpMessageConverter.read(STRING_OBJECT_MAP.getType(), null, new ServletServerHttpRequest(request));
        return mobileLogin;
    }
    private String findMobileValue(HttpServletRequest request) {
        return request.getParameter(mobileKey);
    }
    private String findSmsCodeValue(HttpServletRequest request) {
        return request.getParameter(smsCodeKey);
    }
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 10 * 10000;
    }
}

2.3、创立AuthenticationProvider认证处理器

AuthenticationProvider会依据Convert转化的Authentication类型,确定是否要执行。这儿扩展手机号的认证处理器,如下:

@Component
public class MobileAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider implements Ordered {
    @Getter private final UserDetailsService userDetailsService;
    @Getter private final PasswordEncoder passwordEncoder;
    private final SmsCodeStoreService smsCodeStoreService;
    private final Oauth2ApplicationUserRepository userRepository;
    public MobileAuthenticationProvider(@Qualifier("mobileApplicationUserDetailsService") UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, SmsCodeStoreService smsCodeStoreService, Oauth2ApplicationUserRepository userRepository) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
        this.smsCodeStoreService = smsCodeStoreService;
        this.userRepository = userRepository;
    }
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
        if (mobileAuthenticationToken.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String mobile = mobileAuthenticationToken.getName();
        String smsCode = mobileAuthenticationToken.getCredentials().toString();
        if(!smsCodeStoreService.hasSmsCode(mobile)) {
            this.logger.debug("短信息验证码未发送或许已过期!");
            throw new BadCredentialsException("短信息验证码未发送或许已过期");
        }
        String storeSmsCode = smsCodeStoreService.getSmsCode(mobile);
        if (StringUtils.isBlank(storeSmsCode) || !storeSmsCode.equals(smsCode)) {
            this.logger.debug(format("短信息验证码验证失利!恳求验证码:[%s], store验证码:[%s]", smsCode, storeSmsCode));
            throw new BadCredentialsException("短信息验证码验证失利");
        }
        // 验证码过期
        smsCodeStoreService.evictSmsCode(mobile);
    }
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (Objects.isNull(loadedUser)) {
                loadedUser = helpRegister(username);
                if(Objects.isNull(loadedUser)) {
                    throw new InternalAuthenticationServiceException(
                        "MobileUserDetailsService returned null, which is an interface contract violation");
                }
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
    private UserDetails helpRegister(String mobile) {
        // check sms
        if(!smsCodeStoreService.hasSmsCode(mobile)) {
            throw new RegisterException("验证码不正确!");
        }
        String username = RandomUtils.generateCode();
        String userUniqueCode = RandomUtils.generateCode();
        Oauth2ApplicationUser newUser = new Oauth2ApplicationUser();
        newUser.setMobile(mobile);
        newUser.setUserName(username);
        newUser.setUserUniqueCode(userUniqueCode);
        Oauth2ApplicationUser saved = userRepository.save(newUser);
        return this.getUserDetailsService().loadUserByUsername(saved.getMobile());
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }
    @Override
    public int getOrder() {
        return ProviderOrdered.MOBILE_AUTHENTICATION_PROVIDER.getOrder();
    }
}

在这儿认证成功之后依据用户信息返回access_token,认证流程就完成了。

三、内部服务调用方法

提供过滤器为其他需求校验token的服务。 过滤器流程如下,流程固定,内部运用TokenCheckService用于扩展接口。

public class CheckFilter implements Filter {
    private final String authHeaderKey;
    private final TokenCheckService tokenCheckService;
    public CheckFilter(String authHeaderKey, TokenCheckService tokenCheckService) {
        this.authHeaderKey = authHeaderKey;
        this.tokenCheckService = tokenCheckService;
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest r = (HttpServletRequest) request;
        String uri = r.getRequestURI();
        tokenCheckService.printer("执行 CheckFilter uri:" + uri);
        if(tokenCheckService.whiteList(uri)) {
            chain.doFilter(request, response);
            return ;
        }
        String token = findTokenFromRequestHeader(r);
        if(!tokenCheckService.checkToken(token)) {
            tokenCheckService.printer("token invalid, token -> " + token);
            boolean continuing = tokenCheckService.invalidTokenHandler(token, request, response);
            if(!continuing) {
                return ;
            }
        }
        try {
            tokenCheckService.parseToken(token);
            chain.doFilter(request, response);
        } finally {
        }
    }
    private String findTokenFromRequestHeader(HttpServletRequest request) {
        String token = request.getHeader(authHeaderKey);
        return token;
    }
}

TokenCheckService中可装备获取用户信息后存入线程缓存用于后续接口中运用。

四、总结

至此,一个简单的认证服务内部逻辑就完成了,依托于spring-security结构能够快速的集成认证方法。 各位看客如果有问题或许主张能够评论区发表哦,我看到会即使回复的。