扩展SpringSecurity OAuth Server以支持多种登录方式,如短信、社交媒体或第三方token

类似的介绍有不少,这里总结一下,作为备忘,也从不同的角度进行描述一下,以共用得着的同学参考。

框架原有登录、token生成过程 

  1. client(app,网页等)发出post 请求,到auth server 的/oauth/token 端点,对应org.springframework.security.oauth2.provider.endpoint.TokenEndpoint类中的postAccessToken()方法;
  2. ClientCredentialTokenEndpointFilter拦截处理,封装请求中的登录信息为AuthenticationToken,调用attemptAuthentication() 进行验证;
  3. AuthenticationManager接管认证操作,查找匹配的AuthenticationProvider,调用其authenticate() 方法进行具体的认证处理: 如果通过,返回进一步丰富内容的AuthenticationToken,否则抛出异常;
  4. TokenEndpoint.postAccessToken() 如果拿到通过验证的AuthenticationToken,再进一步验证(scope,grantz_type)会生成TokenRequest,然后调用TokenGranter进行OAuth2AccessToken的生成,最后返回给调用的client。

扩展方案

当然可以扩展TokenEndpoint,以处理登录请求中不同的登录信息。但这种方式有些侵入性,不如下面这种方式内聚性好。那就是:

增加一套如下的类:

  1. AuthenticationFilter:用于拦截扩展的登录请求地址(例如:/oauth/extoken),并初步封装请求中的登录信息,例如电话号码,或者其他社交媒体的userid,或者其他应用系统的token(当然要有对应的验证、解析方法);
  2. AuthenticationToken:用于封装上述Filter中提取到的登录信息;
  3. AuthenticationProvider:用于验证上述自定义的AuthenticationToken中封装的信息,并进一步填充后续token获取所需要的信息;
  4. LoginSuccessHandler:用于替代TokenEndpoint中,进行token生成的操作(因为框架原有的TokenEndpoint.postAccessToken()不再执行);
  5. AuthenticationConfigurer:用于将上述功能配置到系统中。

代码片段

