spring-security-oauth2

概述

通过 Spring Security 实现 OAuth 2.0 快速导航。以下案例基于 java11 搭建。

创建 OAuth 2.0 服务

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?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.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.idea360</groupId>
<artifactId>idc-oauth2</artifactId>
<version>0.0.1</version>
<name>idc-oauth2</name>
<description>Spring Boot + Security + OAuth2</description>

<properties>
<java.version>11</java.version>
</properties>

<dependencies>
<!--基础环境-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--OAuth 2.0-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>

<!--jdk9以上适配-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml

1
2
server:
port: 8080

认证服务配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-10-11
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;

@Primary
@Bean
public InMemoryTokenStore inMemoryTokenStore(){
return new InMemoryTokenStore();
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("clientId")
.secret(passwordEncoder.encode("secret"))
.authorizedGrantTypes("authorization_code", "password" , "refresh_token", "client_credentials") //"authorization_code",
.scopes("all")
.autoApprove(true)
.redirectUris("http://localhost:8080");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(inMemoryTokenStore())
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false) // 重复使用刷新token
.userDetailsService(userDetailsService); // 刷新token需要
}

@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//表示支持 client_id 和 client_secret 做登录认证
oauthServer.allowFormAuthenticationForClients();
}
}

安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-10-11
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Autowired
private PasswordEncoder passwordEncoder;

/**
* 默认密码处理器
* @return
*/
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}


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

/**
* 内置帐号密码
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin").password(passwordEncoder.encode("admin")).roles("ADMIN").build());
manager.createUser(User.withUsername("guest").password(passwordEncoder.encode("guest")).roles("GUEST").build());
return manager;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}

非对称加密(这里没有使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author 当我遇上你
* @公众号 当我遇上你
* @since 2020-10-11
*/
public class KeyConfig {

private static final String KEY_STORE_FILE = ".keystore-oauth2-demo";
private static final String KEY_STORE_PASSWORD = "admin1234";
private static final String KEY_ALIAS = "oauth2-demo-key";
private static KeyStoreKeyFactory KEY_STORE_KEY_FACTORY = new KeyStoreKeyFactory(new ClassPathResource(KEY_STORE_FILE), KEY_STORE_PASSWORD.toCharArray());
static final String VERIFIER_KEY_ID = Base64.getEncoder().encodeToString(KeyGenerators.secureRandom(32).generateKey());

static RSAPublicKey getVerifierKey() {
return (RSAPublicKey) getKeyPair().getPublic();
}

static RSAPrivateKey getSignerKey() {
return (RSAPrivateKey) getKeyPair().getPrivate();
}

private static KeyPair getKeyPair() {
return KEY_STORE_KEY_FACTORY.getKeyPair(KEY_ALIAS);
}
}

测试

密码模式(password)

  • 申请access_token
1
curl -X POST http://localhost:8080/oauth/token?grant_type=password&username=admin&password=admin&client_id=clientId&client_secret=secret
  • 返回access_token
1
2
3
4
5
6
7
{
"access_token": "dba595be-6bbf-4c6b-8fd3-eac650f40c4e",
"token_type": "bearer",
"refresh_token": "916b2145-0e07-44d2-9a29-b0e6c02b399f",
"expires_in": 43199,
"scope": "all"
}
  • 请求资源
1
curl http://localhost:8080/xxx/ping -H "Authorization: Bearer dba595be-6bbf-4c6b-8fd3-eac650f40c4e"

授权码模式(authorization-code)

  • 申请code
1
http://localhost:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=http://localhost:8080&state=123

然后在登录表单填写帐号密码 admin/admin, 返回code和state

1
http://localhost:8080/?code=LHHPXA&state=123
  • 申请access_token
1
curl -X POST http://localhost:8080/oauth/token?grant_type=authorization_code&code=LHHPXA&redirect_uri=http://localhost:8080&client_id=clientId&client_secret=secret
  • 返回access_token
1
2
3
4
5
6
7
{
"access_token": "db2c779f-20f6-44df-98e1-5fc46a2fe9c5",
"token_type": "bearer",
"refresh_token": "c7f87939-6801-4269-b745-e306c94130c2",
"expires_in": 43199,
"scope": "all"
}
  • 请求资源
1
curl http://localhost:8080/xxx/ping -H "Authorization: Bearer db2c779f-20f6-44df-98e1-5fc46a2fe9c5"

隐藏模式(implicit)

  • 请求access_token
1
http://localhost:8080/oauth/authorize?response_type=token&scope=all&client_id=clientId&client_secret=secret&redirect_uri=http://localhost:8080&state=123

然后在登录表单填写帐号密码 admin/admin, 返回access_token和state

1
http://localhost:8080/#access_token=6b8c3fa7-0513-44e3-b7a1-ea89f927010d&token_type=bearer&state=123&expires_in=43167
  • 请求资源
1
curl http://localhost:8080/xxx/ping -H "Authorization: Bearer 6b8c3fa7-0513-44e3-b7a1-ea89f927010d"

客户端凭证(client credentials)

  • 申请access_token
1
curl -X POST http://localhost:8080/oauth/token -H "Accept: application/json" -d "grant_type=client_credentials&scope=all&client_id=clientId&client_secret=secret"
  • 返回access_token
1
2
3
4
5
6
{
"access_token": "a38c2319-30f3-45ef-ab6b-294111e8b207",
"token_type": "bearer",
"expires_in": 43199,
"scope": "all"
}
  • 请求资源
1
curl http://localhost:8080/xxx/ping -H "Authorization: Bearer a38c2319-30f3-45ef-ab6b-294111e8b20"

刷新token

  • 请求refresh_token

以密码模式中的refresh_token演示

1
curl -X POST http://localhost:8080/oauth/token -H "Accept: application/json" -d "grant_type=refresh_token&refresh_token=916b2145-0e07-44d2-9a29-b0e6c02b399f&client_id=clientId&client_secret=secret"
  • 返回access_token
1
2
3
4
5
6
7
{
"access_token": "fcc85ce0-426d-421f-b2dc-005ebad3f9da",
"token_type": "bearer",
"refresh_token": "54880f4c-b5fe-4163-82c3-ae2e9fd90b85",
"expires_in": 43199,
"scope": "all"
}
  • 请求资源
1
curl http://localhost:8080/xxx/ping -H "Authorization: Bearer fcc85ce0-426d-421f-b2dc-005ebad3f9da"

最后

本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力。