0%

spring security oauth2搭建

一、简介

OAuth2是一套开放标准,Spring Cloud Security Oauth2是对它的一种实现。

1.1 角色定义

1.1.1 客户端

​ 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,如:移动端、pc端等。

1.1.2 资源拥有者

​ 通常为用户、也可为应用程序,即资源的拥有者。

1.1.3 资源服务器

​ 存储资源的服务器,一般为提供业务接口服务的程序。

1.1.4 授权服务器

​ 用于对资源拥有者的身份进行认证、对访问资源进行授权,认证成功发放令牌(access_token)给客户端,作为客户端访问资源服务器的凭证;授权服务器就是对客户端和资源拥有着进行认证授权,使其可以访问资源服务器获取资源。

1.2 四种模式

1.2.1 授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式,code保证了token的安全性,即使code被拦截,由于没有app_secret,也是无法通过code获得token的。

1.2.2 隐式授权模式/简化模式

和授权码模式类似,少了获取code的步骤,是直接获取令牌token的,适用于公开的浏览器单页应用,令牌直接从授权服务器返回,不支持刷新令牌,且没有code安全保证,令牌容易因为被拦截窃听而泄露。

1.2.3 密码模式

使用用户名/密码作为授权方式从授权服务器上获取令牌,一般不支持刷新令牌。

1.2.4 客户端凭证模式

一般用于资源服务器是应用的一个后端模块,客户端向认证服务器验证身份来获取令牌。

二、授权服务搭建

2.1 maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 此依赖包含security,所以不需要额外导入security -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- 健康检查 oauth2会使用到一些端点信息需要加上此依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 使用redis存储token才需要引入,其他方式存储可以不配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 配置类

2.2.1 鉴权服务配置

配置类添加@EnableAuthorizationServer,继承org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter类

需重写以下三个方法

2.2.1.1 configure(ClientDetailsServiceConfigurer clients) :

客户端详情信息服务配置,能够使用内存、jdbc方式来实现clientDetailsService(客户端详情服务),负责查找clientDetails。clientDetails能够在应用程序运行时进行更新,通过访问存储服务(如jdbcClientDetailsService)或自己实现ClientRegistrationService接口进行管理

关于clientDetails的几个属性如下:

  • clientId:用来表示客户的id
  • secret:用来限制客户端的访问访问,如果为空(默认为空)的话,客户端拥有全部的访问范围
  • authorizedGrantType:客户端可以使用的授权类型,默认为空(client_credentials,password,authorization_code,implicit,refresh_token)
  • authorities:客户端可以使用的权限
2.2.1.2 configure(AuthorizationServerEndpointsConfigurer endpoints):

令牌访问端点,用来配置令牌的访问端点和令牌服务

2.2.1.3 configure(AuthorizationServerSecurityConfigurer 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
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
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;

/**
* 1.客户端详情信息服务
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

//暂时使用内存方式
/* clients.inMemory()
//client_id
.withClient("c1")
//客户端秘钥
.secret(passwordEncoder.encode("secret"))
//可访问资源列表
.resourceIds("r1")
//允许该client授权的类型
.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
//允许授权范围
.scopes("all")
//如果使用授权码模式,false跳转到授权页面让用户进行授权,true直接发放令牌
.autoApprove(false)
//验证回调地址
.redirectUris("http://baidu.com");*/
//使用数据库方式
clients.withClientDetails(clientDetailsService);
}

