springcloud+oauth2温故而知新

OAuth2的相关定义、原理、4种授权模式、认证流程请参见官网或大佬的相关博文。

oauth2目前出现好多版本依赖,如security中有oauth2、springcloud security中也有oauth2,多种依赖是由其历史原因产生的,在此根据最新版本去学习实践就好,毕竟发布的最新版就oauth2的发展趋势,最后终归一统。

此处引用springcloud中的oauth2依赖进行温习,实践其密码模式,搭建oauth2统一认证中心,应用于分布式架构或微服务架构中,适应多系统需要统一的认证授权需求。
密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,我们自己做前后端分离登录就可以采用这种模式。

登录认证分为有状态登录和无状态登录:
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。
无状态服务,微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息;客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份;

此处选择有状态登录,访问授权服务(统一认证中心)获取token,携带token访问资源服务,资源服务远程调用授权服务(统一认证中心)进行鉴权。授权服务(统一认证中心)进行验证有两方面的检验,一方面是校验客户端,另一方面则是校验用户;客户端校验scope,用户校验role,授权码模式就应用到客户端校验scope的选项了。

无状态登录的一种典型代表:JWT。
JWT,全称是 Json Web Token , 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权;但JWT方式存在续签、注销问题。

有状态登录中授权服务派发了 access_token 之后,客户端拿着 access_token 去请求资源服务,资源服务要去校验 access_token 的真伪,所以我们在资源服务器上配置了 RemoteTokenServices,让资源服务做远程校验,在高并发场景下请求授权服务又不太方便。

单点登录是分布式系统中非常常见的需求,分布式系统由多个不同的子系统组成,而我们在使用系统的时候,只需要登录一次即可,这样其他系统都认为用户已经登录了,不用再去登录。
oauth2+jwt的无状态登录天然满足单点登录的场景;其实oauth2+redis实现共享session的前后端分离模式也满足单点登录的场景;

统一认证中心即将认证授权独立出来,做出单独的一个服务,作为分布式系统的统一认证中心。
此处搭建的统一认证中心即为单独的一个服务,包含授权服务和资源服务(用户),其他子系统也作为资源服务,即一个统一认证中心,多个资源服务

搭建统一认证中心实例
1依赖



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.4.RELEASE
         
    
    com
    oauth2
    0.0.1-SNAPSHOT
    oauth2
    Demo project for Spring Boot
    
        1.8
        Hoxton.RELEASE
    
    
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.apache.commons
            commons-pool2
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
            
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.2.0
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

        
            mysql
            mysql-connector-java
            runtime
        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    
    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        

        
            
                src/main/java
                
                    **/*.xml
                    **/*.properties
                
                
                false
            
            
                src/main/resources
                
                    **/*.xml
                    **/*.properties
                    **/*.yml
                
                false
            
        
    



2application.properties

spring.application.name=oauth2
server.port=

#使用IP注册
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.client.service-url.defaultZone=http://localhost:9001/eureka/

#mysql配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=
spring.datasource.password=

3授权服务配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Resource
    private CustomClientDetailsService customClientDetailsService;

    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //clients.withClientDetails(customClientDetailsService);
        // 配置两个客户端,一个用于client认证,一个用于password认证
        clients.inMemory()
                .withClient("client1")
                .secret(passwordEncoder.encode("1"))
                .authorizedGrantTypes("client_credentials", "refresh_token")
                .resourceIds("resource1")
                .scopes("all")
                .authorities("oauth2")
                .and()
                .withClient("client2")
                .secret(passwordEncoder.encode("1"))
                .authorizedGrantTypes("password", "refresh_token")
                .resourceIds("resource2")
                .scopes("all")
                .authorities("oauth2");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
        //配置TokenService参数
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        //token持久化容器
        tokenServices.setTokenStore(endpoints.getTokenStore());
        //是否支持refresh_token,默认false
        tokenServices.setSupportRefreshToken(true);
        //客户端信息
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        //自定义token生成
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        //access_token 的有效时长 (秒), 默认 12 小时;1小时
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
        //refresh_token 的有效时长 (秒), 默认 30 天;1小时
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
        //是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
        tokenServices.setReuseRefreshToken(false);
        //token相关服务
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
}

4security配置

@Order(1)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Resource
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("admin").password(passwordEncoder().encode("1")).authorities("USER").build());
        userDetailsManager.createUser(User.withUsername("user").password(passwordEncoder().encode("1")).authorities("USER").build());
        return userDetailsManager;
    }

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

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义用户userDetailService、加密
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
        auth.authenticationProvider(customAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().disable();
        http
                .requestMatchers().antMatchers("/oauth/**")
                //拦截上面匹配后的url,需要认证后访问
                .and()
                .authorizeRequests().antMatchers("/oauth/**").authenticated();
        http
                .sessionManagement()
                .invalidSessionUrl("/login")
                .maximumSessions(1)
                .expiredUrl("/login");
    }
}

5资源服务配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("resource2").stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/oauth2/user", "/user/**").authenticated();
    }

}

6资源服务进行验证的接口

/**
     * 提供资源服务进行认证校验(必须)
     */
    @RequestMapping("/user")
    public Principal user(Principal user) {
        return user;
    }

测试实例
搭建资源服务1,资源服务配置

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("resource2").stateless(true);
        RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        tokenService.setClientId("client2");
        tokenService.setClientSecret("1");
        resources.tokenServices(tokenService);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.cors().disable();
        http
                .authorizeRequests()
                .antMatchers("/resource1/**").authenticated();
    }
}

或搭建资源服务2,资源服务配置

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("resource2").stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.cors().disable();
        http
                .authorizeRequests()
                .antMatchers("/resource2/**").authenticated();
    }
}

application.properties

security.oauth2.client.client-id=client2
security.oauth2.client.client-secret=1

security.oauth2.client.access-token-uri=http://localhost:9002/oauth/token

security.oauth2.client.user-authorization-uri=http://localhost:9002/oauth/authorize

#prefer-token-info默认值为true,既优先使用token-info-uri校验token认证信息
security.oauth2.resource.token-info-uri=http://localhost:9002/oauth/check_token
#进行令牌校验,访问认证服务器Controller获取Principal,解析令牌
security.oauth2.resource.user-info-uri=http://localhost:9002/oauth2/user
#或进行令牌校验,访问认证服务器获取公钥,解析令牌
#security.oauth2.resource.jwt.key-uri=http://localhost:9002/oauth/token_key
#prefer-token-info设置为false,或不配置token-info-uri则会使用user-info-uri
security.oauth2.resource.prefer-token-info=false

测试结果
springcloud+oauth2温故而知新_第1张图片
springcloud+oauth2温故而知新_第2张图片
springcloud+oauth2温故而知新_第3张图片
后续还需深入实践。

你可能感兴趣的:(spring,cloud,java)