在SpringBoot Gateway 中實現(xiàn)RedisToken - > JwtToken的token交換

背景

  1. jwt token的載荷是明文(base64)件相,雖然只是用來傳遞一些非敏感信息,但依舊會讓人感覺有些不適
  2. jwt token無法主動失效
  3. 微服務(wù)之間盡量減少耦合度

解決思路

  1. 由認(rèn)證服務(wù)(iam)產(chǎn)生RedisToken氧苍,該token保存在iam服務(wù)的redis中夜矗,可以主動失效,也可以設(shè)置失效時間
  2. 采用jwt作為微服務(wù)間驗證的依據(jù)让虐,如果使用RedisToken紊撕,則所有微服務(wù)均需要依賴同一個redis數(shù)據(jù)庫(或集群)

實現(xiàn)方式

  1. 用戶登陸,由 認(rèn)證服務(wù) 產(chǎn)生RedisToken交給用戶
  2. 用戶使用RedisToken通過 網(wǎng)關(guān)服務(wù) 訪問 業(yè)務(wù)服務(wù)赡突,在 網(wǎng)關(guān)服務(wù) 中使用RedisToken交換JwtToken(可以包含一些非敏感的當(dāng)前用戶的信息)对扶,再使用JwtToken訪問業(yè)務(wù)服務(wù)
  3. 業(yè)務(wù)服務(wù)只對JwtToken進(jìn)行驗證,并且可以從jwt payload中解析出當(dāng)前用戶的信息

代碼實現(xiàn)

注冊中心

eureka惭缰、nacos等辩稽,略

認(rèn)證服務(wù)

使用SpringSecurity實現(xiàn)用戶登陸

參考文檔:https://www.cnblogs.com/cbvlog/p/15624215.html

  1. 依賴注入

其他如數(shù)據(jù)庫,redis从媚,服務(wù)發(fā)現(xiàn)逞泄,服務(wù)調(diào)用等依賴就不寫了

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  1. 設(shè)置用戶密碼加密方式
/**
 * 密碼加密方式
 *
 * @author Jenson
 */
@Component
@Slf4j
public class CustomBCryptPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        // 簡單加密,生成一個salt
        String salt = BCrypt.gensalt();
        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword != null && encodedPassword != null && !encodedPassword.isEmpty()) {
            return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
        } else {
            log.warn("Empty encoded password");
            return false;
        }
    }
}

  1. 登錄認(rèn)證過濾器,設(shè)置登錄地址喷众、調(diào)用方式各谚,繼承 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
/**
 * 登錄認(rèn)證過濾器
 *
 * @author Jenson
 */
public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 設(shè)置登錄地址、調(diào)用方式
     */
    public AuthenticationLoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 讀取表單提取數(shù)據(jù)
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        // 封裝到token中提交
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        return getAuthenticationManager().authenticate(authRequest);
    }
}

  1. 實現(xiàn) org.springframework.security.core.userdetails.UserDetailsService

驗證登陸接口中用戶傳入的賬號密碼到千,生成org.springframework.security.core.userdetails.UserDetails

/**
 * @author Jenson
 */
@Slf4j
@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 使用用戶名查詢數(shù)據(jù)庫(或是緩存中的)中的用戶持久層對象 UserPO(名字隨便了)
        UserPO userPo = this.searchUserPoFromDb(username);
        if(userPo == null){
            // 用戶不存在昌渤,登錄失敗
            return null;
        }
        // 構(gòu)建 org.springframework.security.core.userdetails.User 對象,當(dāng)然最好是繼承它憔四,可以添加一些自己的屬性上去膀息,比如生日年齡性別啥的
        User  user =new User(userPo.getUsername,
                             userPo.getPassword, 
                             AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
        return user ;
    }
}

  1. 實現(xiàn) 認(rèn)證成功處理器,org.springframework.security.web.authentication.AuthenticationSuccessHandler

認(rèn)證成功后了赵,生成token潜支,輸出token

/**
 * 認(rèn)證成功處理器
 *
 * @author Jenson
 */
