当前位置:主页 > 查看内容

微服务认证鉴权gateway+oauth2+security+jwt

发布时间:2021-05-21 00:00| 位朋友查看

简介:文章目录 本文认证鉴权思路方案 一. 认证服务器 1. 需要依赖 2. 编写认证服务 3. 安全配置 4. 开放接口配置 二. 资源服务器(此处可理解为鉴权服务) 1. 需要依赖 2. 编写鉴权管理器 3. 编写资源服务 3. 黑名单过滤器 4. 异常处理 5. JWT刷新方案 6. 配置网关……

本文认证鉴权思路方案

在这里插入图片描述
实现思路受到开源电商项目mallyoulai-mall启发,此处贴上他们的开源地址

mall: https://gitee.com/macrozheng/mall
youlai-mall: https://gitee.com/youlaitech/youlai-mall

该篇内容主要为了提升自己对oauth2技术的理解、记录走过的坑的一些解决方案以及对网上零散实现方式的整合

用户登录,网关远程调用认证授权服务完成登录,办法token,使用jwt,本文仅为password认证模式,其他模式请了解Oauth的授权模式后自行百度,大同小异

当网关收到客户端请求的时候,验证用户Token是否正确,正确则校验用户是否具备当前请求路径的权限

内部服务之间裸奔,不校验权限

一. 认证服务器

1. 需要依赖

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

2. 编写认证服务

package top.sclf.auth.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import top.sclf.auth.domain.CustomUserDetails;
import top.sclf.auth.exception.CustomWebResponseExceptionTranslator;
import top.sclf.common.core.constant.AuthConstants;

import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhangxing
 * @date 2021/4/4
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;

    public AuthorizationServerConfig(
            AuthenticationManager authenticationManager,
            @Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService
    ) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    	// 此处客户端可以写死到内存中,也可以从数据库读取,视具体业务而变,客户端作用此处不在讲述
        clients.inMemory()
                // 客户端id
                .withClient("client")
                // 客户端密码
                .secret("123456")
                // 自动授权配置
                .autoApprove(true)
                .scopes("all")
                // 客户端授权类型(authorization_code:授权码类型 password:密码类型 implicit:简化类型/隐式类型 client_credentials:客户端类型 refresh_token:该为特例,加了才可以刷新授权)
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600 * 24)
                .refreshTokenValiditySeconds(3600 * 24 * 7);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        // 增强jwt载荷的内容
        tokenEnhancers.add(tokenEnhancer());
        // 添加jwt的加密公钥
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                // 使用自己实现的用户密码校验逻辑
                .userDetailsService(userDetailsService)
                // refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
                // 1.重复使用:access_token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
                // 2.非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新而无需失效再次登录
                .reuseRefreshTokens(false);
    }

    /**
     * 允许表单认证
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 支持表单登录
        security.allowFormAuthenticationForClients();
    }

    /**
     * jwt生成使用RS256非对称加密,非对称加密需要私钥和公钥,此处设置公钥
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 从classpath下的密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
                new ClassPathResource("jcps.jks"), "123456".toCharArray());
        return factory.getKeyPair(
                "jcps", "123456".toCharArray());
    }


    /**
     * JWT内容增强
     * 在jwt的载荷中加入自定义的一些内容
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            Map<String, Object> map = new HashMap<>(1);
            CustomUserDetails user = (CustomUserDetails) authentication.getUserAuthentication().getPrincipal();
            map.put(AuthConstants.DETAILS_USER_ID, user.getId());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }
}

PS:jks加密可百度搜索如何生成

以上为认证服务的核心配置
配置好后会自动生成一下几个请求端点

  • /oauth/authorize method=[POST] 授权码类型和隐式类型的授权端点
  • /oauth/token method=[GET,POST] 获取令牌的端点,password模式或有授权code情况下用于获取token
  • /oauth/check_token 请求方式没有测试过,用于检查令牌有效性

3. 安全配置

由于使用Spring Security,故需要开放以上的端点提供给Gateway调用,所以需要添加WebSecurity相关配置

package top.sclf.auth.config;

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * SpringSecurity配置
 *
 * @author zhangxing
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                
                // 使用jwt,则无需使用原本的session管理
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                // actuator中的所有健康检查端点都放行,经测试,此处包含了上述几处oauth端点,直接放行
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                // 此处不加项目的context-path
                .antMatchers("/oauth/logout").permitAll()
                .antMatchers("/getPublicKey").permitAll()
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    	// 上面的认证服务核心配置需要使用AuthenticationManager来配置UserDetailsService
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    	// 测试方便使用密码不加密模式
        return NoOpPasswordEncoder.getInstance();
        // return new BCryptPasswordEncoder();
    }

}

4. 开放接口配置

关于/getPublicKey端点说明:因为jwt使用RS256非对称加密,非对称加密使用相同的公钥和不同的密钥加密而成,所以在认证服务模块中开放公钥的获取方式

添加对外暴露的退出登录接口和开放公钥接口

package top.sclf.auth.controller;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * RSA公钥开放接口
 *
 * @author zhangxing
 * @date 2021/4/5
 */