/**
* 2.1令牌管理服务
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//客户端信息服务
defaultTokenServices.setClientDetailsService(clientDetailsService);
//是否产生刷新令牌
defaultTokenServices.setSupportRefreshToken(true);
//令牌存储策略
defaultTokenServices.setTokenStore(tokenStore);
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
//令牌默认有效时间2小时
defaultTokenServices.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200);
return defaultTokenServices;
}
/**
* 2.2令牌访问端点
*
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
//密码模式需要(此处注入的管理类为security所配置)
.authenticationManager(authenticationManager)
//授权码模式需要
.authorizationCodeServices(authorizationCodeServices)
//令牌管理服务
.tokenServices(tokenService())
//注入自己重写的userDetailsService进行自定义用户信息查询
.userDetailsService(userDetailsService)
//允许post提交访问服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}

/**
* 3.令牌访问端点安全策略
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
//提供公有秘钥端点,用于jwt令牌 公开/oauth/token_key
.tokenKeyAccess("permitAll()")
// /oauth/check_token端点公开
.checkTokenAccess("permitAll()")
//允许表单认证
.allowFormAuthenticationForClients();
}

/**
* 设置授权码模式的授权码存储,暂时使用内存方式
*
* @return
*/
/* @Bean
public AuthorizationCodeServices authorizationCodeServices(){
//设置授权码模式,暂时使用内存方式
return new InMemoryAuthorizationCodeServices();
}*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
//设置授权码模式,暂时使用数据库方式
return new JdbcAuthorizationCodeServices(dataSource);
}

@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
}

2.2.2 令牌存储策略配置

这里编写了三种令牌生成后的存储方式,根据自己的需求进行修改,后面的配置以jwt策略为准,在AuthorizationServerConfiguration类中的tokenService()进行令牌注入配置

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
/**
* 令牌存储策略
*/
@Configuration
public class TokenConfig {
/**
* redis存储时使用
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;

/**
* 内存存储策略
*/
/*@Bean
public TokenStore tokenStore(){
//内存方式生成普通令牌
return new InMemoryTokenStore();
}*/

private String SIGNING_KEY = "uaa123";

/**
* jwt存储策略
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//对称秘钥,资源服务器使用此秘钥来校验
converter.setSigningKey(SIGNING_KEY);
return converter;
}

/**
* redis存储策略
*/
/*
@Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
//配置redis存储的key前缀
tokenStore.setPrefix("myOauth2:");
return tokenStore;
}
*/
}

2.2.3 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
37
38
39
40
41
42
43
44
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and().csrf().disable()
.authorizeRequests().anyRequest().authenticated();
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
/* BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
auth.inMemoryAuthentication().withUser("111").password(bCryptPasswordEncoder.encode("222")).authorities("user")
.and().withUser("admin").password(bCryptPasswordEncoder.encode("admin")).authorities("admin");*/
}

