认证授权-1

Author Avatar
ciky 11月 10,2024
  • 在其它设备中阅读本文章
  • 点击生成二维码

认证授权

    ├── 1 spring security框架
    │   ├── 1.1 认证Demo
    │   │   ├── (1) 添加依赖      
    │   │   ├── (2) 测试controller
    │   │   ├── (3) 安全管理配置 		(WebSecurityConfig)
    │	│	│
    │   ├── 1.2 授权Demo
    │   │   ├── (1) 测试controller     
    │   │   ├── (2) 安全管理配置 		(WebSecurityConfig)  
    │	│	│
    
    ├── 2 OAuth2协议
    │   ├── 2.1 认证流程
    │	│	│
    │   ├── 2.2 授权模式
    │   │   ├── (1) 授权码模式 
    │   │   │   ├── 授权服务器配置 	(AuthorizationServer)
    │   │   │   ├── 令牌配置		  (TokenConfig)
    │   │   │   ├── 配置认证管理bean 	 (WebSecurityConfig)     
    │   │   │   ├── 测试       
    │   │   ├── (2) 密码模式   
    │	│	│
    
    ├── 3 JWT令牌
    │   ├── 3.1 当前问题
    │	│	│
    │   ├── 3.2 jwt组成
    │   │   ├── (1) Header   
    │   │   ├── (2) Payload   
    │   │   ├── (3) Signature
    │	│	│
    │   ├── 3.3 认证服务生成jwt
    │   │   ├── (1) 令牌配置		  (TokenConfig)   
    │   │   ├── (2) 申请令牌   
    │   │   ├── (3) 校验jwt令牌
    │	│	│
    │   ├── 3.4 资源服务校验jwt
    │   │   ├── (1) 添加依赖  
    │   │   ├── (2) 新增令牌配置		(TokenConfig)
    │   │   ├── (3) 资源服务配置		(ResouceServerConfig)
    │   │   ├── (4) 测试
    │	│	│
    │   ├── 3.5 获取用户身份
    │   │   ├── (1) springSecurity获取  
    │   │   ├── (2) ThreadLocal获取  (原理)
    
    ├── 4 网关认证
    │   ├── 4.1 网关的职责
    │	│	│
    │   ├── 4.2 实现网关认证
    │   │   ├── (1) 添加依赖       
    │   │   ├── (2) 配置类   
    │   │   │   ├── 令牌配置类 		  (TokenConfig)
    │   │   │   ├── 安全拦截配置类		 (SecurityConfig)
    │   │   │   ├── 网关认证过滤器 	 (GatewayAuthFilter)     
    │   │   │   ├── 错误响应参数包装 	(RestErrorResponse)
    │   │   ├── (3) 白名单  
    │   │   ├── (4) 修改资源服务  

Spring Security 提供框架支持,OAuth2 用于授权,JWT 用于传递认证信息。

用的是springSecurity框架,基于Oauth2协议实现单点登录


1 spring security框架


1.1 认证Demo


(1) 添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

(2) 测试controller

@RestController
public class LoginController {

    @Autowired
    XcUserMapper userMapper;

    @RequestMapping("/login-success")
    public String loginSuccess() {

        return "登录成功";
    }

    @RequestMapping("/user/{id}")
    public XcUser getuser(@PathVariable("id") String id) {
        XcUser xcUser = userMapper.selectById(id);
        return xcUser;
    }
}

访问http://localhost:63070/auth/user/52

自动进入/login登录页面,/login是spring security提供的

image20241104195840209.png

(3) 安全管理配置(WebSecurityConfig)

