SpringBoot-OAuth2-JWT-ResourceServer

摘要

依赖

  • springboot官方提供了支持

1
2
3
4
5
6
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

//本项目中使用了lombok减代码量,非必须依赖
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

WebSecurityConfig

  • springboot没有为resource-server设置独立的注解和配置类,而是把这部分功能整合到了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
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
package com.example.oauth2resourceserverdemo2.config;

import com.example.oauth2resourceserverdemo2.security.CustomAccessDeniedHandler;
import com.example.oauth2resourceserverdemo2.security.CustomAuthExceptionEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collection;
import java.util.stream.Collectors;

/**
* <h1>WebSecurityConfig</h1>
* Created by hanqf on 2020/11/11 17:01.
*/

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;

@Autowired
private CustomAuthExceptionEntryPoint customAuthExceptionEntryPoint;

@Override
protected void configure(HttpSecurity http) throws Exception {

//开启跨域
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

//权限控制
http.authorizeRequests()//登录成功就可以访问
.antMatchers("/res/**", "/userInfo/**").authenticated()
//需要具备相应的角色才能访问
.antMatchers("/user/**").hasAnyRole("admin", "user")
//不需要登录就可以访问
.antMatchers("/swagger-ui/**", "/v3/api-docs**").permitAll()
//其它路径需要根据指定的方法判断是否有权限访问,基于权限管理模型认证
.anyRequest().access("@rbacService.hasPerssion(request,authentication)");


//鉴权时只支持Bearer Token的形式,不支持url后加参数access_token
http.oauth2ResourceServer()//开启oauth2资源认证
.jwt() //token为jwt
//默认情况下,权限是scope,而我们希望使用的是用户的角色,所以这里需要通过转换器进行处理
.jwtAuthenticationConverter(jwt -> { //通过自定义Converter来指定权限,Converter是函数接口,当前上下问参数为JWT对象
Collection<SimpleGrantedAuthority> authorities =
((Collection<String>) jwt.getClaims()
.get("authorities")).stream() //获取JWT中的authorities
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());

//如果希望保留scope的权限,可以取出scope数据然后合并到一起,这样因为不是以ROLE_开头,所以需要使用hasAuthority('SCOPE_any')的形式
Collection<SimpleGrantedAuthority> scopes = ((Collection<String>) jwt.getClaims()
.get("scope")).stream().map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toSet());
//合并权限
authorities.addAll(scopes);
return new JwtAuthenticationToken(jwt, authorities);
});

//这部分看代码吧,没啥可说的
http.exceptionHandling()
//access_token无效或过期时的处理方式
.authenticationEntryPoint(customAuthExceptionEntryPoint)
//access_token认证后没有对应的权限时的处理方式
.accessDeniedHandler(customAccessDeniedHandler);
}

