发布于 ,更新于 

springboot+serurity+oauth2实战

1. SpringBoot 2.3版本示例

1.1 基础介绍

OAuth2是什么?4中基本授权模式理解:

Sercurity是什么?:

Sercurity + OAuth2:

springcloud + 前后端分离的模式中 主要使用密码模式授权码模式,此文章以密码模式 介绍

作用与分工:

OAuth2主要用于校验客户端合法性、产生token、校验token
Sercurity主要用于用户名密码校验、接口权限控制

OAuth2与Sercurity整合之后,校验顺序:
获取token请求/oauth/token:校验客户端合法性——校验用户名密码——产生token
受保护的接口请求/**:校验token——校验接口权限

授权接口流程:

源码解析:

1.2 主要架构tree:

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
├── auth # 授权服务
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── yuzhe
│ │ │ ├── AuthApplication.java
│ │ │ ├── config
│ │ │ │ ├── AuthorizationServerConfig.java # 授权服务器器配置
│ │ │ │ └── SecurityConfig.java # SpringSecurity 相关配置
│ │ │ └── controller
│ │ │ └── AuthController.java # 退出登录接口
│ │ └── resources
│ │ └── application.yaml
│ └── test
│ └── java
├── pom.xml
├── service-a # 资源服务
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── yuzhe
│ │ │ ├── AppApplication.java
│ │ │ ├── Filter
│ │ │ │ ├── SkipAuthenticationFilter.java # 自定义过滤器
│ │ │ │ └── SkipTokenForSpecificUserFilter.java
│ │ │ ├── config
│ │ │ │ └── ResourceServerConfig.java # 资源服务器配置
│ │ │ └── controller
│ │ │ └── HelloController.java # 资源接口
│ │ └── resources
│ │ └── application.yaml
│ └── test
│ └── java
└── yuzhe-security-oauth2-password-springboot2.3.iml

1.3 授权服务 auth

1.3.0 Maven依赖

spring cloud自从2020.0.0(含)以上版本就移除了spring-cloud-security-dependencies依赖,所以从2020.0.0版本开始,无法引入spring-cloud-starter-oauth2,oauth2授权服务分离为一个独立的project:spring authorization server

本示例使用 springboot 2.3 spring cloud Hoxton.SR11, 所以可以使用spring-cloud-starter-oauth2

1
2
3
4
5
6
7
8
9
<!-- oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

1.3.1 授权服务器器配置

授权服务器器配置
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
119
120
121
122
123
124
125
126
127
128
129
130
package com.yuzhe.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;

/**
* @author zhaoyuzhe
* @date 2025/1/10
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

//-----jwtTokenStore 的配置方式 start -------
// @Bean
// public JwtAccessTokenConverter accessTokenConverter() {
// JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// // 签名密钥
// converter.setSigningKey("my-secret-key");
// return converter;
// }
//
// /** TokenStore组件
// * InMemoryTokenStore:将令牌存储在内存中。
// * JdbcTokenStore:将令牌存储在数据库中。
// * JwtTokenStore:使用 JWT 格式存储令牌
// */
// @Bean
// public TokenStore tokenStore() {
// return new JwtTokenStore(accessTokenConverter());
// }
//-----jwtTokenStore 的配置方式 end -------

//----- redisTokenStore 的配置方式 --------
@Resource
private RedisConnectionFactory redisConnectionFactory;

@Bean
public TokenStore tokenStore(){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
//设置KEY的层级前缀,方便查询
redisTokenStore.setPrefix("ZYZ-TOKEN:");
return redisTokenStore;
}


@Autowired
private PasswordEncoder passwordEncoder;

