Spring Security OAuth2 源码解析 (一)

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】
免费领取 2000+ 道 Java 面试题

有的老铁可能还没没怎么了解OAuth2,没关系给你一个链接去先去看看

http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

OAuth 2.0 定义了四种授权方式:

  1. 授权码模式(authorization code)
  2. 简化模式(implicit)
  3. 密码模式(resource owner password credentials)
  4. 客户端模式(client credentials)

重点讲一讲密码模式(也就是我目前碰到的)

首先我们可以找到位于org.springframework.security.oauth2.provider.endpoint这里面的TokenEnEndpoint类有两个端点(都一样);在里面我们看到很熟悉的@ReRequestMaping(Principal principal,@RequestParam Map<String,String> parameters),里面有俩个参数.解释下:

  • principal :这个其实是在Filter阶段就已经认证好的客户端信息(有兴趣的可以去debug一下);
  • parameters:这个就是前端传递过去的参数啦;
            @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
        public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
        Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

            if (!(principal instanceof Authentication)) {
                throw new InsufficientAuthenticationException(
                        "There is no client authentication. Try adding an appropriate authentication filter.");
            }

            String clientId = getClientId(principal);
            ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

            TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

            if (clientId != null && !clientId.equals("")) {
                // Only validate the client details if a client authenticated during this
                // request.
                if (!clientId.equals(tokenRequest.getClientId())) {
                    // double check to make sure that the client ID in the token request is the same as that in the
                    // authenticated client
                    throw new InvalidClientException("Given client ID does not match authenticated client");
                }
            }
            if (authenticatedClient != null) {
                oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
            }
            if (!StringUtils.hasText(tokenRequest.getGrantType())) {
                throw new InvalidRequestException("Missing grant type");
            }
            if (tokenRequest.getGrantType().equals("implicit")) {
                throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
            }

            if (isAuthCodeRequest(parameters)) {
                // The scope was requested or determined during the authorization step
                if (!tokenRequest.getScope().isEmpty()) {
                    logger.debug("Clearing scope of incoming token request");
                    tokenRequest.setScope(Collections.<String> emptySet());
                }
            }

            if (isRefreshTokenRequest(parameters)) {
                // A refresh token has its own default scopes, so we should ignore any added by the factory here.
                tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
            }

            OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
            if (token == null) {
                throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
            }

            return getResponse(token);

        }

(认证环节)重点看:这段代码

    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
  • getTokenGranter():这个方法主要就是为了拿到tokenGranter,从这个类的set方法中我们可以看到tokenGranter值其实是从AuthorizationServerEndpointsConfiguration这个类中被赋值的(实例化了TokenEndpoint),我们可以通过继承AuthorizationServerConfigurerAdapterz这个适配器来自定义配置AuthorizationServerEndpointsConfigurer内的属性,从而配置认证授权端点配置
  • grant():听名字就知道大概的意思,接下来的操作都在里面(这里的grant()调用的是在AuthorizationServerEndpointsConfigurer类中的方法,可以仔细去看看,意思就是说默认使用CompositeTokenGranter实现先去遍历)来段代码看看

        private TokenGranter tokenGranter() {
                if (tokenGranter == null) {
                    tokenGranter = new TokenGranter() {
                        private CompositeTokenGranter delegate;
    
                        @Override
                        public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                            if (delegate == null) {
                                delegate = new CompositeTokenGranter(getDefaultTokenGranters());
                            }
                            return delegate.grant(grantType, tokenRequest);
                        }
                    };
                }
                return tokenGranter;
            }

    这是CompositeTokenGranter中的代码

        public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                for (TokenGranter granter : tokenGranters) {
                    OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
                    if (grant!=null) {
                        return grant;
                    }
                }
                return null;
            }

TokenGranter接口

grant()方法就在这个接口中的方法;他被AbstractTokenGranter抽象类实现,而AbstractTokenGranter又有分别被下面4个继承(我们也可以自定义一个XxxTokenGranter类去继承这样我们就可以重写里面的方法了)

  1. AuthorizationCodeTokenGranter : 授权码模式
  2. ClientCredentialsTokenGranter : 客户端模式
  3. ImplicitTokenGranter : 简化模式
  4. ResourceOwnerPasswordTokenGranter : 密码模式