@RestController
public class PublicKeyController {

    private final KeyPair keyPair;

    public PublicKeyController(KeyPair keyPair) {
        this.keyPair = keyPair;
    }

    @GetMapping("/getPublicKey")
    public Map<String, Object> getPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }

}

关于/oauth/logout退出登录的端点的说明
因为JWT本身就是字包含的加密文本,所以不需要在服务端存储Token的过期时间,JWT本身就可以验证自己是否正确,以及什么时候过期,所以意味着Token一旦颁发,从Token本身来说必须等Token本身过期才会失效,为了防止用户退出登录后,Token依旧有效,我们可以在用户退出或者修改密码后将Token加入到Redis中,并设置过期时间,可以理解为将Token加入黑名单,再在gateway上添加过滤器识别token是否在黑名单中即可实现用户的退出改密作废Token的功能

package top.sclf.auth.controller;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import top.sclf.common.core.constant.AuthConstants;
import top.sclf.common.core.http.ResultEntity;

import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 自定义Oauth2获取令牌接口
 *
 * @author zhangxing
 */
@RestController
@RequestMapping("/oauth")
public class AuthController {

    @Autowired
    private TokenEndpoint tokenEndpoint;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostMapping("/token")
    public ResultEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        return ResultEntity.ok(oAuth2AccessToken);
    }

    @DeleteMapping("/logout")
    public ResultEntity<?> logout(HttpServletRequest request) {
        String payload = request.getHeader(AuthConstants.USER_TOKEN_HEADER);
        JSONObject jsonObject = JSONUtil.parseObj(payload);

        // JWT唯一标识
        String jti = jsonObject.getStr("jti");
        // JWT过期时间戳(单位:秒)
        long exp = jsonObject.getLong("exp");

        long currentTimeSeconds = System.currentTimeMillis() / 1000;

        // token已过期
        if (exp < currentTimeSeconds) {
            return ResultEntity.fail("登录凭证超时");
        }
        redisTemplate.opsForValue().set(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti, null, (exp - currentTimeSeconds), TimeUnit.SECONDS);
        return ResultEntity.ok();
    }
}

为什么这里又重写了获取/oauth/token端点呢,是为了通过@RestControllerAdvice来捕捉异常

添加异常捕获

/**
 * @author zhangxing
 */
@ControllerAdvice
public class Oauth2ExceptionHandler {
    @ResponseBody
    @ExceptionHandler(value = OAuth2Exception.class)
    public ResultEntity<?> handleOauth2(OAuth2Exception e) {
        return ResultEntity.fail(e.getMessage());
    }
}

添加认证中查询用户的实现