@Slf4j
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    // 這是自己寫的redis-token工具對象
    @Autowired
    private RedisTokenUtils redisTokenUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        User user = (User) authentication.getPrincipal();
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 根據(jù)獲取的用戶信息生成token,并將token保存在redis中柿汛,設(shè)置失效時間冗酿,
        // 考慮到登陸時,系統(tǒng)中可能有未失效的token络断,為了避免多端登陸互相踢出裁替,可以先嘗試獲取用戶的token,
        // 如果存在則刷新緩存時間(token續(xù)期)貌笨,更新token綁定用戶緩存信息弱判,返回老token;如果不存在則沈城新token
        UserTokenRel userTokenRel = redisTokenUtils.refreshToken(user);
        // 書寫響應(yīng)體
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.OK.value());
        // 在響應(yīng)體中寫出token
        byte[] body = JSON.toJSONString(userTokenRel.getLoginToken()).getBytes(StandardCharsets.UTF_8);
        OutputStream outputStream = response.getOutputStream();
        try {
            outputStream.write(body);
        } finally {
            outputStream.flush();
            outputStream.close();
        }

    }
}
  1. 實現(xiàn) 認(rèn)證失敗處理器锥惋,org.springframework.security.web.authentication.AuthenticationFailureHandler
/**
 * 認(rèn)證失敗處理
 *
 * @author Jenson
 */
