主頁 >  其他 > 微服務認證鑒權gateway+oauth2+security+jwt

微服務認證鑒權gateway+oauth2+security+jwt

2021-04-09 11:48:07 其他

文章目錄

  • 本文認證鑒權思路方案
  • 一. 認證服務器
    • 1. 需要依賴
    • 2. 撰寫認證服務
    • 3. 安全配置
    • 4. 開放介面配置
  • 二. 資源服務器(此處可理解為鑒權服務)
    • 1. 需要依賴
    • 2. 撰寫鑒權管理器
    • 3. 撰寫資源服務
    • 3. 黑名單過濾器
    • 4. 例外處理
    • 5. JWT重繪方案
    • 6. 配置網關模塊呼叫認證模塊獲取jwt加密公鑰地址
  • 三. 配置完畢,開始測驗
    • 1. 獲取Token
    • 2. 重繪Token
    • 3. 攜帶Token訪問資源
    • 4. 退出登錄
    • 5. 退出登錄后再次訪問資源

本文認證鑒權思路方案

在這里插入圖片描述
實作思路受到開源電商專案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://www.uj5u.com/qita/274165.html

標籤:其他

上一篇:小白都能看懂的Redis集群部署手冊

下一篇:基于量子密鑰的經典身份認證系統

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more