security · 2023-11-30 0

Spring Security OAuth2 使用

一、搭建认证服务

1.maven 依赖

<groupId>com.example</groupId>
<artifactId>auth-center</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.配置

AuthorizationServer.java

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

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

    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);
        services.setSupportRefreshToken(true);
        services.setTokenStore(tokenStore());
        services.setAccessTokenValiditySeconds(7200);
        services.setRefreshTokenValiditySeconds(259200);
        return services;
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        return new InMemoryAuthorizationCodeServices();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("c1")
                .secret(new BCryptPasswordEncoder().encode("secret"))//$2a$10$0uhIO.ADUFv7OQ/kuwsC1.o3JYvnevt5y3qX/ji0AUXs4KYGio3q6
                .resourceIds("r1")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                .scopes("all")
                .autoApprove(false)
                .redirectUris("https://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenServices())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

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

WebSecurityConfig.java

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 认证管理器
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder().encode("123"))
                .authorities("p1", "p2")
                .and()
                .withUser("lisi")
                .password(passwordEncoder().encode("456"))
                .authorities("p1");
    }
}

3.controller

IndexController.java

@RestController
public class IndexController {

    @GetMapping("/index")
    public String index() {
        return "auth-center index";
    }

    @GetMapping("/hello")
    public String hello() {
        return "auth-center hello";
    }
}

4.配置文件

application.yml

server:
  port: 30000
spring:
  application:
    name: auth-center

5.启动类

@SpringBootApplication
public class AuthCenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthCenterApplication.class, args);
    }
}

二、搭建资源服务

1.maven 依赖

<groupId>com.example</groupId>
<artifactId>resource-server</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.配置

ResouceServerConfig.java

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    @Bean
    public ResourceServerTokenServices resourceServerTokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        remoteTokenServices.setCheckTokenEndpointUrl("http://127.0.0.1:30000/oauth/check_token");
        remoteTokenServices.setClientId("c1");
        remoteTokenServices.setClientSecret("secret");
        return remoteTokenServices;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("r1")
                .tokenServices(resourceServerTokenServices()) // 令牌服务
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r1").authenticated() // r1 需要认证
                .anyRequest().permitAll(); // 其他请求则允许所有用户访问
    }
}

3.controller

@RestController
public class IndexController {

    @GetMapping("/index")
    public String index() {
        return "this is index";
    }

    @GetMapping("/r1")
    public String r1(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "this is r1";
    }
}

4.配置文件

server:
  port: 30001
spring:
  application:
    name: resource-server

5.启动类

@SpringBootApplication
public class ResourceServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }
}

三、认证

1.授权码认证模式

最安全的一种模式。一般用于 client 是 Web 服务器端应用或第三方的原生 App 调用资源服务的时候。因为在这种模式中 access_token 不会经过浏览器或移动端的 App,而是直接从服务端去交换,这样就最大限度的减小了令牌泄漏的风险。 该模式下获取 token 需要分两步走,第一步获取授权码,第二步获取 token。

1) 获取 code

接口地址:http://127.0.0.1:30000/oauth/authorize

请求方式:GET

请求参数:

字段名 描述
client_id 改值必须和配置在 clients 中的值保持一致
response_type 固定传值 code 表示使用授权码模式进行认证
scope 改值必须配置的 clients 中的值一致
redirect_uri 获取 code 之后重定向的地址,必须和配置的 clients 一致

请求示例:

http://127.0.0.1:30000/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=https://www.baidu.com

账号密码分别输入:zhangsan/123,进入授权页面之后点击授权按钮,页面跳转之后获取到 code

重定向到:

https://www.baidu.com/?code=rBWbw3

2) 获取 token

接口地址:http://127.0.0.1:30000/oauth/token

请求方式:POST

请求参数:

字段名 描述
code 上一步获取到的 code
grant_type 在授权码模式,固定使用 authorization_code
client_id 改值必须和配置在 clients 中的值保持一致
client_secret 这里的值必须和代码中配置的 clients 中配置的保持一致
redirect_uri 获取 token 之后重定向的地址,该地址可以随意写