@Slf4j
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        String exceptionMsg = "認(rèn)證失敗原因";
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        OutputStream outputStream = response.getOutputStream();
        // 自定義的響應(yīng)體
        CuxResponseEntity<ExceptionResponse> responseEntity =
                new CuxResponseEntity<>(new ExceptionResponse("AUTHENTICATION_FAILURE", exceptionMsg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
        byte[] body = JSON.toJSONString(responseEntity.getBody()).getBytes(StandardCharsets.UTF_8);
        try {
            outputStream.write(body);
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

  1. 登錄過濾器的配置

需要使用到上述創(chuàng)建的三個實例

/**
 * 登錄過濾器的配置
 *
 * @author Jenson
 */
@Configuration
public class AuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    /**
     * userDetailService
     */
    @Qualifier("customUserDetailsServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 登錄成功處理器
     */
    @Autowired
    private AuthenticationSuccessHandler loginAuthenticationSuccessHandler;

    /**
     * 登錄失敗處理器
     */
    @Autowired
    private AuthenticationFailureHandler loginAuthenticationFailureHandler;

    /**
     * 加密
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 將登錄接口的過濾器配置到過濾器鏈中
     * 1. 配置登錄成功裕循、失敗處理器
     * 2. 配置自定義的userDetailService(從數(shù)據(jù)庫中獲取用戶數(shù)據(jù))
     * 3. 將自定義的過濾器配置到spring security的過濾器鏈中,配置在UsernamePasswordAuthenticationFilter之前
     *
     * @param http HttpSecurity
     */
    @Override
    public void configure(HttpSecurity http) {
        AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //認(rèn)證成功處理器
        filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
        //認(rèn)證失敗處理器
        filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
        //直接使用DaoAuthenticationProvider
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //設(shè)置userDetailService
        provider.setUserDetailsService(userDetailsService);
        //設(shè)置加密算法
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);
        //將這個過濾器添加到UsernamePasswordAuthenticationFilter之前執(zhí)行
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

  1. Token校驗過濾器

此處的token净刮,是jwt-token剥哑,在上述“認(rèn)證成功過濾器”中,發(fā)放的是redis-token淹父,但是使用redis-token調(diào)用網(wǎng)關(guān)接口時株婴,網(wǎng)關(guān)會使用redis-token交換jwt-token,所以該認(rèn)證服務(wù)的filter需要認(rèn)證的是jwt-token

/**
 * Token校驗過濾器
 *
 * @author Jenson
 */
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        HttpServletRequest httpServletRequest;
        httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        log.info("---------> Authorization : {}", authorization);
        if (StringUtils.hasText(authorization)) {

            String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
            String token = tokenDetail[1];
            if (StringUtils.hasText(token)) {
                User user = null;
                try {
                    // JwtUtils 是自定義的jwt-token解析工具
                    // 解析jwt-token暑认,創(chuàng)建用戶對象
                    // HttpServletResponseUtils 是自定義的異常打印工具類
                    User = new User(JwtUtils.verify(token));
                } catch (UnauthorizedException e) {
                    HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
                    return;
                } catch (ForbiddenException e) {
                    HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
                    return;
                } catch (Exception e) {
                    HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
                    return;
                }
                // 賬號不為空且還沒有認(rèn)證過
                if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // 認(rèn)證成功困介,設(shè)置當(dāng)前用戶對象
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

該過濾器沒有像參考文檔一樣繼承OncePerRequestFilter,以為我實際使用中遇到了非自定義異常時蘸际,又會過一遍filter才拋出(不知道是不是哪配置錯了)座哩,而這次過filter時filter調(diào)用鏈中沒有了認(rèn)證的filter,就會拋出認(rèn)證失敗的異常粮彤,將原異常覆蓋根穷。

  1. 接口未認(rèn)證異常
/**
 * 用戶未通過認(rèn)證訪問受保護(hù)的資源 401
 *
 * @author Jenson
 */
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        authException.printStackTrace();
        // 自定義異常打印工具
        HttpServletResponseUtils.outPrintUnauthorizedException(response);
    }
}

  1. 接口認(rèn)證無權(quán)限異常

/**
 * 認(rèn)證成功的用戶訪問受保護(hù)的資源姜骡,但是權(quán)限不夠 403
 *
 * @author Jenson
 */
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        // 自定義異常打印工具
        HttpServletResponseUtils.outPrintForbiddenException(response);
    }
}
  1. 接口授權(quán)配置

/**
 * @author Jenson
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private AuthenticationSecurityConfig authenticationSecurityConfig;
    @Qualifier("entryPointUauthenticationHandler")
    @Autowired
    private AuthenticationEntryPoint entryPointUauthenticationHandler;
    @Qualifier("requestAccessDeniedHandler")
    @Autowired
    private AccessDeniedHandler requestAccessDeniedHandler;

    /**
     * 授權(quán)配置,最高優(yōu)先級
     *
     * @param http HttpSecurity
     * @return SecurityFilterChain
     * @throws Exception
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        http
                // 禁用表單登錄
                .formLogin().disable()
                // 應(yīng)用登錄過濾器的配置屿良,配置分離
                .apply(authenticationSecurityConfig)
                .and()
                // 設(shè)置URL的授權(quán)
                .authorizeHttpRequests()
                // 這里需要將登錄頁面放行,permitAll()表示不再攔截圈澈,/login 登錄的url,/refreshToken刷新token的url
                .requestMatchers(
                        // 登陸接口
                        "/login",
                        // token交換接口(redis-token -> jwt-token)
                        "/token/generate/jwt/{redisToken}"
                )
                .permitAll()
                .anyRequest()
                .authenticated()
                //處理異常情況:認(rèn)證失敗和權(quán)限不足
                .and()
                .exceptionHandling()
                //認(rèn)證未通過尘惧,不允許訪問異常處理器
                .authenticationEntryPoint(entryPointUauthenticationHandler)
                //認(rèn)證通過康栈,但是沒權(quán)限處理器
                .accessDeniedHandler(requestAccessDeniedHandler)
                .and()
                //禁用session,JWT校驗不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //將TOKEN校驗過濾器配置到過濾器鏈中喷橙,否則不生效啥么,放到UsernamePasswordAuthenticationFilter之前
                .addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 解決跨域問題(其實沒有解決)
                .cors()
                .and()
                // 關(guān)閉csrf
                .csrf().disable();

        return http.build();
    }

}

將登陸接口(/login)和 token交換接口(/token/generate/jwt/{redisToken}) 設(shè)置為免登錄。
/token/generate/jwt/{redisToken}其實是需要認(rèn)證的贰逾,但是filter配置為了認(rèn)證jwt-token悬荣,當(dāng)然也可以在filter中增加一種認(rèn)證方式,但我這里選擇在容器內(nèi)認(rèn)證似踱,所以該接口設(shè)置為免登錄。

  1. token 交換接口 /token/generate/jwt/{redisToken}
 /**
     * 生成JWT
     *
     * @param redisToken redisToken
     * @return jwt
     */
    @GetMapping("/generate/jwt/{redisToken}")
    public WmResponseEntity<String> generateJwt(@PathVariable String redisToken) {

        log.info("-----> generateJwt redisToken : {}", redisToken);
        // 根據(jù) redisToken 獲取用戶信息 ,redisTokenUtils 為自定義用戶redis-token管理工具
        UserTokenRel userTokenRel = redisTokenUtils.getUserTokenRel(redisToken);
        if (userTokenRel == null) {
            // CommonException 為自定義 RuntimeException
            throw new CommonException("NOT_LOGGED_IN", "用戶未登錄");
        }
        User user = userService.searchUserByUserId(userTokenRel.getUserId());
        // 生成 jwt-token稽煤,jwt的payload中可以盡可能裝入除用戶密碼外的用戶信息核芽,
        // 在業(yè)務(wù)服務(wù)中進(jìn)行解析,就不需要跨服務(wù)獲取調(diào)用者信息了酵熙,實現(xiàn)了服務(wù)解耦
        String jwt = tokenService.generateJwt(user);
        // 異步刷新一下token轧简,避免使用中失效
        Long userId = user.getUserId();
        // 異步刷新token,token自動續(xù)期匾二,防止用戶用著用著突然掉線
        CompletableFuture.runAsync(() -> redisTokenUtils.refreshToken(userId), commonExecutor);
        return Results.success(jwt);
    }

網(wǎng)關(guān)

  1. 依賴注入

其他工具依賴就省略了

         <!-- gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>4.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

  1. gateway 路由配置
    無論采用靜態(tài)路由配置還是動態(tài)路由配置哮独,不在此處細(xì)說
    配置認(rèn)證服務(wù)的路由前綴為 /iam,則登陸接口地址就是 /iam/login

spring-gateway+nacos 實現(xiàn)動態(tài)路由配置可以參考以下文章:
https://blog.csdn.net/qq_38374397/article/details/125874882

  1. 實現(xiàn) GlobalFilter 察藐,攔截請求進(jìn)行客戶化處理
    自定義filter皮璧,在網(wǎng)關(guān)路由前調(diào)用認(rèn)證服務(wù)接口交換token(只能異步調(diào)用),使用jwt調(diào)用服務(wù)
/**
 * 網(wǎng)關(guān)請求攔截客戶化處理
 *
 * @author Jenson
 * @version 1.0
 */
@Slf4j
@Component
public class CuxGlobalFilter implements GlobalFilter, Ordered {

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String path = request.getURI().getPath();
        log.info("-----------> path : {}", path);
        HttpHeaders headers = request.getHeaders();
        String authorization = headers.getFirst("Authorization");
        String tenant = headers.getFirst("Tenant");
        if (!"/iam/login".equals(path) && StringUtils.hasText(authorization)) {
            // 非 認(rèn)證 服務(wù)分飞,換用 jwt token悴务,此舉是為了在認(rèn)證層面解耦服務(wù)
            headers = HttpHeaders.writableHttpHeaders(headers);
            String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
            String token = tokenDetail[1];
            if (!StringUtils.hasText(token)) {
                return outUnauthorizedResponse(response);
            }

            // ------ 獲取Token start -----
            // 在此處,異步 feign調(diào)用 交換 token 接口譬猫,獲得新jwt-token
            CompletableFuture<ResponseEntity<String>> newJwtFuture = CompletableFuture.supplyAsync(() -> tokenService.generateJwt(token));
            String jwt;
            try {
                ResponseEntity<String> responseEntity = newJwtFuture.get(1, TimeUnit.SECONDS);
                // FeignRspEntityParseUtils 自定義接口響應(yīng)結(jié)果解析工具
                jwt = FeignRspEntityParseUtils.parse(responseEntity, String.class);
            } catch (CommonException e) {
                if ("NOT_LOGGED_IN".equals(e.getCode())) {
                    return outUnauthorizedResponse(response);
                }
                return outCommonExceptionResponse(response, e.getCode(), e.getMsg());
            } catch (Exception e) {
                log.info("----> 換取 jwt 失敗 , {}", e);
                return outCommonExceptionResponse(response, "SWITCH_JWT_ERROR", "換取 jwt 失敗 ,請聯(lián)系管理員檢查認(rèn)證服務(wù)");
            }
            log.info("-----> jwt : {}", jwt);
            // ---- 在這里獲取Token end -----

            headers.setBearerAuth(jwt);
        }
        return chain.filter(exchange);
    }

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

    /**
     * 輸出未認(rèn)證響應(yīng)
     *
     * @param response 響應(yīng)體
     * @return 未認(rèn)證
     * @throws JsonProcessingException json解析異常
     */
    private Mono<Void> outUnauthorizedResponse(ServerHttpResponse response) throws JsonProcessingException {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        DataBufferFactory bufferFactory = response.bufferFactory();
        ObjectMapper objectMapper = new ObjectMapper();
        CuxResponseEntity<ExceptionResponse> responseEntity =
                new CuxResponseEntity<>(new ExceptionResponse("UNAUTHORIZED", "Unauthorized !"), new HttpHeaders(), HttpStatus.UNAUTHORIZED);
        DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
        return response.writeWith(Mono.fromSupplier(() -> wrap));
    }

    /**
     * 輸出通用異常響應(yīng)信息
     *
     * @param response 響應(yīng)體
     * @return 未認(rèn)證
     * @throws JsonProcessingException json解析異常
     */
    private Mono<Void> outCommonExceptionResponse(ServerHttpResponse response, String code, String msg) throws JsonProcessingException {
        response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
        DataBufferFactory bufferFactory = response.bufferFactory();
        ObjectMapper objectMapper = new ObjectMapper();
        CuxResponseEntity<ExceptionResponse> responseEntity =
                new CuxResponseEntity<>(new ExceptionResponse(code, msg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
        DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
        return response.writeWith(Mono.fromSupplier(() -> wrap));
    }
}

  1. 自定義異常處理(非必須)
    將網(wǎng)關(guān)拋的錯改造為自定義的異常格式讯檐,方便前端處理
/**
 * 自定義異常處理
 *
 * @author Jenson
 * @version 1.0
 */
@Slf4j
@Configuration
@Order(-1)
public class CuxWebExceptionHandler implements WebExceptionHandler {
    @SneakyThrows
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        } else if (ex instanceof ResponseStatusException rspEx) {
            HttpStatusCode httpStatusCode = rspEx.getStatusCode();
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            response.setStatusCode(httpStatusCode);
            DataBufferFactory bufferFactory = response.bufferFactory();
            ObjectMapper objectMapper = new ObjectMapper();
            CuxResponseEntity<ExceptionResponse> responseEntity =
                    new CuxResponseEntity<>(new ExceptionResponse(((HttpStatus) httpStatusCode).name(), rspEx.getReason()),
                            rspEx.getHeaders(), httpStatusCode.value());
            DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
            return response.writeWith(Mono.fromSupplier(() -> wrap));
        } else {
            return Mono.error(ex);
        }
    }
}

  1. 解決跨域
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * 跨域
 *
 * @author Jenson
 * @version 1.0
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 允許的請求頭
        config.addAllowedMethod("*");
        // 允許的請求源 (如:http://localhost:8080)
        config.addAllowedOrigin("*");
        // 允許的請求方法 ==> GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
        config.addAllowedHeader("*");
        // URL 映射 (如: /admin/**)
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}