package top.sclf.auth.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import top.sclf.api.resource.RemoteResUserService;
import top.sclf.api.resource.domain.model.LoginUser;
import top.sclf.auth.constant.MessageConstant;
import top.sclf.auth.domain.CustomUserDetails;
import top.sclf.common.core.enums.DelFlagEnum;
import top.sclf.common.core.http.ResultEntity;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * @author zhangxing
 * @date 2021/4/4
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	// 远程调用用户服务
    @Autowired
    private RemoteResUserService remoteResUserService;

    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
        ResultEntity<LoginUser> loginUserRes = remoteResUserService.getByLoginName(loginName);
        LoginUser.User user = Optional.ofNullable(loginUserRes)
                .map(ResultEntity::getData)
                .map(LoginUser::getResUser)
                .orElse(null);
        if (user == null) {
            throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
        }
        CustomUserDetails userDetails = new CustomUserDetails();
        userDetails.setId(user.getId());
        userDetails.setLoginName(user.getLoginName());
        userDetails.setUserName(user.getUserName());
        userDetails.setPassword(user.getLoginPwd());
        userDetails.setEnable(Objects.equals(user.getDelFlag(), DelFlagEnum.DEFAULT.getVal()));

		// 此处查询系统中用户的角色权限等信息
        List<CustomUserDetails.Perm> perms = new ArrayList<CustomUserDetails.Perm>(){{
            add(new CustomUserDetails.Perm("/a"));
            add(new CustomUserDetails.Perm("/b"));
        }};

        userDetails.setPermList(perms);

        return userDetails;
    }
}
package top.sclf.auth.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * @author zhangxing
 * @date 2021/4/4
 */
@Data
public class CustomUserDetails implements UserDetails, Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String loginName;

    private String userName;

    @JsonIgnore
    private String password;

    private boolean enable;

    private List<Perm> permList;

    @Data
    @AllArgsConstructor
    public static class Perm implements GrantedAuthority {

        private String uri;

        @Override
        public String getAuthority() {
            return uri;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return permList;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.loginName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enable;
    }
}

二. 资源服务器(此处可理解为鉴权服务)

1. 需要依赖

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

2. 编写鉴权管理器

package top.sclf.gateway.config;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
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.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import top.sclf.common.core.constant.AuthConstants;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * 鉴权管理器
 *
 * @author zhangxing
 */