/**
* oauth2 的密码模式需要在令牌访问端点注入此配置,管理security操作的用户信息
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}



@Bean
public PasswordEncoder passwordEncoder() {
//不使用加密方式的编码器,字符串比较
// return NoOpPasswordEncoder.getInstance();
//使用加密密码编码器
return new BCryptPasswordEncoder();
}
}

2.2.4 四种模式的认证请求

2.2.4.1 授权码模式(较安全模式):

客户端进行授权请求(此处授权的客户端信息为oauth2表格维护的客户端信息),用户登录后进行登录(此处登录使用的是userDetailService查回的信息,即security管理的用户)后进行授权,通过回调url返回code(授权码)https://www.baidu.com/?code=m4aOtl

根据授权码和客户端secret等信息获取token(此处的token使用的是jwt):

1
2
3
4
5
6
7
8
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FQSSJdLCJleHAiOjE2MDY2MjY0NTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiMDQ1N2ZiNTgtMmY0Ny00NGJmLTk0NzMtMTRiNWU2ZmI4M2ZmIiwiY2xpZW50X2lkIjoiYzEifQ.EjYisjGOtqrTJQc7nqWBtaStphF1PxTe07_pC0oireM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FQSSJdLCJhdGkiOiIwNDU3ZmI1OC0yZjQ3LTQ0YmYtOTQ3My0xNGI1ZTZmYjgzZmYiLCJleHAiOjE2MDY4Nzg0NTcsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNGQ4NjMxM2YtYjI3Zi00NTZiLWJlYzItMjc1NDY2N2UwMjIwIiwiY2xpZW50X2lkIjoiYzEifQ.1qItJNh8cGEiTEGJY1b2gBeeGq904lx8sZB7GaRMPmk",
"expires_in": 7199,
"scope": "ROLE_API",
"jti": "0457fb58-2f47-44bf-9473-14b5e6fb83ff"
}
2.2.4.2 隐式授权模式/简化模式
  • 申请token-GET:

http://localhost:9305/oauth/authorize?client_id=c1&response_type=token&scope=ROLE_API&redirect_uri=http://www.baidu.com

此模式不需要获取授权码,用户登录后进行登录(此处登录使用的是userDetailService查回的信息,即security管理的用户)后进行授权,回调url返回token

https://www.baidu.com/#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FQSSJdLCJleHAiOjE2MDY2Mjc3NzUsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNjhiOTkyMjYtNTI3NC00MmI5LTk1MTMtNzA5NzE2OWE2OTA2IiwiY2xpZW50X2lkIjoiYzEifQ.3jVUpS6o_zmRRF0eHjjCRHHwYncbaaZg4zIBt-0Jz_s&token_type=bearer&expires_in=7199&jti=68b99226-5274-42b9-9513-7097169a6906

2.2.4.3 密码模式(一般用于自有开发的客户端使用,否则有密码泄露风险)
  • 申请token-POST:

http://localhost:9300/auth/oauth/token?username=zhangsan&password=123&grant_type=password&scope=ROLE_API&client_id=c1&client_secret=secret

1
2
3
4
5
6
7
8
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FQSSJdLCJleHAiOjE2MDY2Mjg0MjYsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNDRjZjNlN2QtNjIwYi00M2FhLThjMWItMGZkMWVkZjE3YTMwIiwiY2xpZW50X2lkIjoiYzEifQ.N_Wd2DKyEpzGBmnvnDrP-vx4lSzIjiInQRNTsJtxMkM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwidXNlcl9uYW1lIjoiemhhbmdzYW4iLCJzY29wZSI6WyJST0xFX0FQSSJdLCJhdGkiOiI0NGNmM2U3ZC02MjBiLTQzYWEtOGMxYi0wZmQxZWRmMTdhMzAiLCJleHAiOjE2MDY4ODA0MjYsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNmJiOWQ5YTAtYzhiYi00ZDk4LTgwMGYtMzY1NGY5ZjU1MDgxIiwiY2xpZW50X2lkIjoiYzEifQ.AL2ODdEhSKwuzrkYCvikY6UBrljFBroPtFiI9UT5xs4",
"expires_in": 7199,
"scope": "ROLE_API",
"jti": "44cf3e7d-620b-43aa-8c1b-0fd1edf17a30"
}
2.2.4.4 客户端模式(这种方式最简便也最不安全,需要对客户端完全信任,用于合作系统的对接,没有刷新token)
  • 申请token-POST

http://localhost:9305/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

1
2
3
4
5
6
7
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicjEiXSwic2NvcGUiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiIsIlJPTEVfQVBJIl0sImV4cCI6MTYwNjYyODU2MSwianRpIjoiZDBmODBjNTEtMWU5Ny00ODA5LThjNmEtYmEzYjYzNTZhMGE5IiwiY2xpZW50X2lkIjoiYzEifQ.I7KIvzQLYqlkzMQqw2hOPZ-ev8cV4ehoDU9LsP4qAAM",
"token_type": "bearer",
"expires_in": 7199,
"scope": "ROLE_ADMIN ROLE_USER ROLE_API",
"jti": "d0f80c51-1e97-4809-8c6a-ba3b6356a0a9"
}

三、资源服务(学习过渡,之后的资源服务不这么配置,参考目录五)

3.1 maven依赖(同鉴权服务)

3.2 配置类

3.2.1 资源服务配置

对于token的校验可以请求远程auth服务,也可以使用其他tokenStore进行自我校验,本次使用jwt方式进行自我校验,不请求远程资源服务器

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

public static final String RESOURCE_ID = "r1";

@Autowired
private TokenStore tokenStore;
@Autowired
private ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//符合对应的scope才能访问
.antMatchers("/**").access("#oauth2.hasScope('ROLE_API')")
.and()
.csrf().disable()
//不用记录session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//资源id
resources.resourceId(RESOURCE_ID)
//验证令牌服务(远程校验)
// .tokenServices(tokenService())
//本服务自身校验jwt令牌
.tokenStore(tokenStore)
.stateless(true)
//权限不足处理类
.accessDeniedHandler(myAccessDeniedHandler)
//各种AuthenticationException细化处理
.authenticationEntryPoint(resourceAuthExceptionEntryPoint);
}

/**
* 远程请求校验令牌
*/
@Bean
public ResourceServerTokenServices tokenService(){
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:9305/oauth/check_token");
services.setClientId("c1");
services.setClientSecret("secret");
return services;
}
}

3.2.2 令牌存储策略(同鉴权服务2.2.2)

3.2.3 security配置

将方法权限的控制交给security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/order/r1").hasAuthority("p2")
.antMatchers("order/r2").hasAuthority("p2")
//所有/order/**的请求必须认证通过
.antMatchers("/order/**").authenticated()
//除了/order/**,其他请求可以访问
.anyRequest().permitAll();
}
}

四、网关资源校验服务