業(yè)務(wù)服務(wù)

業(yè)務(wù)服務(wù)的認(rèn)證邏輯都是統(tǒng)一的,所以采用依賴starter組件的方式染服,就可以快速為每一個業(yè)務(wù)服務(wù)增加接口認(rèn)證

  1. 構(gòu)建cux-start-core工程
  • pom.xml 依賴注入
......
<groupId>com.cux</groupId>
    <artifactId>cux-starter-core</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>cux-starter-core</name>

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        ......
  1. 接口認(rèn)證filter(jwt)

此處代碼邏輯和認(rèn)證服務(wù)是一致的别洪,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)

/**
 * Token校驗過濾器
 *
 * @author Jenson
 */
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        HttpServletRequest httpServletRequest;
        httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        log.info("---------> Authorization : {}", authorization);
        if (StringUtils.hasText(authorization)) {

            String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
            String token = tokenDetail[1];
            if (StringUtils.hasText(token)) {
                User user = null;
                try {
                    // JwtUtils 是自定義的jwt-token解析工具
                    // 解析jwt-token,創(chuàng)建用戶對象
                    // HttpServletResponseUtils 是自定義的異常打印工具類
                    User = new User(JwtUtils.verify(token));
                } catch (UnauthorizedException e) {
                    HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
                    return;
                } catch (ForbiddenException e) {
                    HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
                    return;
                } catch (Exception e) {
                    HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
                    return;
                }
                // 賬號不為空且還沒有認(rèn)證過
                if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // 認(rèn)證成功柳刮,設(shè)置當(dāng)前用戶對象
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

  1. 接口未認(rèn)證異常
    此處代碼邏輯和認(rèn)證服務(wù)是一致的挖垛,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)
/**
 * 用戶未通過認(rèn)證訪問受保護(hù)的資源 401
 *
 * @author Jenson
 */
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        authException.printStackTrace();
        // 自定義異常打印工具
        HttpServletResponseUtils.outPrintUnauthorizedException(response);
    }
}

  1. 接口認(rèn)證無權(quán)限異常
    此處代碼邏輯和認(rèn)證服務(wù)是一致的痒钝,其實可以考慮將此核心依賴包運(yùn)用于認(rèn)證服務(wù)