/**
 * @description 安全管理配置
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 配置用户信息服务
     */
    @Bean
    public UserDetailsService userDetailsService() {
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    /**
     * 配置加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        //BCrypt加密方式
        //return new BCryptPasswordEncoder();

        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
    }
    
    /**
     * 配置安全拦截机制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
            .anyRequest().permitAll()//其它请求全部放行
            .and()
            .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
    }
    
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

访问http://localhost:64410/auth/user/52 ---->可以正常访问

访问http://localhost:64410/auth/r/r1 ---->未登录则显示登录界面,登录后则可以访问


1.2 授权Demo


(1) 测试controller

在controller中配置**/r/r1需要p1权限**,/r/r2需要p2权限

@RequestMapping("/r/r1")
@PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
public String r1() {
    return "访问r1资源";
}

@RequestMapping("/r/r2")
@PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
public String r2() {
    return "访问r2资源";
}

(2) 安全配置管理(WebSecurityConfig)

在WebSecurityConfig类配置zhangsan拥有p1权限,lisi拥有p2权限

//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

2 OAuth2协议


2.1 认证流程


微信扫码认证是基于OAuth2协议实现

image20241104195840209.png


2.2 授权模式


(1) 授权码模式

使用授权码去获取令牌(授权码的获取需要资源拥有者亲自授权同意才可以获取)

image20241104195840209.png


- 授权服务器配置(AuthorizationServer)

添加**@EnableAuthorizationServer**注解

继承AuthorizationServerConfigurerAdapter配置OAuth2授权服务器

/**
 * @description 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource(name = "authorizationServerTokenServicesCustom")
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    //客户端详情服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()  // 使用in-memory存储
                .withClient("XcWebApp") // client_id
                .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥(BCrypt加密)
                .resourceIds("xuecheng-plus")   //资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")      // 允许的授权范围
                .autoApprove(false) //false跳转到授权页面
                .redirectUris("http://www.51xuecheng.cn");  //客户端接收授权码的重定向地址
    }

    //令牌端点的访问配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)//认证管理器
                .tokenServices(authorizationServerTokenServices)//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    //令牌端点的安全配置
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("permitAll()")                  //oauth/check_token公开
                .allowFormAuthenticationForClients()                //表单认证(申请令牌)
        ;
    }

}

- 令牌配置(TokenConfig)

/**
 * @description 令牌配置
 **/
@Configuration
public class TokenConfig {
    
    @Autowired
    TokenStore tokenStore;

    @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }
    
    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
}

- 配置认证管理bean(WebSecurityConfig)

在WebSecurityConfig类配置

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

