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依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com</groupId>
    <artifactId>oauth2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <!--<version>2.2.4.RELEASE</version>-->
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
                <!-- 是否替换资源中的属性 -->
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                    <include>**/*.yml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

</project>

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

测试结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
后续还需深入实践。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值