网关进行token校验,合法后对用户和权限信息封装,再下发下游服务,下游服务可以不再集成oauth2依赖,获取网关封装的权限信息,再次封装入security的上下文中,通过security完成权限校验

  • 网关对鉴权服务的认证请求放行
  • 网关的资源服务会对用户的请求进行合法性校验
  • 网关过滤器对token进行封装自定义用户信息json格式下发下游服务
  • 下游服务过滤器对自定义用户权限信息封装的security安全上下文中
  • security接管下游服务的权限控制

4.1 maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 此依赖包含security,所以不需要额外导入security -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- 健康检查 oauth2会使用到一些端点信息需要加上此依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 使用redis存储token才需要引入,其他方式存储可以不配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

4.2 配置类

4.2.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
/**
* oauth2资源服务配置类
* 网关整合都有的微服务资源拦截
* 此类用于配置每个微服务的资源配置类
*/
@Configuration
public class ResourceServerConfig {

/**
* 资源id(实际使用时,每个服务对应自己的资源id)
*/
public static final String RESOURCE_ID = "r1";
@Autowired
private TokenStore tokenStore;
@Autowired
private ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;

/**
* auth微服务资源
*/
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenStore(tokenStore)
.resourceId(RESOURCE_ID)
.stateless(true)
//权限不足处理类
.accessDeniedHandler(myAccessDeniedHandler)
//各种AuthenticationException细化处理
.authenticationEntryPoint(resourceAuthExceptionEntryPoint);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//需要放行,否则用户无法获取token
.antMatchers("/auth/**").permitAll();

}
}

/**
* pay微服务资源
*/
@Configuration
@EnableResourceServer
public class PayServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenStore(tokenStore)
.resourceId(RESOURCE_ID)
.stateless(true);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/**").access("#oauth2.hasScope('ROLE_API')");
}
}
}

4.2.2 令牌存储策略配置(同鉴权服务2.2.2)

4.2.3 security配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* security拦截配置
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and()
.csrf()
.disable();
}
}

4.2.4 过滤器封装token用户及权限信息

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
/**
* 该过滤器进行权限信息解析,封装到header路由转发都下级微服务
* 下级微服务将权限信息放入security上下文中,从而可以使用security的权限控制
*/
@Component
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {

// 获取令牌内容
RequestContext currentContext = RequestContext.getCurrentContext();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

//不是oauth认证信息,无法访问
if (!(authentication instanceof OAuth2Authentication)) {
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
Object principal = userAuthentication.getPrincipal();

// 组装明文token,转发给微服务,放入header,名称为json-token
List<String> authList= new ArrayList<>();
userAuthentication.getAuthorities().forEach(auth-> authList.add(auth.getAuthority()));

//获取请求参数
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
jsonToken.put("principal",principal);
jsonToken.put("authorities",authList);


ObjectMapper objectMapper = new ObjectMapper();
String jsonTokenStr = "";
try {
jsonTokenStr = objectMapper.writeValueAsString(jsonToken);
} catch (JsonProcessingException e) {
e.printStackTrace();
}

//将身份信息和权限信息放在json中,加入header中,转发给下级微服务
currentContext.addZuulRequestHeader("json-token", Base64.getEncoder().encodeToString(jsonTokenStr.getBytes(StandardCharsets.UTF_8)));

return null;
}
}

4.2.5 自定义处理类

4.2.5.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
/**
* 授权拒绝处理器,覆盖默认的OAuth2AccessDeniedHandler
* 当权限不足时走此类
*/
@Slf4j
@Component
@AllArgsConstructor
public class MyAccessDeniedHandler extends OAuth2AccessDeniedHandler {
private final ObjectMapper objectMapper;

/**
* 授权拒绝处理,使用R包装
*
* @param request request
* @param response response
* @param authException authException
*/
@Override
@SneakyThrows
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) {
log.info("授权失败,禁止访问 {}", request.getRequestURI());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(new Result(false,403,"权限不足,禁止访问")));
}
}

4.2.5.2 各种AuthenticationException细化处理

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
/**
* 可以根据 AuthenticationException 不同细化异常处理
*/
@Slf4j
@Component
@AllArgsConstructor
public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
private static final Logger logger = LoggerFactory.getLogger(ResourceAuthExceptionEntryPoint.class);
@Override
@SneakyThrows
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
Result result = new Result();
result.setCode(HttpStatus.UNAUTHORIZED.value());
if (authException != null) {
result.setMsg("error");
result.setData(authException.getMessage());
}