/**
 * 認(rèn)證成功的用戶訪問受保護(hù)的資源,但是權(quán)限不夠 403
 *
 * @author Jenson
 */
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        // 自定義異常打印工具
        HttpServletResponseUtils.outPrintForbiddenException(response);
    }
}
  1. 認(rèn)證配置
/**
 * @author Jenson
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private PermitRequestMatchers permitRequestMatchers;

    /**
     * 授權(quán)配置晕换,最高優(yōu)先級
     *
     * @param http HttpSecurity
     * @return SecurityFilterChain
     * @throws Exception
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        http
                // 禁用表單登錄
                .formLogin().disable()
                // 設(shè)置URL的授權(quán)
                .authorizeHttpRequests()
                // 需要放行的url,動態(tài)獲取免登錄接口
                .requestMatchers(permitRequestMatchers.requestMatchersToArray())
                .permitAll()
                .anyRequest()
                .authenticated()
                //處理異常情況:認(rèn)證失敗和權(quán)限不足
                .and()
                .exceptionHandling()
                //認(rèn)證未通過午乓,不允許訪問異常處理器
                .authenticationEntryPoint(new EntryPointUauthenticationHandler())
                //認(rèn)證通過,但是沒權(quán)限處理器
                .accessDeniedHandler(new RequestAccessDeniedHandler())
                .and()
                //禁用session闸准,JWT校驗不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //將TOKEN校驗過濾器配置到過濾器鏈中益愈,否則不生效,放到UsernamePasswordAuthenticationFilter之前
                .addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 解決跨域問題
                .cors()
                .and()
                // 關(guān)閉csrf
                .csrf().disable();

        return http.build();
    }

}


  1. 服務(wù)間調(diào)用夷家,token傳遞(openfeign)

微服務(wù)間feign調(diào)用是不走網(wǎng)關(guān)的蒸其,為了互相傳遞當(dāng)前調(diào)用接口的jwt-token,需要配置feign库快,token的傳遞可以進(jìn)行公共配置

  • feign 依賴
 <!--服務(wù)調(diào)用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>4.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>4.0.1</version>
        </dependency>

  • Feign 請求攔截器
/**
 * Feign 請求攔截器
 * <p>
 * 請求帶上token
 *
 * @author Jenson
 */
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            String authorization = request.getHeader("Authorization");
            requestTemplate.header("Authorization", authorization);
        }
    }
}