本文并未给出所用到的clientDetailService,和userDetailService,相信读者应该已经有了对应的实现,或者简单做一个inMemory的实现也很简单。

  1. Filter
    package stoney.exer.spring.authsvr.exauth;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.authentication.AuthenticationEventPublisher;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 一个通用的外部授权Filter。生成Bean时,可以指定路径,和请求参数 key
     */
    @Slf4j
    
    public class Ex1AuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
        @Setter@Getter
        private AuthenticationEventPublisher eventPublisher;
        private String exTokenKey ;
    
        private boolean postOnly = true;
    
    
        /**
         * 
         * @param authPath 请求登录的路径
         * @param exTokenKey 约定的Header中的token key
         */
        public Ex1AuthenticationFilter(String authPath,String exTokenKey) {
            super(new AntPathRequestMatcher(authPath, "POST"));
            this.exTokenKey = exTokenKey;
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException, IOException, ServletException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String extoken = obtainToken(request);
            if (extoken == null) {
                extoken = "";
            }
            extoken = extoken.trim();
    
            AbstractAuthenticationToken authRequest = new Ex1AuthenticationToken(extoken,extoken , null);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            Authentication authResult = null;
            try {
                authResult = this.getAuthenticationManager().authenticate(authRequest);
    
                log.debug("Authentication success: " + authResult);
                SecurityContextHolder.getContext().setAuthentication(authResult);
    
            } catch (Exception failed) {
                SecurityContextHolder.clearContext();
                log.debug("Authentication request failed: " + failed);
    
            }
            return authResult;
        }
    
        protected String obtainToken(HttpServletRequest request) {
            String token = request.getParameter(exTokenKey);
            if (token == null ) {
                token = request.getHeader(exTokenKey);
            }
    
            return token;
        }
    
    
        protected void setDetails(HttpServletRequest request,
                                  AbstractAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
    }

     

  2. Token
    package stoney.exer.spring.authsvr.exauth;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.SneakyThrows;
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    
    import java.util.Collection;
    
    
    //若标记为@Transient暂态,HttpSessionSecurityContextRepository 将不会存储这个Authentication;
    // 若在sessionCreationPolicy 为 STATELESS 时,也可以不标记
    //@Transient
    @Getter @Setter
    public class Ex1AuthenticationToken extends AbstractAuthenticationToken {
        private static final long serialVersionUID = -2826984876386002624L;
    
        protected  Object principal;
        protected String exToken;
    
        public Ex1AuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            super.setAuthenticated(false);
        }
    
        public Ex1AuthenticationToken(Object principal, String exToken,
                                      Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            this.exToken = exToken;
            super.setAuthenticated(true);
        }
        @Override
        @SneakyThrows
        public void setAuthenticated(boolean isAuthenticated) {
            if (isAuthenticated) {
                throw new IllegalArgumentException(
                        "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
            }
    
            super.setAuthenticated(false);
        }
        @Override
        public Object getCredentials() {
            return null;
        }
    
        @Override
        public Object getPrincipal() {
            return principal;
        }
    
        @Override
        public void eraseCredentials() {
            super.eraseCredentials();
        }
    }
    

     

  3. Provider
    package stoney.exer.spring.authsvr.exauth;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.support.MessageSourceAccessor;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.SpringSecurityMessageSource;
    import org.springframework.security.core.userdetails.UserDetails;
    import stoney.exer.spring.authsvr.service.MemoryUserService;
    
    @Slf4j
    @Getter @Setter
    public class Ex1AuthenticationProvider implements AuthenticationProvider {
        private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
        @Autowired
        private MemoryUserService userDetailsService;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // TODO: 如果是针对外部token,此处要做token解析以得到对应的本系统用户信息;或者是电话号码,此处要做短信码验证;或者是微信openid,此处要做绑定关系查询
            Ex1AuthenticationToken exAuthToken = (Ex1AuthenticationToken)authentication;
            String exToken = exAuthToken.getExToken();
            // 假设解析出来的uid已经设置为用户信息
            String principal = authentication.getPrincipal().toString();
            //TODO: 假设解析出来的用户就是user+token。此处一定要改为自己的用户获取逻辑!!!
            principal = "user"+exToken;
            UserDetails userDetails = userDetailsService.loadUserByUsername(principal);
            if (userDetails == null) {
                log.debug("Authentication failed: no credentials provided");
    
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.noopBindAccount",
                        "没有绑定账号:"+principal));
            }
    
            // 检查账号状态:锁定,禁用等等
    //        detailsChecker.check(userDetails);
    
            // 填充userDetails 和 Authorities
            Ex1AuthenticationToken authenticationToken = new Ex1AuthenticationToken(userDetails.getUsername(), exToken,
                    userDetails.getAuthorities()); // 如果对应本系统正常用户,可以在此提取权限信息
            authenticationToken.setDetails(authenticationToken.getDetails());
            return authenticationToken;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return  Ex1AuthenticationToken.class.isAssignableFrom(aClass);
        }
    }
    

     

  4. Handler
    package stoney.exer.spring.authsvr.exauth;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.Builder;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
    import org.springframework.security.oauth2.provider.*;
    import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
    import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    import stoney.exer.common.auth.AuthUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.HashMap;
    
    
    @Builder
    //@Component
    @Slf4j
    public class Ex1LoginSuccessHandler implements AuthenticationSuccessHandler {
        private static final String BASIC_ = "Basic ";
        private ObjectMapper objectMapper;
        private PasswordEncoder passwordEncoder;
        private ClientDetailsService clientDetailsService;
        private AuthorizationServerTokenServices defaultAuthorizationServerTokenServices;
    
        /**
         * Called when a user has been successfully authenticated.
         * 调用spring security oauth API 生成 oAuth2AccessToken
         * 如果不进行 生成 oAuth2AccessToken这一步,认证虽然可以成功(在有Session的情况下,会保存SessionID),
         * 但无法返回token
         *
         * @param request        the request which caused the successful authentication
         * @param response       the response
         * @param authentication the <tt>Authentication</tt> object which was created during
         */
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
            log.debug("Authentiation Success event!");
            String header = request.getHeader(HttpHeaders.AUTHORIZATION);
    
    //        if(true) return;
    
            if (header == null || !header.startsWith(BASIC_)) {
                throw new UnapprovedClientAuthenticationException("请求头中client信息为空");
            }
    
            try {
                String[] tokens = AuthUtils.extractAndDecodeHeader(header);
                assert tokens.length == 2;
                String clientId = tokens[0];
    
                ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
    
    
                TokenRequest tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "external");
    
                //校验scope
                new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
                OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
                OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
                OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication);
                log.info(" 获取token 成功:{}", oAuth2AccessToken.getValue());
    
                response.setCharacterEncoding("UTF-8");
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);  // APPLICATION_JSON_UTF8_VALUE is deprecated
                PrintWriter printWriter = response.getWriter();
                printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken));
            } catch (
                    IOException e) {
                throw new BadCredentialsException(
                        "Failed to decode basic authentication token");
            }
            clearAuthenticationAttributes(request);
    
        }
        protected final void clearAuthenticationAttributes(HttpServletRequest request) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                session.removeAttribute("SPRING_SECURITY_LAST_EXCEPTION");
            }
        }
    }
    

     

  5. Configurer
    package stoney.exer.spring.authsvr.exauth;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.Getter;
    import lombok.Setter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.security.authentication.AuthenticationEventPublisher;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.provider.ClientDetailsService;
    import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
    import org.springframework.security.web.DefaultSecurityFilterChain;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import stoney.exer.spring.authsvr.service.MemoryUserService;
    
    @Setter @Getter
    @Slf4j
    public class Ex1AuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
        @Autowired // 来自 MemoryUserService 的@Component
        private MemoryUserService userDetailsService;
        @Autowired // 来自框架
        private AuthenticationEventPublisher defaultAuthenticationEventPublisher;
        @Lazy @Autowired // 来自本文件 @Bean定义
        private AuthenticationSuccessHandler ex1LoginSuccessHandler;
    
        @Override
        public void configure(HttpSecurity http)  throws Exception {
            log.debug("Configuring Ex1AuthenticationFilter");
            Ex1AuthenticationFilter ex1AuthenticationFilter = new Ex1AuthenticationFilter("/ex1token","ex1token");
    
            ex1AuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    //        ex1AuthenticationFilter.setEventPublisher(defaultAuthenticationEventPublisher);
    //        ExtAuthenticationFilter.setAuthenticationEntryPoint(new ResourceAuthExceptionEntryPoint(objectMapper));
            ex1AuthenticationFilter.setAuthenticationSuccessHandler(ex1LoginSuccessHandler);
            log.debug("LoginSuccessHandler for {} is {}",ex1AuthenticationFilter,ex1LoginSuccessHandler);
    
            Ex1AuthenticationProvider ex1AuthenticationProvider = new Ex1AuthenticationProvider();
            ex1AuthenticationProvider.setUserDetailsService(userDetailsService);
            log.debug("UserDetailService for {} is {}",ex1AuthenticationProvider,userDetailsService);
    
            http
                   
                    // 加入 AuthenticationProvider 和 AuthenticationFilter
                    .authenticationProvider(ex1AuthenticationProvider)
                    .addFilterBefore(ex1AuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            ;
    
            
        }
    
        @Bean("ex1LoginSuccessHandler")
        public Ex1LoginSuccessHandler ex1LoginSuccessHandler(
                @Autowired ObjectMapper objectMapper, // 来自框架
                @Autowired ClientDetailsService clientDetailsService, // 来自 ??
                @Autowired PasswordEncoder passwordEncoder, // 来自SecurityConfig @Bean 定义
                @Lazy @Autowired AuthorizationServerTokenServices defaultAuthorizationServerTokenServices) // 来自框架
        {
            log.debug("~~~ ex1LoginSuccessHandler Bean init info:\n\tObject Mapper: {},\n\tclientDetailService: {}," +
                            "\n\tPasswordEncoder: {},\n\tAuthorizationServerTokenServices: {}",
                    objectMapper,clientDetailsService,passwordEncoder,defaultAuthorizationServerTokenServices);
            return Ex1LoginSuccessHandler.builder()
                    .objectMapper(objectMapper)
                    .clientDetailsService(clientDetailsService)
                    .passwordEncoder(passwordEncoder)
                    .defaultAuthorizationServerTokenServices(defaultAuthorizationServerTokenServices).build();
        }
    }
    

     

  6. 其他工具类
    package stoney.exer.common.auth;
    
    
    import lombok.SneakyThrows;
    import lombok.experimental.UtilityClass;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    
    import javax.servlet.http.HttpServletRequest;
    import java.nio.charset.StandardCharsets;
    import java.util.Base64;
    
    /**
     * 认证授权相关工具类
     */
    @Slf4j
    @UtilityClass
    public class AuthUtils {
    	private final String BASIC_ = "Basic ";
    
    	/**
    	 * 从header 请求中的clientId/clientsecect
    	 *
    	 * @param header header中的参数
    	 * @throws RuntimeException if the Basic header is not present or is not valid
    	 *                          Base64
    	 */
    	@SneakyThrows
    	public String[] extractAndDecodeHeader(String header) {
    
    		byte[] base64Token = header.substring(6).getBytes("UTF-8");
    		byte[] decoded;
    		try {
    			decoded = Base64.getUrlDecoder().decode(base64Token);
    		} catch (IllegalArgumentException e) {
    			throw new RuntimeException(
    					"Failed to decode basic authentication token");
    		}
    
    		String token = new String(decoded, StandardCharsets.UTF_8);
    
    		int delim = token.indexOf(":");
    
    		if (delim == -1) {
    			throw new RuntimeException("Invalid basic authentication token");
    		}
    		return new String[]{token.substring(0, delim), token.substring(delim + 1)};
    	}
    
    	/**
    	 * *从header 请求中的clientId/clientsecect
    	 *
    	 * @param request
    	 * @return
    	 */
    	@SneakyThrows
    	public String[] extractAndDecodeHeader(HttpServletRequest request) {
    		String header = request.getHeader(HttpHeaders.AUTHORIZATION);
    
    		if (header == null || !header.startsWith(BASIC_)) {
    			throw new RuntimeException("请求头中client信息为空");
    		}
    
    		return extractAndDecodeHeader(header);
    	}
    }
    

     