/** 客户端相关配置 ClientDetailsService组件
* InMemoryClientDetailsService:将客户端信息存储在内存中。
* JdbcClientDetailsService:将客户端信息存储在数据库中
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 内存模式。可以配置jdbc模式从数据库获取client-id,client-secret等配置
.inMemory()
// 客户端id
.withClient("client")
// 客户端密码密码存储时加密,注意如果这里配置了加密,那么,对应的WebSecurityConfigurerAdapter中的用户密码也需要配置加密
.secret(passwordEncoder.encode("secret"))
// 密码未加密方式 {noop}表示无加密操作
// .secret("{noop}secret")
// 授权类型
.authorizedGrantTypes("password", "refresh_token")
// 范围权限 对应的可以在资源服务器的接口上配置:@PreAuthorize("#oauth2.hasScope('read')"),正常不需要这个
.scopes("read", "write")
// token有效期
.accessTokenValiditySeconds(360)
.refreshTokenValiditySeconds(720)
// 允许访问的资源服务的resourceId配置
.resourceIds("service-a", "service-b")
;
}

/*** 端点配置 TokenEndpoint组件*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 密码模式需要此配置
.authenticationManager(authenticationManager)
// redisTokenStore 的配置方式
.tokenStore(tokenStore());

// jwtTokenStore 的配置方式
//.tokenStore(tokenStore())
//.accessTokenConverter(accessTokenConverter());

// token 添加额外信息
// .tokenEnhancer((accessToken, authentication) -> {
// SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();
// LinkedHashMap<String, Object> map = new LinkedHashMap<>();
// // 追加额外信息
// map.put("username",signInIdentity.getUsername());
// map.put("authorities", signInIdentity.getAuthorities());
// DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
// token.setAdditionalInformation(map);
// return token;
// })

}


// TokenGranter 授权模式
// AuthorizationCodeTokenGranter:处理授权码模式。
// ResourceOwnerPasswordTokenGranter:处理密码模式。
// ClientCredentialsTokenGranter:处理客户端凭证模式。
// RefreshTokenGranter:处理刷新令牌模式

}

主要组件:

客户端 相关配置 ClientDetailsService组件

  • InMemoryClientDetailsService:将客户端信息存储在内存中。
  • JdbcClientDetailsService:将客户端信息存储在数据库中

端点 配置 TokenEndpoint组件

​ 主要配置授权模式,tokenStore,tokenEnhancer等

TokenStore 组件

  • InMemoryTokenStore:将令牌存储在内存中。
  • JdbcTokenStore:将令牌存储在数据库中。
  • JwtTokenStore:使用 JWT 格式存储令牌。
  • RedisTokenStore: 使用Redis存储令牌。

常用JwtTokenStore和RedisTokenStore,

​ JwtTokenStore 授权时 授权服务器将用户信息、权限、过期时间等存到token中,故而token会很长,资源服务器直接解析token中信息做校验

​ RedisTokenStore 授权时 授权服务器将用户信息、权限、过期时间存到redis,token只有很短的一个字符串,资源服务器校验时用此字符串从redis中获取信息做校验

​ 微服务中最好使用RedisTokenStore,因为RedisTokenStore是有状态的,代码可以控制token的失效和删除(用来做 登出 操作),但是JwtTokenStore是无状态的,无法删除和失效,只有过期时间结束了,此token才会失效

授权接口:

curl -X POST -u client:secret -d “grant_type=password&username=user&password=password” http://localhost:17700/oauth/token

1.3.2 WebSecurity配置

WebSecurity配置
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
package com.yuzhe.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author zhaoyuzhe
* @date 2025/1/10
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

@Autowired
private PasswordEncoder passwordEncoder;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// .withUser("admin")
.withUser("user")
// .password("{noop}password")
// 这里配置加密,对应的client密码也需要配置加密,即授权服务器中配置 clients.secret(passwordEncoder.encode("secret"))
.password(passwordEncoder.encode("password"))
.roles("USER");
// 从数据库中读取用户
// auth.userDetailsService(userDetailsService)
// .passwordEncoder(passwordEncoder);
}

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

@Override
protected void configure(HttpSecurity http) throws Exception {
http
// //前后端分离架构不需要csrf保护,这里关闭
.csrf().disable()
// 禁用生成默认的登陆页面
.formLogin().disable()
// 关闭httpBasic
.httpBasic().disable()
// 前后端分离是无状态的,不用session了,直接禁用
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll()
.antMatchers("/oauth/logout").permitAll()
.anyRequest().authenticated()
;
}
}

AuthenticationManagerBuilder 实际项目中是使用自定义userDetailsService

a
1
2
//        auth.userDetailsService(userDetailsService)
// .passwordEncoder(passwordEncoder);

从数据库中读取 帐号、密码、角色、 权限,这里userDetailsService相关的代码不贴了

1.3.1 自定义授权模式

1.4 资源服务 service-a

1.4.0 Maven依赖

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

1.4.1 资源服务器配置

ResourceServe配置
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
package com.yuzhe.config;

import com.yuzhe.Filter.SkipAuthenticationFilter;
import com.yuzhe.Filter.SkipTokenForSpecificUserFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
* @author zhaoyuzhe
* @date 2025/1/10
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限验证相关注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
//资源服务的id,需要与授权服务中的配置对应
resources.resourceId("service-a");
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 无须验证
.antMatchers("/api/public").permitAll()
// 其他接口拒绝访问
//.anyRequest().denyAll()
// 其他接口需要验证
.anyRequest().authenticated()
//.and()
// 添加自定义过滤器
//.addFilterBefore(new SkipAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
//.and()
//.addFilterBefore(new SkipTokenForSpecificUserFilter(), BearerTokenAuthenticationFilter.class);
;
}

//----- redisTokenStore 的配置方式 --------
@Resource
private RedisConnectionFactory redisConnectionFactory;

@Bean
public TokenStore tokenStore(){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
//设置KEY的层级前缀,方便查询
redisTokenStore.setPrefix("ZYZ-TOKEN:");
return redisTokenStore;
}

// JwtTokenStore 配置方式
// @Bean
// public JwtAccessTokenConverter accessTokenConverter() {
// JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// converter.setSigningKey("my-secret-key"); // 与授权服务器一致
// return converter;
// }
//
// @Bean
// public TokenStore tokenStore() {
// return new JwtTokenStore(accessTokenConverter());
// }

// 如果不设置jwt。可以使用RemoteTokenServices,资源服务器会向授权服务器发起请求,验证token正确性,但是这样会产生请求次数多的压力
// 所以一般使用jwt封装用户等信息,资源服务器可以直接从jwt中获取用户信息去验证
// public ResourceServerTokenServices resourceServerTokenServices() {
// RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8080/auth/oauth/check_token");
// remoteTokenServices.setClientId("hello");
// remoteTokenServices.setClientSecret("123456");
// return remoteTokenServices;
// }
}

1.4.2 自定义过滤器

过滤器链:

todo。。。

1.5 对接外部SSO

todo。。

1.6 使用oauth2开发SSO

todo。。

2. SpringBoot 2.6版本示例

3. SpringBoot 3 版本示例