SpringSecurty-OAuth2配置鉴权

这两天负责一个新项目的搭建,需要对接第三方用户系统,对方使用的是keycloak认证中心平台,所以只需要拿到对方的/certs地址就可以进行对用户的请求头Authorizationtoken进行签名的校验,然后获取token中的权限和一些信息内容

1.pom.xml中引入spring-security依赖
1
2
3
4
5
6
7
8
9
10
11
<!--security start-->
<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>
<!--security end-->

因为目前只做鉴权用户和识别用户权限,只需要这两个就够了

2.配置application.yml中的授权地址
1
2
3
4
5
6
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://project.com/auth/realms/realms-name/protocol/openid-connect/certs

基本认证中心的地址都长的差不多

3.配置SecurityConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy())
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
//允许无权限访问
.antMatchers("/api/v1/sample/messages").permitAll()
.antMatchers("/api/v1/sample/**").hasAnyAuthority("SCOPE_ligafi.end","ligafi.end")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.jwt();
}
}
4.写一个SampleController用来测试可行性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/api/v1/sample")
@Api(value = "A Sample Controller", tags = {"Demo Tag"})
public class SampleController extends BaseController{

@PostMapping("/messages")
@ApiOperation("from user get message")
public String message(){
return "this is a test message";
}

@GetMapping("/getParam")
@ApiOperation("getParam")
public String getParam() {
return "this is a test getParam";
}

}

这里的Swagger引入和配置就不说了,有手就行的东西,无非就是在SecurityConfig加一个允许的权限

这样:

1
2
3
4
5
6
7
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/v2/api-docs/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/webjars/**");
}
5.找到keycloak平台获取token的地址,获取一个token进行测试

https://project.com/auth/realms/realms-name/protocol/openid-connect/token

该平台因为需要校验用户的权限ROLE和客户端的权限SCOPE,这些JWT(Json Web Token)里面的内容就不做解释了,去https://jwt.io/就可以了解到了

从图中可以看到,获取到了token,尝试解析一下token

现在第三方调用我们业务的时候需要同时校验realm_access中的roles里面的ligafi.end权限和客户端的scope权限ligafi.end,现在去Swagger试试

image.png

需要注意的是,请求头的AuthorizationBearer类型的token

看着好像是通过了,但是我们通过在源码里面打断点看看情况

org.springframework.security.access.expression.SecurityExpressionRoot#hasAnyAuthorityName

获取到的权限里面只有客户端的scope部分,而且hasAnyAuthority("SCOPE_ligafi.end","ligafi.end")的校验中,只要有一项权限符合要求就通过,所以不能同时校验客户端scope和用户的roles

先处理无法获取用户权限的问题

在配置SecurityConfig中自定义一个AuthenticationConverter

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
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy())
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
//允许无权限访问
.antMatchers("/api/v1/sample/messages").permitAll()
//先校验客户端权限,接口校验用户权限,因为hasAnyAuthority不能实现hasEveryAuthority,需要分开校验
.antMatchers("/api/v1/sample/**").hasAuthority("SCOPE_ligafi.end")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.jwt()
.jwtAuthenticationConverter(grantedAuthoritiesExtractorConverter());
}

@Bean
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractorConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesExtractor());
return jwtAuthenticationConverter;
}

@Bean
GrantedAuthoritiesExtractor grantedAuthoritiesExtractor() {
return new GrantedAuthoritiesExtractor();
}

@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/v2/api-docs/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/webjars/**");
}

}

通过@EnableGlobalMethodSecurity(prePostEnabled = true)开启方法上的注解@PreAuthorize来校验权限

GrantedAuthoritiesExtractor

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
public class GrantedAuthoritiesExtractor implements Converter<Jwt, Collection<GrantedAuthority>> {

@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
List<GrantedAuthority> authorities = new ArrayList<>();
String realmAccess = "realm_access";
String roles = "roles";
String scope = "scope";
if (jwt.containsClaim(realmAccess)) {
JSONObject realmAccessJson = (JSONObject) jwt.getClaims().get(realmAccess);
if (realmAccessJson.containsKey(roles)) {
JSONArray realmRoles = (JSONArray) realmAccessJson.get(roles);
for (Object realmRole : realmRoles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" +realmRole.toString()));
}
}
}
if (jwt.containsClaim(scope)) {
String scopeStr = (String) jwt.getClaims().get(scope);
if (!StringUtils.isEmpty(scopeStr) && !scopeStr.isEmpty()) {
String[] scopes = scopeStr.split("\\s");
for (String scopeAuthority : scopes) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scopeAuthority));
}
}
}
return authorities;
}
}

ROLE_SCOPE_来区分客户端和用户的权限

在方法上添加注解

1
2
3
4
5
6
@GetMapping("/getParam")
@PreAuthorize("hasRole('ligafi.end')")
@ApiOperation("getParam")
public String getParam() {
return "this is a test getParam";
}

hasRole会自动帮权限加上ROLE_,所以我们之前就直接authorities.add(new SimpleGrantedAuthority("ROLE_" +realmRole.toString()))自己手动加上。

再去Swagger试试

现在所有权限都获取到了,而且校验了两次,所以这样校验是正确的方式

成功返回结果。

总结:最主要的地方就是SecurityConfig中自定义的jwtAuthenticationConverter 和方法上的注解@PreAuthorize("hasRole('ligafi.end')"),因为不能同时校验两个权限,目前想到的方式就是分开校验。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!