0%

SpringBoot-OAuth2-JWT-WebFlux-ResourceServer

摘要

依赖

  • springboot官方提供了支持
    1
    2
    3
    4
    5
    6
    7
    //webflux
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

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

    ReactiveSecurityConfig

    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
    package com.example.oauth2resourceserverwebfluxdemo.config;

    import com.example.oauth2resourceserverwebfluxdemo.security.CustomReactiveAuthorizationManager;
    import com.example.oauth2resourceserverwebfluxdemo.security.CustomServerAccessDeniedHandler;
    import com.example.oauth2resourceserverwebfluxdemo.security.CustomServerAuthenticationEntryPoint;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
    import org.springframework.security.config.web.server.ServerHttpSecurity;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
    import org.springframework.security.web.server.SecurityWebFilterChain;
    import reactor.core.publisher.Mono;

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

    /**
    * <h1>安全认证配置</h1>
    * Created by hanqf on 2020/11/19 10:26.
    */


    @Configuration
    //@EnableWebFluxSecurity //非必要
    @EnableReactiveMethodSecurity //启用@PreAuthorize注解配置
    public class ReactiveSecurityConfig {


    @Autowired
    private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;

    @Autowired
    private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;

    @Autowired
    private CustomReactiveAuthorizationManager customReactiveAuthorizationManager;

    /**
    * 注册安全验证规则
    * 配置方式与HttpSecurity基本一致
    */
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { //定义SecurityWebFilterChain对安全进行控制,使用ServerHttpSecurity构造过滤器链;
    return http.authorizeExchange()
    //.anyExchange().authenticated() //所有请求都需要通过认证
    .pathMatchers("/res/**", "/userInfo/**").authenticated()
    //需要具备相应的角色才能访问
    .pathMatchers("/user/**").hasAnyRole("admin", "user")
    //不需要登录就可以访问
    .pathMatchers("/swagger-ui/**", "/v3/api-docs**").permitAll()

    //其它路径需要根据指定的方法判断是否有权限访问,基于权限管理模型认证
    .anyExchange().access(customReactiveAuthorizationManager)
    .and()
    .csrf().disable() //关闭CSRF(Cross-site request forgery)跨站请求伪造
    .httpBasic().disable() //不支持HTTP Basic方式登录
    .formLogin().disable()//不支持login页面登录
    .cors() //开启跨域支持
    .and()

    //鉴权时只支持Bearer Token的形式,不支持url后加参数access_token
    .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 Mono.just(new JwtAuthenticationToken(jwt, authorities));
    })
    .and()
    .accessDeniedHandler(customServerAccessDeniedHandler)
    .authenticationEntryPoint(customServerAuthenticationEntryPoint)
    .and().build();
    }
    }

    CustomReactiveAuthorizationManager

    基于RBAC权限认证管理模型的认证方式
    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
    package com.example.oauth2resourceserverwebfluxdemo.security;

    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.security.authorization.AuthorizationDecision;
    import org.springframework.security.authorization.ReactiveAuthorizationManager;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.oauth2.jwt.Jwt;
    import org.springframework.security.web.server.authorization.AuthorizationContext;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.StringUtils;
    import reactor.core.publisher.Mono;

    import java.util.HashSet;
    import java.util.Set;

    /**
    * <h1>ReactiveAuthorizationManager</h1>
    * Created by hanqf on 2020/11/30 12:05.
    */

    @Component
    public class CustomReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
    return authentication.map(auth -> {
    ServerHttpRequest request = object.getExchange().getRequest();
    Object principal = auth.getPrincipal();
    String username;
    if (principal instanceof Jwt) {
    username = ((Jwt) principal).getClaimAsString("user_name");
    } else {
    username = principal.toString();
    }
    boolean hasPerssion = false;
    if (StringUtils.hasText(username) && !"anonymousUser".equals(username)) {
    //根据用户名查询用户资源权限,这里应该访问数据库查询
    Set<String> uris = new HashSet<>();
    for (String uri : uris) {
    //验证用户拥有的资源权限是否与请求的资源相匹配
    if (new AntPathMatcher().match(uri, request.getURI().toString())) {
    hasPerssion = true;
    break;
    }
    }
    }

    //暂时全部返回true
    hasPerssion = true;
    return new AuthorizationDecision(hasPerssion);
    });
    }
    }

    CustomServerAccessDeniedHandler

    没有权限时的处理方式
    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
    package com.example.oauth2resourceserverwebfluxdemo.security;


    import com.example.oauth2resourceserverwebfluxdemo.exception.AjaxResponse;
    import com.example.oauth2resourceserverwebfluxdemo.exception.CustomException;
    import com.example.oauth2resourceserverwebfluxdemo.exception.CustomExceptionType;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.SneakyThrows;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;

    /**
    * <h1>没有权限时的处理方式</h1>
    * Created by hanqf on 2020/11/20 11:56.
    */

    @Component
    public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
    @SneakyThrows
    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
    return setErrorResponse(serverWebExchange.getResponse(),e.getMessage());
    }

    protected Mono<Void> setErrorResponse(ServerHttpResponse response, String message) throws JsonProcessingException {
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    ObjectMapper objectMapper = new ObjectMapper();
    AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, message));
    return response.writeWith(Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(ajaxResponse))));

    }
    }

    CustomServerAuthenticationEntryPoint

    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
    package com.example.oauth2resourceserverwebfluxdemo.security;


    import com.example.oauth2resourceserverwebfluxdemo.exception.AjaxResponse;
    import com.example.oauth2resourceserverwebfluxdemo.exception.CustomException;
    import com.example.oauth2resourceserverwebfluxdemo.exception.CustomExceptionType;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.SneakyThrows;
    import org.springframework.http.MediaType;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;

    /**
    * <h1>ServerAuthenticationEntryPoint</h1>
    * Created by hanqf on 2020/11/20 12:01.
    * <p>
    * token格式错误或过期时的处理方式
    */
    @Component
    public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
    @SneakyThrows
    @Override
    public Mono<Void> commence(ServerWebExchange serverWebExchange, AuthenticationException e) {
    return setErrorResponse(serverWebExchange.getResponse(), e.getMessage());
    }

    protected Mono<Void> setErrorResponse(ServerHttpResponse response, String message) throws JsonProcessingException {
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    ObjectMapper objectMapper = new ObjectMapper();
    AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, message));
    return response.writeWith(Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(ajaxResponse))));

    }
    }

    WebFluxConfig

    配置跨域
    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
    package com.example.oauth2resourceserverwebfluxdemo.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.reactive.config.CorsRegistry;
    import org.springframework.web.reactive.config.WebFluxConfigurer;

    /**
    * <h1>WebFlux配置类</h1>
    * Created by hanqf on 2020/11/18 17:43.
    *
    * 我们若需要配置Spring WebFlux只需让配置配实现接口WebFluxConfigurer,
    * 这样我们既能保留Spring Boot给WebFlux配置又能添加我们的定制配置。
    * 若我们向完全控制WebFlux,则在配置类添加注解@EnableWebFlux
    * 配置方式和Spring MVC类似
    */

    @Configuration
    public class WebFluxConfig implements WebFluxConfigurer {

    //跨域设置
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**") //添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
    .allowedMethods("GET","POST", "PUT", "DELETE") //开放哪些Http方法,允许跨域访问
    .allowedHeaders("*") //允许HTTP请求中的携带哪些Header信息
    //When allowCredentials is true, allowedOrigins cannot contain the special value "*"since that cannot be set on the "Access-Control-Allow-Origin" response header.
    // To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
    //.allowedOrigins("*") //开放哪些ip、端口、域名的访问权限
    .allowedOriginPatterns("*")
    .allowCredentials(true); //是否允许发送Cookie信息

    }
    }

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#资源服务器端口号
server:
port: 8083
spring:
application:
name: oauth2-resource-server-webflux

#oauth2 配置
security:
oauth2:
resourceserver:
jwt:
# 公钥文件路径
# public-key-location: classpath:oauth2_key.pub

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

资源接口示例

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


import com.example.oauth2resourceserverwebfluxdemo.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 reactor.core.publisher.Mono;

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

/**
* <h1>res</h1>
* Created by hanqf on 2020/11/6 17:22.
*/

@RestController
public class UserController {

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

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

/**
* 获取用户的claim信息
*/
@RequestMapping("/userInfo")
public Mono<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 Mono.just(map);
}

}
---------------- The End ----------------

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