/**
 * @author Jenson
 */
@Configuration
public class FeignRequestInterceptorConfig {

    @Bean
    public RequestInterceptor createFeignRequestInterceptor() {
        return new FeignRequestInterceptor();
    }
}
  1. spring.factories 配置文件

定義需要裝載的配置類

image.png
  1. 打包摸袁,安裝依賴

本地安裝:mvn install

  1. 將該核心依賴運(yùn)用于業(yè)務(wù)服務(wù),即可完成對業(yè)務(wù)服務(wù)的接口權(quán)限控制
    業(yè)務(wù)服務(wù)與認(rèn)證服務(wù)义屏、網(wǎng)關(guān)需要在同一個注冊中心下靠汁;
    網(wǎng)關(guān)需要配置對應(yīng)業(yè)務(wù)服務(wù)的路由;
    此時闽铐,直接訪問業(yè)務(wù)服務(wù)的接口需要jwt-token蝶怔,通過網(wǎng)關(guān)訪問,需要redis-token兄墅;
    只需將網(wǎng)關(guān)暴露到外部而不需要暴露業(yè)務(wù)服務(wù)

其他補(bǔ)充

  1. jwt 生成 和 解析
  • pom.xml 依賴
<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.5.0</version>
        </dependency>
  • 生成jwt
 /**
     * 生成jwt,設(shè)置超時時間
     *
     * @param payload 載荷
     * @return jwt
     */
    public static String generate(Map<String, String> payload) {
        //過期時間
        Date expireDate = new Date(System.currentTimeMillis() + DEFAULT_JWT_TTL);
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        JWTCreator.Builder jwtBuilder = JWT.create()
                // 添加頭部
                .withHeader(map)
                //超時設(shè)置,設(shè)置過期的日期
                .withExpiresAt(expireDate)
                //簽發(fā)時間
                .withIssuedAt(new Date());

        // 構(gòu)建 jwt 載荷
        payload.forEach(jwtBuilder::withClaim);
        // 簽名踢星,返回
        return jwtBuilder.sign(Algorithm.HMAC256(DEFAULT_JWT_SECRET));
    }

  • 解析 jwt
