统一认证服务内部设计

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

一、内部使用认证流程

可以发现,这里认证和外部接入的请求方式是相同的,差异点是登录直接使用认证服务提供的登录页面或接口,而无需用户授权页。

二、认证服务的内部编码设计

内部认证方式依托于spring-security框架扩展,目前扩展出手机号验证码登录。具体实现如下:

2.1、创建认证Filter节点

创建一个Filter节点用于拦截登录请求。

  • 这里配置DelegatingAuthenticationConverte用于配置不同的Token类型转换器。
java 复制代码
@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用于支持手机号登录,扩展代码如下:

java 复制代码
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类型,确定是否要执行。这里扩展手机号的认证处理器,如下:

java 复制代码
@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用于扩展接口。

java 复制代码
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框架可以快速的集成认证方式。 各位看客如果有问题或者建议可以评论区发表哦,我看到会即使回复的。

相关推荐
热河暖男10 分钟前
【实战解决方案】Spring Boot+Redisson构建高并发Excel导出服务,彻底解决系统阻塞难题
spring boot·后端·excel
国际云,接待3 小时前
云服务器的运用自如
服务器·架构·云计算·腾讯云·量子计算
noravinsc4 小时前
redis是内存级缓存吗
后端·python·django
noravinsc6 小时前
django中用 InforSuite RDS 替代memcache
后端·python·django
好吃的肘子6 小时前
Elasticsearch架构原理
开发语言·算法·elasticsearch·架构·jenkins
喝醉的小喵6 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
编程星空6 小时前
架构与UML4+1视图
架构
kaixin_learn_qt_ing6 小时前
Golang
开发语言·后端·golang
炒空心菜菜7 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
zkmall8 小时前
商业架构 2.0 时代:ZKmall开源商城前瞻性设计如何让 B2B2C 平台领先同行 10 年?
架构·开源