主要流程

  1. 客户端前端页面点击授权,访问授权服务器的授权接口
    1
    page
  2. 跳转到用户登录页,用户登录并授权
    login
    1
  3. 返回code并重定向到指定的客户端链接地址(客户端获取token的接口)
    http://localhost:8080/oauth/authorize?client_id=client&response_type=code&scope=all&redirect_uri=http://localhost:8082/authCallback中的redirect_uri=http://localhost:8082/authCallback,并携带code
  4. 用code访问授权服务器获取token地址,返回token
    这里在正式应用里面应该是将token保存起来复用,并设定定时刷新
    获取token
  5. 访问资源服务器,这里访问的时候调用资源服务器接口,返回后继续执行后面的
    request
  6. 资源服务器访问授权服务器校验token并获取用户信息
    鉴定
    校验
    通过以上可以认证客户端和用户了,也就可以执行资源的获取,这里scope可以是一个列表,具体看请求的scope和授权的scope
  7. 资源服务器确认、处理业务并返回资源
    处理返回

入门案例

本文所有使用的依赖版本为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--************spring-boot相关************-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>

授权服务器搭建及配置

授权服务器搭建在springsecurity的基础上

引入依赖

oauth2依赖,security依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

用户配置SecurityConfig

用户配置主要遵循spring-security基本的一套用户认证;所有配置 与spring-security相同

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
@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login.html", "/css/**", "/js/**", "/images/**");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里默认使用基于内存的UserDetailService,仅仅做展示,数据库的则按照security的方式修改即可
auth.inMemoryAuthentication()
.withUser("sang")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("admin")
.authorities("s1","s2")
.and()
.withUser("javaboy")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("user");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 登录地址,默认/login 授权访问地址默认/oauth/authorize , 这两个需要暴露出来,默认已经暴露出来了,若要修改,则需要两处同时修改放开
.requestMatchers().antMatchers("/login").antMatchers("/oauth/authorize")
.and()
.authorizeRequests().anyRequest().authenticated().and()
//默认表单登录,因为是重定向过来的,这里登录时会直接调到login.html然后再执行/login,如果授权服务器前后端分离则应当注意重定向的地址及跳转
//.formLogin().loginProcessingUrl("/login").loginPage("/login.html").and()
.formLogin().and()
.csrf().disable();
}
}

客户端授权配置AuthenticationServer

客户端授权配置主要配置令牌的安全约束令牌生成的保存客户端信息

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

/**
* 授权服务器配置
* 对授权服务器做进一步的详细配置,类上加@EnableAuthorizationServer 注解,表示开启授权服务器的自动化配置。
* 主要重写三个 configure 方法。
* AuthorizationServerConfigurerAdapter
* @author kewen
* @since 2022/9/7
*/
@EnableAuthorizationServer
@EnableResourceServer
@Configuration
public class AuthenticationServer implements AuthorizationServerConfigurer {

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

/**
* 配置令牌的安全约束
* 谁能访问
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//令牌允许访问,默认denyAll()禁止所有,这里改为允许所有permitAll()
security.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
/**
* 配置客户端的详细信息
* 配置后可以得到 ClientDetailsService
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置客户端信息,
.withClient("client")
//秘钥
.secret(passwordEncoder().encode("123"))

.resourceIds("res1", "res2")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/authCallback")
.and()
.withClient("res-client")
.secret(passwordEncoder().encode("456"))
.and()
.withClient("client1")
.secret(passwordEncoder().encode("secret1"))
.autoApprove(true)
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8001/login")
.and()
.withClient("client2")
.secret(passwordEncoder().encode("secret2"))
.autoApprove(true)
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8002/login")
;
}
/**
* 配置令牌的访问端点和服务
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authorizationCodeServices(authorizationCodeServices()) // 配置授权码的存储
.tokenServices(authorizationServerTokenServices()); // 配置令牌的存储
}
/**
* 授权码生成存储使用,简单使用内存,真实场景替换即可
*/
@Bean
AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}