/**
* 跨域配置类
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
//开放哪些ip、端口、域名的访问权限,星号表示开放所有域
corsConfiguration.addAllowedOrigin("*");
//corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8080","http://localhost:8081"));
//开放哪些Http方法,允许跨域访问
corsConfiguration.addAllowedMethod("*");
//corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE"));
//允许HTTP请求中的携带哪些Header信息
corsConfiguration.addAllowedHeader("*");
//是否允许发送Cookie信息
corsConfiguration.setAllowCredentials(true);

//添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", corsConfiguration);

return configSource;
}
}

WebSecurityConfig说明

整体配置方式就是spring-security的配置方式,这里唯一的不同就是如下这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//鉴权时只支持Bearer Token的形式,不支持url后加参数access_token
http.oauth2ResourceServer()//开启oauth2资源认证
.jwt() //token为jwt
//默认情况下,权限是scope,而我们希望使用的是用户的角色,所以这里需要通过转换器进行处理
.jwtAuthenticationConverter(jwt -> { //通过自定义Converter来指定权限,Converter是函数接口,当前上下问参数为JWT对象
Collection<SimpleGrantedAuthority> authorities =
((Collection<String>) jwt.getClaims()
.get("authorities")).stream() //获取JWT中的authorities
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());

//如果希望保留scope的权限,可以取出scope数据然后合并到一起,这样因为不是以ROLE_开头,所以需要使用hasAuthority('SCOPE_any')的形式
Collection<SimpleGrantedAuthority> scopes = ((Collection<String>) jwt.getClaims()
.get("scope")).stream().map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toSet());
//合并权限
authorities.addAll(scopes);
return new JwtAuthenticationToken(jwt, authorities);
});
  • 一般情况下,我们只需要这样配置

1
http.oauth2ResourceServer().jwt();

此时就已经开启的oauth2资源服务器的支持,但是此时我们鉴权后获取到的权限是scope,这可能并不是我们希望的,所以我们可以为其指定一个转换器,从access_token的payload中获取authorities属性,并将其设置为用户的权限,如果我们还希望同时得到scope,则也可以再次取出scope的值放入用户的权限列表中。

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
spring:
#oauth2 配置
security:
oauth2:
resourceserver:
jwt:
# 公钥文件路径
# public-key-location: classpath:oauth2_key.pub

# 认证服务器提供的密钥验证路径,这种方式每次验证access_token都需要访问认证服务器
jwk-set-uri: http://localhost:8080/.well-known/jwks.json

说明

  • 本项目基于jks等非对称密钥的方式

  • 密钥配置方式支持本地配置和认证服务器远程获取

  • 如果是基于本地配置的方式,则认证服务器什么都不需要做,但是如果是基于认证服务器远程获取的方式,则需要为认证服务器添加对应的功能。

AuthServer改造

JwkSetEndpoint

  • 增加jwk-set验证端点,该功能就是为了获取公钥

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
package com.example.oauth2authserverdemo.controller;

import com.example.oauth2authserverdemo.security.jwt.JwtTokenProperties;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;

@FrameworkEndpoint //@FrameworkEndpoint和@Controller相同功能,只用于框架提供的端点
public class JwkSetEndpoint {

@Autowired
private JwtTokenProperties jwtTokenProperties;


@GetMapping("/.well-known/jwks.json")
@ResponseBody
public Map<String, Object> getKey() {
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(jwtTokenProperties.getJksKeyFileResource(), jwtTokenProperties.getJksStorePassword().toCharArray());
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(jwtTokenProperties.getJksKeyAlias(), jwtTokenProperties.getJksKeyPassword().toCharArray());
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}

}

SecurityConfig开发对应的权限

1
2
3
4
http.authorizeRequests()
.antMatchers(customSecurityProperties.getPermitAll()).permitAll() //不用身份认证可以访问
.mvcMatchers("/.well-known/jwks.json").permitAll() //开放JWK SET端点,提供给资源服务器访问获取公钥信息
.anyRequest().authenticated(); //其它的请求要求必须有身份认证

资源,这里以UserController举例

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
package com.example.oauth2resourceserverdemo2.controller;

import com.example.oauth2resourceserverdemo2.exception.AjaxResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {

//注意权限区分大小写
@PreAuthorize("hasRole('admin') or hasRole('user')")
//@PreAuthorize("#oauth2.hasScope('any')") //不支持oauth2表达式
@RequestMapping(value = "/user")
public AjaxResponse user(Principal principal) {
//principal在经过security拦截后,是org.springframework.security.authentication.UsernamePasswordAuthenticationToken
//在经OAuth2拦截后,是OAuth2Authentication
return AjaxResponse.success(principal);
}

//注意权限区分大小写
@PreAuthorize("hasAuthority('SCOPE_any')")
@RequestMapping(value = "/user2")
public AjaxResponse user2(Principal principal) {
//principal在经过security拦截后,是org.springframework.security.authentication.UsernamePasswordAuthenticationToken
//在经OAuth2拦截后,是OAuth2Authentication
return AjaxResponse.success(principal);
}

/**
* 获取用户的claim信息
*/
@RequestMapping("/userInfo")
public Map<String, Object> userInfo(Authentication authentication){
Map<String,Object> map = new HashMap<>();
Object principal = authentication.getPrincipal();
if(principal instanceof Jwt){
map.put("username", ((Jwt) principal).getClaim("user_name"));
map.putAll(((Jwt) principal).getClaims());
}
return map;
}

}

资源服务器访问方式

  • 通过access_token访问受保护的资源

只支持Bearer Token

1
2
3
4
5
http://localhost:8082/user

# 在请求的header中设置参数:参数名称:Authorization,值是`[grant_type] [access_token]`,grant_type值与access_token值之间用空格分开。例如:

bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY1ODc4NiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJjYjEzZjhmZC03NWRiLTRmODItOTkxOC00YzFjZGI3MDEwMGMiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.W78nue0rPxB-Te7ZsxfzmTUYTasHHfQT0lMgAMG_i5g

使用Postman接口测试工具时,也可以使用其提供的认证功能[Authorization–>TYPE–> Bearer Token],然后将access_token填入