TokenGranter还有被一个叫CompositeTokenGranter的类实现,会根据你前端传递的参数grant_type来判断到底进哪个模式

而这个模式也是可以自定义的 ,也就是上面图中的tokenGranter()方法回默认用CompositeTokenGranter实现先去遍历(4中模式),

这里的默认也就是说在AuthorizationServerEndpointsConfigurer类中未配置TokenGranter这个属性,如果要配置的其实也很简单

看下面:

AuthorizationServerConfigurer 和 AuthorizationServerConfigurerAdapter

我们要做的只是写一个配置类来继承AuthorizationServerConfigurerAdapter这个适配器,重写三个configure方法


    /**
     * @author Dave Syer
     *
     */
    public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        }

    }

其中 endpoints.tokenGranter()就是配置让我们自定义的配置tokenGranter的,建议配置CompositeTokenGranter这个.

因为只有子类ClientCredentialsTokenGranter重写了grant(),其他子类重用父类grant()方法;其实在这里我们也可以自己去继承AbstractTokenGranter类来自定义一个XxGranter;然后在自定义的认证配置里面(也就是继承AuthorizationServerConfigurerAdapter适配器)的endpoints端点配置中去搞他.

    @Autowired
        private AuthenticationManager authenticationManager;

        @Autowired
        private Environment env;

        @Autowired
        private UserService userService;

        @Autowired
        private CaptchaService captchaService;

        @Autowired
        private TokenStore tokenStore;

        @Autowired
        private TokenEnhancerChain tokenEnhancerChain; 

    @Configuration
    @EnableAuthorizationServer
    public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore)
                    .tokenEnhancer(tokenEnhancerChain)
                    .authenticationManager(authenticationManager);

            endpoints.tokenGranter(tokenGranter(endpoints))
                    .userDetailsService(userService)
                    .reuseRefreshTokens(true);
    }

            private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer configurer) {
            AuthorizationServerTokenServices tokenService = configurer.getTokenServices();
            OAuth2RequestFactory requestFactory = configurer.getOAuth2RequestFactory();
            ClientDetailsService clientDetailsService = configurer.getClientDetailsService();

            List<TokenGranter> tokenGranters = new ArrayList<>();
            tokenGranters.add(new ClientCredentialsTokenGranter(tokenService, clientDetailsService, requestFactory));
            tokenGranters.add(new RefreshTokenGranter(tokenService, clientDetailsService, requestFactory));
            //UsernamePasswordGranter就是我们需要的自定义的XXGranter继承AbstractTokenGranter类
            UsernamePasswordGranter tokenGranter =
                    new UsernamePasswordGranter(authenticationManager,
                            tokenService, clientDetailsService,
                            requestFactory, captchaService,
                            userService);
            tokenGranters.add(tokenGranter);

            return new CompositeTokenGranter(tokenGranters);
        }

    }

不管是不是自定义Granter,认证还是流程还是必须走AuthenticationManager接口的authenticate方法滴!然后就是走ProvideManager中的authenticate方法去遍历providers(认证提供者好多个)其中的authenticate方法

关于UserDetail 和 UserDetailsService

首先你的User类你的必须实现UserDetail接口和里面的参数, UserDetailsService中的loadUserByUsername()方法在认证(authenticate方法)过程中是肯定要用到.一般来说我们用UserService去继承UserDetailsService然后在他是实现类里面UserServiceImpl去实现loadUserByUsername()方法.

关于authenticate方法,也就是真正的认证了

其实跟前面的TokenGranter也有点类似, 1.大总管(AuthenticationManager) 2.小总管(ProviderManager) 3…..然后后面…..

在ProviderManager中的authenticate方法中会遍历List providers这个认证供应者(百度一下),AuthenticationProvider是一个接口,所有每个provider的实现中又有一个authenticate方法,好了进入这里就是真正的认证了.