请求示例:

zxm@zxm-pc:~$ curl -X POST "http://localhost:30000/oauth/token?code=rBWbw3&grant_type=authorization_code&client_id=c1&client_secret=secret&redirect_uri=https://www.baidu.com"
{"access_token":"992cdbf8-fe9c-47e3-ac0e-8ceb66a479c5","token_type":"bearer","refresh_token":"ee6e1589-1b5d-408a-a432-efa4f60d3da9","expires_in":7199,"scope":"all"}

2.简化模式

该模式去掉了授权码,用户登陆之后直接获取token并显示在浏览器地址栏中,参数和请求授权码的接口基本上一模一样,唯一的区别就是 response_type 字段,授权码模式下使用的是 code 字段,在简化模式下使用的是 token 字段。一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

接口地址:http://127.0.0.1:30000/oauth/authorize

请求方式:GET

请求参数:

字段名 描述
client_id 改值必须和配置在 clients 中的值保持一致
response_type 固定传值 token 表示使用简化模式进行认证
scope 该值必须和配置的 clients 中的值一致
redirect_uri 获取 code 之后重定向的地址,必须和配置的 clients 一致

请求示例:

http://127.0.0.1:30000/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=https://www.baidu.com

重定向到:

https://www.baidu.com/#access_token=992cdbf8-fe9c-47e3-ac0e-8ceb66a479c5&token_type=bearer&expires_in=6521

3.密码模式

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了 client,因此这就说明这种模式只能用于 client 是我 们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

接口地址:http://127.0.0.1:30000/oauth/token

请求方式:POST

请求参数:

字段名 描述
client_id 改值必须和配置在 clients 中的值保持一致
client_secret 改值必须和配置在 clients 中的值保持一致
grant_type 在密码模式下,该值固定为 password
username 用户名
password 密码

请求示例:

zxm@zxm-pc:~$ curl -X POST "http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123"
{"access_token":"992cdbf8-fe9c-47e3-ac0e-8ceb66a479c5","token_type":"bearer","refresh_token":"ee6e1589-1b5d-408a-a432-efa4f60d3da9","expires_in":6284,"scope":"all"}

4.客户端模式

这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

接口地址:http://127.0.0.1:30000/oauth/token

请求方式:POST

请求参数:

| 字段名 | 描述 |
| client_id | 改值必须和配置在 clients 中的值保持一致 |
| client_secret | 改值必须和配置在 clients 中的值保持一致 |
| grant_type | 在密码模式下,该值固定为 client_credentials |

请求示例:

zxm@zxm-pc:~$ curl -X POST "http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials"
{"access_token":"006912ba-ac7a-4ce0-84ec-e5c5a826ed17","token_type":"bearer","expires_in":7199,"scope":"all"}

5.refresh_token 换取新 token

接口地址:http://127.0.0.1:30000/oauth/token

请求方式:POST

请求参数:

字段名 描述
client_id 该值必须和配置在 clients 中的值保持一致
client_secret 该值必须和配置在 clients 中的值保持一致
grant_type 如果想根据 refresh_token 换新的 token,这里固定传值 refresh_token
refresh_token 未失效的r efresh_token

请求示例:

zxm@zxm-pc:~$ curl -X POST "http://127.0.0.1:30000/oauth/token?grant_type=refresh_token&refresh_token=ee6e1589-1b5d-408a-a432-efa4f60d3da9&client_id=c1&client_secret=secret"
{"access_token":"f0e0e9ef-ea4e-480d-9b35-b8779c56f6c3","token_type":"bearer","refresh_token":"ee6e1589-1b5d-408a-a432-efa4f60d3da9","expires_in":7199,"scope":"all"}

四、使用

客户端请求

curl -X GET -H "Authorization: Bearer c5c7ed0c-cad7-4f37-8119-bd32cdea194b" "http://127.0.0.1:30001/r1"