/**
* 默认会配置一个
* 由 configure(ClientDetailsServiceConfigurer clients)方法配置 ClientDetailsServiceConfigurer,再调用.and().build() 得到此类,
*/
@Autowired
ClientDetailsService clientDetailsService;
/**
* 授权服务,令牌存储及使用
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
tokenServices.setAccessTokenValiditySeconds(2 * 60 * 60);
tokenServices.setRefreshTokenValiditySeconds(3 * 24 * 60 * 60);
return tokenServices;
}
/**
* token 存储仓库,存放生成的token
*/
@Bean
public TokenStore tokenStore() {
InMemoryTokenStore tokenStore = new InMemoryTokenStore();
System.out.println(tokenStore.getAccessTokenCount());
return tokenStore;
}
}

资源服务器

依赖

资源服务器和授权服务器使用相同的依赖,只有使用的启动注解和配置不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

配置

需要开启@EnableResourceServer

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
/**
* 资源服务器配置
*/
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {

/**
* 这里的token保存服务用的是远程的,原因是授权服务器和资源服务器是分开的,资源服务器需要去授权服务器校验token,
* 如果资源服务器和授权服务器是一个服务,则使用DefaultTokenServices公用相同的tokenStore即可
*/
@Bean
RemoteTokenServices remoteTokenServices(){
RemoteTokenServices tokenServices = new RemoteTokenServices();
//
tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token"); //access_token校验地址
tokenServices.setClientId("res-client"); //资源服务器id
tokenServices.setClientSecret("456"); //资源服务器密码
return tokenServices;
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//配置本客户端的资源ID,这是会在授权服务器注册的
resources.resourceId("res1")
.tokenServices(remoteTokenServices()) //设置资源服务获取token的地址
;
}

@Override
public void configure(HttpSecurity http) throws Exception {
//权限控制
http.authorizeRequests().antMatchers("/admin/**").hasRole("admin") //设置资源权限
.anyRequest().authenticated();
}
}

资源服务器不再需要用户认证,因为客户端带来了token,资源服务器的token的认证就相当于替换了资源服务器本身的认证过程

客户端

客户端不需要有spring-security相关的东西,只需要普通的客户端,用来模拟访问授权服务器和资源服务器即可

依赖

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

客户端后台

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
@Configuration
public class ClientConfig {

@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}

}
@Controller
public class HelloController {
@Autowired
private RestTemplate restTemplate;

@GetMapping("/index.html")
public String hello() {
return "index";
}

/**授权回调方法*/
@GetMapping("/authCallback")
public String callback(String code, Model model) {
//组装获取token参数
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "client");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/authCallback");
map.add("grant_type", "authorization_code");
//发送请求获取token
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
String access_token = resp.get("access_token");
System.out.println(access_token);
HttpHeaders headers = new HttpHeaders();
//组装Authorization
headers.add("Authorization", "Bearer " + access_token);
HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
//携带token发起请求至资源服务器,资源服务器会根据收到的Authorization 到认证服务器校验
ResponseEntity<String> entity = restTemplate.exchange("http://localhost:8081/hello", HttpMethod.GET, httpEntity, String.class);
String adminBody = entity.getBody();
System.out.println("hello 资源返回"+adminBody);
try {
ResponseEntity<String> entityAdmin = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
adminBody = entityAdmin.getBody();
System.out.println("admin hello 资源返回"+adminBody);
} catch (Exception e) {
System.out.println("访问接口admin/hello 异常 "+e.getMessage());
}
model.addAttribute("msg",adminBody);
return "index";
}
}

客户端页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Kewen</title>
</head>
<body>
你好,kewen!

<a href="http://localhost:8080/oauth/authorize?client_id=client&response_type=code&scope=all&redirect_uri=http://localhost:8082/authCallback">第三方登录</a>
<div></div>

<h1 th:text="${msg}"></h1>
</body>
</html>