在这里最好自己打个断点看看往哪走,在进入AbstractUserDetailsAuthenticationProvider实现下面放代码


    package org.springframework.security.authentication.dao;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.security.authentication.AccountExpiredException;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.CredentialsExpiredException;
    import org.springframework.security.authentication.DisabledException;
    import org.springframework.security.authentication.LockedException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.SpringSecurityMessageSource;
    import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
    import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
    import org.springframework.security.core.userdetails.UserCache;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsChecker;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.core.userdetails.cache.NullUserCache;

    import org.springframework.beans.factory.InitializingBean;

    import org.springframework.context.MessageSource;
    import org.springframework.context.MessageSourceAware;
    import org.springframework.context.support.MessageSourceAccessor;

    import org.springframework.util.Assert;

    public abstract class AbstractUserDetailsAuthenticationProvider implements
            AuthenticationProvider, InitializingBean, MessageSourceAware {

        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));

            // Determine username
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                    : authentication.getName();

            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);

            if (user == null) {
                cacheWasUsed = false;

                try {
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                catch (UsernameNotFoundException notFound) {
                    logger.debug("User '" + username + "' not found");

                    if (hideUserNotFoundExceptions) {
                        throw new BadCredentialsException(messages.getMessage(
                                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                                "Bad credentials"));
                    }
                    else {
                        throw notFound;
                    }
                }

                Assert.notNull(user,
                        "retrieveUser returned null - a violation of the interface contract");
            }

            try {
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    // There was a problem, so try again after checking
                    // we're using latest data (i.e. not from the cache)
                    cacheWasUsed = false;
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                    preAuthenticationChecks.check(user);
                    additionalAuthenticationChecks(user,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
                else {
                    throw exception;
                }
            }

            postAuthenticationChecks.check(user);

            if (!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }

            Object principalToReturn = user;

            if (forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }

            return createSuccessAuthentication(principalToReturn, authentication, user);
        }

    }

这里应该基本上没啥问题,仔细看一下就明白了. 其中retrieveUser()点进去看就能明白我们之前继承UserDetailService实现的loadUserByUsername()就会被在这调用到, 还有一个就是校验密码的时候—-additionalAuthenticationChecks()方法点进去看

            protected void additionalAuthenticationChecks(UserDetails userDetails,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            Object salt = null;

            if (this.saltSource != null) {
                salt = this.saltSource.getSalt(userDetails);
            }

            if (authentication.getCredentials() == null) {
                logger.debug("Authentication failed: no credentials provided");

                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }

            String presentedPassword = authentication.getCredentials().toString();

            if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
                    presentedPassword, salt)) {
                logger.debug("Authentication failed: password does not match stored value");

                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
        }

重点在密码验证这if(!passwordEncoder.isPasswordVlid(userDetails.getPassword(),presentedPassword.salt)){…………..},还有前面的UserDetailService的loadUserByUsernam()方法~~哎奇怪是不是感觉少个配置啊,就是下面这个东东

其实我们还需要一个继承WebSecurityConfigConfigurerAdpter的Security配置类

先上个图看看吧

    package com.hz.coreconfig;

    import com.hz.pojo.User;
    import com.hz.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    /**
     * @author hz
     * @date 2018-8-22 16:10
     */
    @Slf4j
    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        UserService userService;

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }

        /**
         * userDetailsService(userService):配置用户服务
         * passwordEncoder(User.passwordEncoder):配置密码校验规则,在DaoAuthenticationProvider类中
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth.userDetailsService(userService)
                     .passwordEncoder(new BCryptPasswordEncoder(11));
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().permitAll();
        }
    }

在.userDetailService()中配置的就是我们自己的LoadUserByUsername()方法, .passwordEncode()中配置的就是我们需要密码校验的格式;

好了基本上/oauth/token的流程就是这样了,我也是看了好几天源码,边看边debug才明白的.其实刚开始看真的很懵逼,没办法只能一步一步debug下去.这一字一句都是我自己亲自敲出来的,很开心这是我第一篇博客(其实光写这个篇我花了半天时间,生怕哪里写错了,然后被大佬指出来~~~那就很low很尴尬了),但肯定不是最后一篇!

当然错误肯定会有,希望看官大佬不要吝啬指教~


来源:http://ddrv.cn/a/88268

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Spring Security OAuth2 源码解析 (一)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