- 测试

  1. get请求获取授权码

    GET http://localhost:64410/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn

    client_id:客户端准入标识。

    response_type:授权码模式固定为code。

    scope:客户端权限。

    redirect_uri:跳转uri(当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码))

    image20241104195840209.png

  2. 请求成功,重定向至http://www.51xuecheng.cn/?code=授权码

    (例:http://www.51xuecheng.cn/?code=Wqjb5H)

  3. 使用httpClient工具发送POST申请令牌

    POST http://localhost:64410/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=Wqjb5H&redirect_uri=http://www.51xuecheng.cn

    • client_id:客户端准入标识。

    • client_secret:客户端秘钥。

    • grant_type:授权类型,填写authorization_code,表示授权码模式

    • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。

    • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

    image20241104195840209.png

  4. 申请令牌成功

    {
      "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
      "token_type": "bearer",
      "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
      "expires_in": 7199,
      "scope": "all"
    }
    

    • access_token,访问令牌,用于访问资源使用。

    • token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容

    • refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。

    • expires_in:过期时间(秒)

    • scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。


(2) 密码模式

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client

image20241104195840209.png

  1. POST请求获取令牌

    POST http://localhost:64410/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

    • client_id:客户端准入标识。

    • client_secret:客户端秘钥。

    • grant_type:授权类型,填写password表示密码模式

    • username:资源拥有者用户名。

    • password:资源拥有者密码。

  2. 申请令牌成功

    {
      "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
      "token_type": "bearer",
      "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
      "expires_in": 6806,
      "scope": "all"
    }
    

3 JWT令牌


3.1 当前问题

普通令牌: "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2"

image20241104195840209.png

  • 步骤4.5.6,携带令牌获取资源时,需要远程请求认证服务校验令牌,令牌合法才返回资源
  • 每次获取获取资源都需要远程调用认证服务校验令牌,性能低

3.2 jwt组成


(1) Header

头部:包括令牌的类型及使用的哈希算法

{
	"alg": "HS256",
	"typ": "JWT"
}

(2) payload

负载:存放有效信息的地方(例如:iss签发者,exp过期时间戳,sub面向的用户,自定义字段等)

​ 不建议存放敏感信息,此部分可以解码还原原始内容

{
    "sub": "1234567890",
    "name": "456",
    "admin": true
}

(3) Signature

  • 签名:用于防止jwt内容被篡改

    1.使用base64url将前两部分进行编码

    2.编码后使用.连接组成字符串

    3.最后使用header中声明的签名算法进行签名

 HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

// secret:签名所使用的密钥

3.3 认证服务生成jwt


(1) 令牌配置(TokenConfig)

/**
 * @description 令牌配置
 **/
@Configuration
public class TokenConfig {

    // jwt签名
    private String SIGNING_KEY = "mq123";

    @Autowired
    TokenStore tokenStore;

    // 使用jwt的tokenstore
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

}

(2) 申请令牌

POST http://localhost:64410/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123


(3) 校验jwt令牌

POST http://localhost:64410/auth/oauth/check_token?token=jwt...

/auth/oauth/token

/auth/oauth/check_token

都是spring security框架自带的

//校验结果
{
  "aud": [
    "xuecheng-plus"
  ],
  "user_name": "zhangsan",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1721317102,
  "authorities": [
    "p1"
  ],
  "jti": "09fa337e-c8d5-44f2-a7b3-9c7b72080b32",
  "client_id": "XcWebApp"
}

3.4 资源服务校验jwt


(1) 添加依赖

<!--认证相关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

(2) 新增令牌配置(TokenConfig)

/**
 * @description 令牌配置
 **/
@Configuration
public class TokenConfig {

    String SIGNING_KEY = "mq123";
    
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

(3) 资源服务配置(ResouceServerConfig)

/**
* @description 资源服务配置
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    //资源服务标识	(=========对应认证服务配置的resourceId=========)
    public static final String RESOURCE_ID = "xuecheng-plus";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
    resources.resourceId(RESOURCE_ID)//资源 id
           .tokenStore(tokenStore)
           .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
              .authorizeRequests()
      		  //所有/r/** 和 /course/**的请求必须认证通过
              .antMatchers("/r/**","/course/**").authenticated()
              //除了上面配置的之外的请求全部放行 
              .anyRequest().permitAll()	
        ;
    }

}

(4) 测试

### 课程查询
GET http://localhost:64430/content/course/40
Authorization: Bearer JWT令牌...

3.5 获取用户身份


(1) springSecurity获取

底层是用本地线程变量ThreadLocal

//获取当前用户的身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

(2) ThreadLocal获取(原理)

拦截器,在接口入口获取user信息,放到线程本地变量

则在controller和service都可以直接从线程本地变量获取用户信息

- 登录用户本地变量

/**
 * @Author: ciky
 * @Description: 登录用户本地变量
 * @DateTime: 2024/9/19 21:18
 **/
public class LoginUserContext {
    private static final Logger LOG = LoggerFactory.getLogger(LoginUserContext.class);

    private static ThreadLocal<UserLoginResp> userThreadLocal = new ThreadLocal<>();

    public static UserLoginResp getUser() {
        return userThreadLocal.get();
    }

    public static void setUser(UserLoginResp user) {
        LoginUserContext.userThreadLocal.set(user);
    }

    public static Long getId(){
        try {
            return userThreadLocal.get().getId();
        } catch (Exception e) {
            LOG.error("获取登录用户信息异常",e);
            throw e;
        }
    }
}

- 用户拦截器

/**
 * @Author: ciky
 * @Description: 用户拦截器
 * @DateTime: 2024/9/19 21:32
 **/
@Component
public class UserInterceptor implements HandlerInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(UserInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取header的tokne参数
        String token = request.getHeader("token");
        if(StrUtil.isNotBlank(token)){
            LOG.info("获取用户登录token:{}",token);
            //解析token,转成JSON
            JSONObject loginUser = JwtUtil.getJSONObject(token);
            LOG.info("当前登录用户:{}",loginUser);
            //将json转成UserLoginResp用户登录信息
            UserLoginResp user = JSONUtil.toBean(loginUser, UserLoginResp.class);
            //存到线程本地变量
            LoginUserContext.setUser(user);
        }
        return true;
    }
}


//============== JwtUtil的getJSONObject()方法===================;
/**
 * 解析token
 */
public static JSONObject getJSONObject(String token){
    GlobalBouncyCastleProvider.setUseBouncyCastle(false);
    JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
    JSONObject payloads = jwt.getPayloads();

    payloads.remove(JWTPayload.ISSUED_AT);
    payloads.remove(JWTPayload.EXPIRES_AT);
    payloads.remove(JWTPayload.NOT_BEFORE);
    LOG.info("根据token获取原始内容:{}",payloads);

    return payloads;
}

- 配置拦截器生效

/**
 * @Author: ciky
 * @Description: webMVC配置类
 * @DateTime: 2024/9/19 21:43
 **/
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;

    /**
     * 添加拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
            			"/user/hello"
                        "/user/user/send-code",
                        "/user/user/login"
                );
    }
}

- 获取用户信息

//从本地线程获取userId
LoginUserContext.getUser();

4 网关认证


4.1 网关的职责


  1. 认证(网关不负责授权,只负责认证)
  2. 路由转发
  3. 白名单维护

image20241104195840209.png


4.2 实现网关认证


(1) 添加依赖

<!--springSecurity-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<!--Oauth2-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

(2) 配置类

- 令牌配置类(TokenConfig)

@Configuration
public class TokenConfig {

    String SIGNING_KEY = "mq123";
    
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

- 安全拦截配置类(SecurityConfig)

@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {

    //安全拦截配置
    @Bean
    public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {

    return http.authorizeExchange()
           .pathMatchers("/**").permitAll()	//允许所有路径的请求无需认证即可访问
           .anyExchange().authenticated()	//所有其他未匹配的请求都需要经过身份验证
           .and().csrf().disable().build();	//禁用 CSRF(跨站请求伪造)保护
    }
}

- 网关认证过滤器(GatewayAuthFilter)

@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {

    //白名单
    private static List<String> whitelist = null;

    //初始化白名单
    static {
        //加载白名单
        try (
                InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
        ) {
            Properties properties = new Properties();
            properties.load(resourceAsStream);
            Set<String> strings = properties.stringPropertyNames();
            whitelist= new ArrayList<>(strings);


        } catch (Exception e) {
            log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
            e.printStackTrace();
        }
    }

    @Autowired
    private TokenStore tokenStore;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("进入过滤器了");
        //请求的URL
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        //白名单放行
        for (String url : whitelist) {
            if (pathMatcher.match(url, requestUrl)) {
                return chain.filter(exchange);
            }
        }

        //检查token是否存在
        String token = getToken(exchange);
        if (StringUtils.isBlank(token)) {
            return buildReturnMono("没有认证",exchange);
        }
        //判断是否是有效的token
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenStore.readAccessToken(token);

            boolean expired = oAuth2AccessToken.isExpired();
            if (expired) {
                return buildReturnMono("认证令牌已过期",exchange);
            }
            return chain.filter(exchange);
        } catch (InvalidTokenException e) {
            log.info("认证令牌无效: {}", token);
            return buildReturnMono("认证令牌无效",exchange);
        }

    }

    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return token;
    }

    private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        String jsonString = JSON.toJSONString(new RestErrorResponse(error));
        byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }

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

- 错误响应参数包装(RestErrorResponse)

public class RestErrorResponse implements Serializable {

    private String errMessage;

    public RestErrorResponse(String errMessage){
        this.errMessage= errMessage;
    }

    public String getErrMessage() {
        return errMessage;
    }

    public void setErrMessage(String errMessage) {
        this.errMessage = errMessage;
    }
}

(3) 白名单

security-whitelist.properties

/**=临时全部放行
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口
/checkcode/**=验证码服务接口

(4) 修改资源服务

将资源服务的ResouceServerConfig中认证请求相关的注释掉

@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
      .authorizeRequests()
      //.antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
      .anyRequest().permitAll();
}