application.xml

首先引进redis的相关配置 token是要存在这里的
#Mon Nov 04 11:33:55 CST 2019
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.shutdown-timeout=100
spring.redis.lettuce.pool.max-wait=10000
spring.redis.host=127.0.0.1
spring.redis.timeout=10000

授权服务器 

AuthorizationServer1 .java

package com.example.demo;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;


@Configuration
@EnableAuthorizationServer
public class AuthorizationServer1 extends
        AuthorizationServerConfigurerAdapter {

	@Autowired
	private RedisConnectionFactory redisConnectionFactory;

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private AuthenticationManager authenticationManager;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
                .withClient("password1")
                .authorizedGrantTypes("password","refresh_token")
                .accessTokenValiditySeconds(1800)
        //这个其实就是权限更粗一点的 例如ROLE_CMS_NEWS,ROLE_APP_INFO,ROLE_KVCONFIG,ROLE_NEWS_COMMENT这几个微服务的权限 由自已灵活使用 前面几个是微服务名
                .authorities("ROLE_ADMIN")
             //这个也就是权限更高一点的 针对于资源服务器 需要和资源服务器一一对应 都设置成一样就行
                .resourceIds("rid")
              //这个也是针对于资源服务器的
                .scopes("read");


    }
  


    

    //用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    	//这个会自动把认证信息 存入存入到redis 只要你配了数据源 在掉接口的时候/oauth/token
		//比如access_token key名就为 access:e18e1f70-d89d-4c70-b6bf-1f3a7036b310 在用户认证的时候可以直接拿key 看存不存在
		endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
				//这个啊 就是让他支持密码模式 否则会报 不支持 密码模式
				.authenticationManager(authenticationManager)
				//这个可以用来验证用户名 如果你自已定义的的userDetailsService 没有也得加上 默认的
				//否则会报错Handling error: IllegalStateException, UserDetailsService is required.
				.userDetailsService(userDetailsService);
	}
	
    //用来配置令牌端点(Token Endpoint)的安全约束
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security)
			throws Exception {
    	//允许check_token 这个接口是用来解token 的 看以看到token 存放具体什么信息
		security.checkTokenAccess("permitAll()")
				//主要是让/oauth/token支持client_id以及client_secret作登录认证 否则会报Unauthorized
		.allowFormAuthenticationForClients()//允许表单认证
		;
	}

	
}

资源服务器 ResouceServerConfig.class 可以配置多个资源服务器

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;

@Configuration
public class ResouceServerConfig {