配置方法

在auth server原有的配置(WebSecurityConfigurerAdapter)中,加入:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
// TODO: 此处略去原有配置若干行 ...

                // 添加外部token支持的配置。若希望是无状态 token验证(比较严格),应加上SessionCreationPolicy.STATELESS 配置
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .apply(ex1AuthenticationConfigurer);
        ;
}

 @Bean
    public Ex1AuthenticationConfigurer ex1AuthenticationConfigurer()
    {
        Ex1AuthenticationConfigurer ex1AuthenticationConfigurer = new Ex1AuthenticationConfigurer();

        return ex1AuthenticationConfigurer;
    }

注意上述配置中,加入了sessionCreationPolicy(SessionCreationPolicy.STATELESS) 的内容,这是因为如果不这样设置,对应的请求会使用HttpSessionSecurityCHttpSessionSecurityContextRepositoryontextRepository存储session(而这还会在一些框架版本组合中引起tomcat的一个AbstractMethod异常),对于分布式服务环境来说没有必要。这样设置之后,框架会用NullSecurityContextRepository 去代替 HttpSessionSecurityContextRepository, 从而不在session中存储token,而要求每次请求必须带有token,强制验证。

请求方法

curl -X POST -i -u order-client:order-secret-8888 "http://localhost:6081/ex1token" -H "ex1token: 1234567890"

对比之前原有的请求:

curl -X POST -i "http://localhost:6081/oauth/token?grant_type=password&client_id=client1&client_secret=secret1&username=user1&password=123456&scope=all"

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值