@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final RedisTemplate redisTemplate;

    public AuthorizationManager(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String path = request.getURI().getPath();
        PathMatcher pathMatcher = new AntPathMatcher();

        // 1. 对应跨域的预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }

        // 2. token为空拒绝访问
        String token = request.getHeaders().getFirst(AuthConstants.HEADER);
        if (StrUtil.isBlank(token)) {
            return Mono.just(new AuthorizationDecision(false));
        }

        // 3.缓存取资源权限角色关系列表
        // Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstants.RESOURCE_ROLES_KEY);
        Map<Object, Object> resourceRolesMap = new HashMap<>(0);
        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();

        // 4.请求路径匹配到的资源需要的角色权限集合authorities
        List<String> authorities = new ArrayList<>();
        while (iterator.hasNext()) {
            String pattern = (String) iterator.next();
            if (pathMatcher.match(pattern, path)) {
                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
            }
        }
        return mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(roleId -> {
                    // 5. roleId是请求用户的角色(格式:ROLE_{roleId}),authorities是请求资源所需要角色的集合
                    log.info("访问路径:{}", path);
                    log.info("用户角色roleId:{}", roleId);
                    log.info("资源需要权限authorities:{}", authorities);
                    // return authorities.contains(roleId);
                    return true;
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
}

以上的鉴权逻辑参考的mall项目,可以根据自己的业务修改

3. 编写资源服务

资源服务需要申明哪些资源需要被保护起来,哪些资源放行,以及被保护起来的资源的保护逻辑(鉴权管理器)

package top.sclf.gateway.config;


import cn.hutool.core.util.ArrayUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import top.sclf.gateway.filter.BlackListFilter;
import top.sclf.gateway.handler.CustomServerAccessDeniedHandler;
import top.sclf.gateway.handler.CustomServerAuthenticationEntryPoint;
import top.sclf.gateway.properties.IgnoreWhiteProperties;

/**
 * 资源服务器配置
 *
 * @author zhangxing
 */
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    private final AuthorizationManager authorizationManager;
    private final CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
    private final CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
    private final IgnoreWhiteProperties ignoreWhiteProperties;
    private final BlackListFilter blackListFilter;

    public ResourceServerConfig(AuthorizationManager authorizationManager, CustomServerAccessDeniedHandler customServerAccessDeniedHandler, CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint, IgnoreWhiteProperties ignoreWhiteProperties, BlackListFilter blackListFilter) {
        this.authorizationManager = authorizationManager;
        this.customServerAccessDeniedHandler = customServerAccessDeniedHandler;
        this.customServerAuthenticationEntryPoint = customServerAuthenticationEntryPoint;
        this.ignoreWhiteProperties = ignoreWhiteProperties;
        this.blackListFilter = blackListFilter;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        // 自定义处理JWT请求头过期或签名错误的结果
        http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);

		// 在鉴权之前,添加一个黑名单过滤器,即认证服务中说到的用户退出或修改密码后,应该将Token加入黑名单,如果Token已经在黑名单中了,则由该Token发起的请求也无需再做鉴权判断了,所以黑名单过滤器必须在鉴权之前,所以放在了认证过滤器的前面
        http.addFilterBefore(blackListFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        http.authorizeExchange()
        		// 在网关上放行的请求,该白名单为List<String>,可自行配置,必须包含如下两个请求
        		// /oauth/login,/getPublicKey
                .pathMatchers(ArrayUtil.toArray(ignoreWhiteProperties.getWhites(), String.class)).permitAll()
                // 认证通过后即可发起退出登录请求
                .pathMatchers("/auth/oauth/logout").authenticated()
                // 鉴权管理器,剩下的请求通过鉴权管理器判定
                .anyExchange().access(authorizationManager)
                .and()
                // 添加异常处理的响应
                .exceptionHandling()
                // 处理未授权
                .accessDeniedHandler(customServerAccessDeniedHandler)
                // 处理未认证
                .authenticationEntryPoint(customServerAuthenticationEntryPoint)
                // csrf自行百度
                .and().csrf().disable();

        return http.build();
    }


    /**
     * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
     * 需要把jwt的Claim中的authorities加入
     * 方案:重新定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter
     */
    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 将认证服务中返回的用户对象信息保存在JWT中,即自主实现的UserDetailsService返回的对象权限加载到JWT中
        // UserDetailsService返回对象中的权限添加到jwt中,并加上前缀
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        // 通过authorities字段从jwt中获取UserDetailsService返回的对象中的权限
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }

}

3. 黑名单过滤器

package top.sclf.gateway.filter;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import top.sclf.common.core.constant.AuthConstants;
import top.sclf.common.core.exception.CustomException;
import top.sclf.common.core.http.ResultEntity;
import top.sclf.common.core.http.ResultEnum;

import java.nio.charset.StandardCharsets;
import java.text.ParseException;

/**
 * @author zhangxing
 * @date 2021/4/7
 */
@Component
public class BlackListFilter implements WebFilter {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst(AuthConstants.HEADER);
        if (StrUtil.isEmpty(token)) {
            return chain.filter(exchange);
        }
        String realToken = token.replace("Bearer ", "");
        JWSObject jwsObject;
        try {
            // 从token中解析用户信息并设置到Header中去
            jwsObject = JWSObject.parse(realToken);
        } catch (ParseException e) {
            throw new CustomException(ResultEnum.SERVER_ERROR, "token解析错误");
        }
        String payloadStr = jwsObject.getPayload().toString();