/**
     * 校驗token并解析token
     *
     * @return 從token中解析出的載荷
     */
    public static Map<String, Claim> verify(String token) {
        if ("token_absent".equals(token)) {
            // redis token 不存在
            throw new UnauthorizedException();
        }
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(DEFAULT_JWT_SECRET)).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaims();
        } catch (TokenExpiredException e) {
            log.error("jwt token已過期");
            throw new UnauthorizedException();
        } catch (JWTVerificationException e) {
            log.error("jwt token不存在或不正確");
            throw new ForbiddenException();
        }
    }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市隙咸,隨后出現(xiàn)的幾起案子沐悦,更是在濱河造成了極大的恐慌,老刑警劉巖五督,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藏否,死亡現(xiàn)場離奇詭異,居然都是意外死亡充包,警方通過查閱死者的電腦和手機(jī)秕岛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來误证,“玉大人继薛,你說我怎么就攤上這事∮保” “怎么了遏考?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蓝谨。 經(jīng)常有香客問我灌具,道長青团,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任咖楣,我火速辦了婚禮督笆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诱贿。我一直安慰自己娃肿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布珠十。 她就那樣靜靜地躺著料扰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪焙蹭。 梳的紋絲不亂的頭發(fā)上晒杈,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機(jī)與錄音孔厉,去河邊找鬼拯钻。 笑死,一個胖子當(dāng)著我的面吹牛撰豺,可吹牛的內(nèi)容都是我干的粪般。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼郑趁,長吁一口氣:“原來是場噩夢啊……” “哼刊驴!你這毒婦竟也來了姿搜?” 一聲冷哼從身側(cè)響起寡润,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舅柜,沒想到半個月后梭纹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡致份,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年变抽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氮块。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡绍载,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滔蝉,到底是詐尸還是另有隱情击儡,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布蝠引,位于F島的核電站阳谍,受9級特大地震影響蛀柴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜矫夯,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一鸽疾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧训貌,春花似錦制肮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至区拳,卻和暖如春拘领,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背樱调。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工约素, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笆凌。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓圣猎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乞而。 傳聞我的和親對象是個殘疾皇子送悔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容