0%

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填入

---------------- The End ----------------

欢迎关注我的其它发布渠道