        JSONObject payload = JSONUtil.parseObj(payloadStr);

        // 校验该token是否存在于黑名单中(登出、修改密码)
        // JWT唯一标识
        String jti = payload.getStr("jti");
        Boolean isBlack = redisTemplate.hasKey(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti);
        if (Boolean.TRUE.equals(isBlack)) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.OK);
            response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            response.getHeaders().set("Access-Control-Allow-Origin", "*");
            response.getHeaders().set("Cache-Control", "no-cache");
            String body = JSONUtil.toJsonStr(ResultEntity.fail(ResultEnum.TOKEN_EXPIRED, ResultEnum.TOKEN_EXPIRED.getMessage()));
            DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(buffer));
        }

        return chain.filter(exchange);
    }
}

4. 异常处理

package top.sclf.gateway.handler;

import cn.hutool.json.JSONUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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;
import top.sclf.common.core.http.ResultEntity;
import top.sclf.common.core.http.ResultEnum;

import java.nio.charset.StandardCharsets;

/**
 * 无权访问自定义响应
 */
@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        response.getHeaders().set("Cache-Control", "no-cache");
        String body = JSONUtil.toJsonStr(ResultEntity.fail(ResultEnum.NOT_PERMISSION.getCode(), ResultEnum.NOT_PERMISSION.getMessage()));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
package top.sclf.gateway.handler;

import cn.hutool.json.JSONUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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;
import top.sclf.common.core.http.ResultEntity;
import top.sclf.common.core.http.ResultEnum;
import top.sclf.common.core.util.StringUtils;

import java.nio.charset.StandardCharsets;

/**
 * 无效token/token过期 自定义响应
 *
 * @author zhangxing
 */
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {

        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        response.getHeaders().set("Cache-Control", "no-cache");
        ResultEntity<String> fail = ResultEntity.fail(ResultEnum.TOKEN_INVALID.getCode().intValue(), "未登陆或登录已过期");

		// 文章下面会详细描述为什么此处需要判断是否是jwt过期的情况
        String message = e.getMessage();
        if (message != null && StringUtils.containsIgnoreCase(message, "Jwt expired")) {
            fail.setCode(ResultEnum.TOKEN_EXPIRED.getCode());
        }

        String body = JSONUtil.toJsonStr(fail);
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }

}

5. JWT刷新方案

权限校验失败的自定义响应中,我们判断了是否是应为jwt过期导致的鉴权失败
当请求进来时,如果jwt过期,我们定制一个和前端约定好的错误编码来表示jwt过期,当前端请求访问失败,并且发现响应编码是因为jwt过期导致的请求失败,则前端使用refresh_token使用刷新token的请求方式来重新获取一次新的授权JWT,返回新JWT后重新设置到请求头上再次发起刚才鉴权失败的请求即可

6. 配置网关模块调用认证模块获取jwt加密公钥地址

在网关模块的配置文件中加入

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
		  # 网上找了一圈没有找到此处配置负载均衡的方式,似乎不支持
		  # 所以此处配置的获取公钥的地址直接访问的网关的地址,曲线救国的方式实现负载均衡
          jwk-set-uri: http://localhost:8088/auth/getPublicKey # /auth是我的认证模块的context-path

三. 配置完毕,开始测试

1. 获取Token

登录获取Token端点: POST /oauth/token,/auth是我的认证模块context-path
在这里插入图片描述
返回值:

  • access_token用于访问资源
  • refresh_token用于刷新token,以获取新的access_token

2. 刷新Token

刷新Token端点: POST /oauth/token,/auth是我的认证模块context-path
在这里插入图片描述

3. 携带Token访问资源

在这里插入图片描述

4. 退出登录

在这里插入图片描述

5. 退出登录后再次访问资源

在这里插入图片描述

;原文链接:https://blog.csdn.net/qq1010830256/article/details/115496503
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