    @Bean
    public WebResponseExceptionTranslator webResponseExceptionTranslator() {
        return new DefaultWebResponseExceptionTranslator() {

            @Override
            public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
                OAuth2Exception body = responseEntity.getBody();
                // 认证失败(过期)
                if (e instanceof InsufficientAuthenticationException) {
                    body.addAdditionalInformation("code", "610");
                    body.addAdditionalInformation("msg", body.getOAuth2ErrorCode());
                }
                HttpHeaders headers = new HttpHeaders();
                headers.setAll(responseEntity.getHeaders().toSingleValueMap());
                // do something with header or response
                return new ResponseEntity<>(body, headers, responseEntity.getStatusCode());
            }
        };
    }

    /**
     * 测试资源服务
     */
    @Configuration
    @EnableResourceServer
    public class ResouceTestServerConfig extends
            ResourceServerConfigurerAdapter {

//        @Autowired
//        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources)
                throws Exception {
            resources.resourceId("rid")
                    //只能是基于令牌的认证方式 默认就是true
                    .stateless(true);
            // 定义异常转换类生效
            AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
            ((OAuth2AuthenticationEntryPoint) authenticationEntryPoint).setExceptionTranslator(webResponseExceptionTranslator());
            resources.authenticationEntryPoint(authenticationEntryPoint);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            //不管配置多少个资源服务器 会自动合并成一个 没有冲突的合并 有冲突的前面覆盖后面 比如这个session
            http.sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    //下面这句其实OAUTH是根据FilterChain 里面有十几个过滤器
                    //一般有三个SecurityConfig一个 还有默认的也就是oauth/tocken oauth/check_token等等 也是一个 这个默认是默认的所有没要必要
                    //在配置什么permitAll 还有就是这个资源服务器里 不过你配置多少个资源服务器 最终会汇总到一个
                    //优先级oauthToken>资源服务器>SecurityConfig 下面这个就是匹配一下匹配到就执行这个chain匹配不到就交给下一级调用链
                    //但是假如不写这个 那他也不会到下一级 也就是说SecurityConfig的HttpSecurity
                    //基本没有调用的机会 因为默认的RequestMatch是NotOAuthRequestMatcher 最后用ReuquestMatcher调用链
                    .requestMatchers()
                    .antMatchers("/uua/**")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/uua/fegin/**").permitAll()
                    .antMatchers("/uua/fegin1/**")
                    //客户端也有更细粒度啊的权限控制
                    .access("#oauth2.hasScope('read') and #oauth2.clientHasRole('ROLE_ADMIN')")
                    //下面这个表示掐他请求都需要认证
                    .anyRequest().authenticated();
            // and
            // hasRole('ROLE_USER')

        }

    }
}

security.config

package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //这个必须要 因为比如说下面这个 他会选择不把登录填的密码加密与实际密码比对 如果是其他方式会加密
    //密码比对是有这个框架自动做的 (至少我现在写的这种方式是)
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    //尤其注意这个 AuthenticationManagerBuilder  因为父类重构了好几个configure方法认准这个就是做认证的
    //就是下面这个.anyRequest().authenticated() 会来这里认证
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("123456").roles("admin")
                .and().withUser("sang").password("123456").roles("ADMIN");
    }

    //基本没啥调用机会 因为资源服务器就给拦住了
    @Override
    protected void configure(HttpSecurity http) throws Exception {

    }

    //不定义没有password grant_type
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return super.userDetailsService();
    }


}

下面说一下调用流程 (我随便说的哦 不一定准确)

redis 认证链接方式 redis认证服务器_oauth2

看一下 /oauth/token经历了啥

总所周知 他这个需要 经历 三个步骤 用户认证 发放令牌 令牌在跑到资源服务器 去校验一下令牌 然后就去访问了

首先 用户认证吧 其实这个框架 主要就是围绕一个过滤器 来进行 就一个一个的进去过滤器进去执行

有三个过滤器

redis 认证链接方式 redis认证服务器_redis_02

匹配到那个就去执行哪个 第一个过滤器链是oauth/token 那几个默认的端点 第二个就是我们的资源服务器里 第三个就是

那个SecurityConfig 

很明显匹配到了第一个过滤器链

redis 认证链接方式 redis认证服务器_oauth2_03

 

在下面这个类里get一下客户端的相关信息  username是前台传的clientId

redis 认证链接方式 redis认证服务器_spring_04

然后把客户端的信息存入到UserDetails username是前台传clientId 然后后面把前台传过来的客户端密码和后台存的客户端密码比较一下比较的类和方法是


DaoAuthenticationProvider.additionalAuthenticationChecks


 

DaoAuthenticationProvider.class 
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
           //看见没 这个this.passwordEncoder会把前台传来密码加密一下 这下就知道 为什么必须配置这个Bean了吧
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
AbstractSecurityInterceptor.class 
//这个方法很关键 因为在具体取资源的时候这个就是关键最后的一把决定要不要放行
protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        boolean debug = this.logger.isDebugEnabled();
        if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
        } else {
//取出我当前这个访问链接需不需要认证 需要认证的话 需要什么权限
            Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
            if (attributes != null && !attributes.isEmpty()) {
                if (debug) {
                    this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
                }

                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
                }
                //我当前的链接已经有什么权限 
                Authentication authenticated = this.authenticateIfRequired();

                try {
               //把已经有的和需要有的比较一下看是否放行
                    this.accessDecisionManager.decide(authenticated, object, attributes);
                } catch (AccessDeniedException var7) {
                    this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
                    throw var7;
                }

                if (debug) {
                    this.logger.debug("Authorization successful");
                }

                if (this.publishAuthorizationSuccess) {
                    this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
                }

                Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
                if (runAs == null) {
                    if (debug) {
                        this.logger.debug("RunAsManager did not change Authentication object");
                    }

                    return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
                } else {
                    if (debug) {
                        this.logger.debug("Switching to RunAs Authentication: " + runAs);
                    }

                    SecurityContext origCtx = SecurityContextHolder.getContext();
                    SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
                    SecurityContextHolder.getContext().setAuthentication(runAs);
                    return new InterceptorStatusToken(origCtx, true, attributes, object);
                }
            } else if (this.rejectPublicInvocations) {
                throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'");
            } else {
                if (debug) {
                    this.logger.debug("Public object - authentication not attempted");
                }

                this.publishEvent(new PublicInvocationEvent(object));
                return null;
            }
        }
    }

redis 认证链接方式 redis认证服务器_spring security_05

 

这个端口有就开始真正的检验客户端权限 以及scope  上面方法的这个


OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);


就是认证用户方法Token

ResourceOwnerPasswordTokenGranter.class

 protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String username = (String)parameters.get("username");
        String password = (String)parameters.get("password");
        parameters.remove("password");
        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        Authentication userAuth;
        try {
          //去认证了
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
         
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }

这样的话就把用户信息生成token 前面有声明RedisTokenStore这个Bean 他会自动把token信息存入到redis里

redis 认证链接方式 redis认证服务器_spring_06

redis 认证链接方式 redis认证服务器_oauth2_07

再去资源服务器取得时候key+前台传的token能取到就是认证过 没取到就是没有

{
    "access_token": "7dde09dc-12d7-4dc9-b3fc-6521adf51507",
    "token_type": "bearer",
    "refresh_token": "36869a92-278b-4975-b7ea-f7d26ecb84c5",
    "expires_in": 601,
    "scope": "read"
}

于是到这里 用户认证 发放令牌到这里就结束了

然后去资源服务器取值

例如http://10.0.75.1:8083/uua/fegin1/test?access_token=7dde09dc-12d7-4dc9-b3fc-6521adf51507

访问一个受限的资源  其实和上面差不多 一直到 


AbstractSecurityInterceptor.


beforeInvocation的最后一个方法


具体去认证


OAuth2AuthenticationProcessingFilter.class


Authentication authResult = this.authenticationManager.authenticate(authentication);


redis 认证链接方式 redis认证服务器_redis 认证链接方式_08


 


redis 认证链接方式 redis认证服务器_oauth2_09

 

redis 认证链接方式 redis认证服务器_oauth2_10