if(authException.getCause() == null){
result.setMsg("error");
logger.error("no right to access or token invalid!,msg:{}",authException.getMessage());
}
else{
String exceptionStr = authException.getCause().toString();
if (StringUtils.contains(exceptionStr, "unauthorized")) {//oauth2.0认证,用户不存在
result.setCode(11);
result.setMsg(BusinessErrorEnum.LOGIN_USER_NOTEXIST.getText());
logger.info(BusinessErrorEnum.LOGIN_USER_NOTEXIST.getText(),authException);

} else if (StringUtils.contains(exceptionStr, "invalid_grant")) {//oauth2.0认证,密码错误
result.setCode(BusinessErrorEnum.LOGIN_PASSWORD_ERROR.getIndex());
result.setMsg(BusinessErrorEnum.LOGIN_PASSWORD_ERROR.getText());
logger.info(BusinessErrorEnum.LOGIN_PASSWORD_ERROR.getText(),authException);

} else if (StringUtils.contains(exceptionStr, "invalid_token")) {//oauth2.0认证,token无效
result.setCode(BusinessErrorEnum.LOGIN_TOKEN_ERROR.getIndex());
result.setMsg(BusinessErrorEnum.LOGIN_TOKEN_ERROR.getText());
logger.info(BusinessErrorEnum.LOGIN_TOKEN_ERROR.getText(),authException);

} else {//其他错误
result.setCode(BusinessErrorEnum.LOGIN_ERROR.getIndex());

result.setMsg(BusinessErrorEnum.LOGIN_ERROR.getText());
logger.info(BusinessErrorEnum.LOGIN_ERROR.getText(),authException);
}
}

response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(result));


}
}

五、下游资源微服务

5.1 maven依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

5.2 配置

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
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenAuthenticationFilter tokenAuthenticationFilter;
/**
* 权限不足处理器
*/
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
/**
* 用来解决匿名用户访问无权限资源时的异常处理器
*/
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/pay/r1").hasAuthority("p3")
.antMatchers("pay/r2").hasAuthority("p1")
//所有/pay/**的请求必须认证通过
.antMatchers("/pay/**").authenticated()
//除了/pay/**,其他请求可以访问
.anyRequest().permitAll();
http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
//处理异常情况:认证失败和权限不足
http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint).accessDeniedHandler(myAccessDeniedHandler);
}
}

5.3 自定义异常处理

5.3.1 权限不足

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @Description: 权限不足效验
*/
@Component
@Slf4j
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Result result = new Result(false,403,"权限不足");
HttpResponseUtil.responseJsonWriter(httpServletResponse,result);
}
}

5.3.2 匿名用户无权限处理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {

Result result = new Result(false,403,"超过登录有效期,请重新登录!");
//输出
HttpResponseUtil.responseJsonWriter(response, result);
}
}

5.4 封装自定义权限到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
/**
* 将网关下发的权限信息放入security上下文中,交由security进行权限控制
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

//解析header中的token
String token = httpServletRequest.getHeader("json-token");
if(StringUtils.hasText(token)){
String jsonTokenStr = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonTokenStr);
String principal = jsonNode.get("principal").asText();
UserPo userPo = new UserPo();
userPo.setUsername(principal);
Iterator<JsonNode> authorities = jsonNode.get("authorities").iterator();
List<String> authList = new ArrayList<>();
while (authorities.hasNext()){
authList.add(authorities.next().asText());
}
String[] authArr = authList.toArray(new String[authList.size()]);

//封装新的权限token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userPo,null, AuthorityUtils.createAuthorityList(authArr));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));

//存入security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

5.5 自定义security权限注解校验

5.5.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
/**
* 权限判断工具
*/
@Component("pms")
public class PermissionUtil {
/**
* 判断接口是否有xxx:xxx权限
*
* @param permission 权限
* @return {boolean}
*/
public boolean hasPermission(String permission) {

if(StringUtils.isEmpty(permission)){
return false;
}

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
UserPo userPo = (UserPo) authentication.getPrincipal();


Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permission, x));
}
}

5.5.2 注解

方法上添加注解进行权限控制

1
@PreAuthorize("@pms.hasPermission('p1')" )

六、数据库表格

https://www.cnblogs.com/zxy-come-on/p/14047791.html