微服務整合springsecurity實現認證

天启A發表於2024-07-27

module:auth

1.新增依賴:spring-cloud-starter-security與spring-cloud-starter-oauth2

2.配置WebSecurityConfig:注入AuthenticationManager覆寫configure

3.配置tokenConfig

4.授權服務器配置:AuthorizationServer extends AuthorizationServerConfigurerAdapter配置客戶端詳細任務,令牌端點配置

5.實現UserDetailsService,覆寫loadUserByUsername(最熟悉的一集)

tips:這裡return 的UserDetails物件不用自己建立,springsecurity提供了一個User類實現了UserDetails,透過建造者模式可以直接生成username,password,authorities等資訊。這裡如果需要多引數分發給其他module使用,建議的做法是將資料庫中查出來的物件透過json轉成字串存入username,後續可以直接將username反寫成具體的class

module:gateway

首先明確閘道器的作用:路由轉發、認證、白名單放行:針對當前閘道器的路由進行轉發,如果是白名單則直接放行,如果是需要JWT校驗則需要校驗JWT合法性

security中提供了認證與授權,這裡我們在閘道器進行了認證,也就是說授權模組是在各個微服務中進行的

gateway整合springsecurity:

1.新增依賴:spring-cloud-starter-security與spring-cloud-starter-oauth2

2.新增配置:過濾器;再新增tokenConfig、securityConfig


/**
*
* @description 閘道器認證過濾器:這段程式碼主要是作為過濾器去處理一個http請求,首先是檢查請求路徑是否在白名單中,請求是否攜帶token/有沒有過期等安全相關檢查
*/

@Component @Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered { //白名單 private static List<String> whitelist = null; static { //載入白名單 try ( InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties"); ) { Properties properties = new Properties(); properties.load(resourceAsStream); Set<String> strings = properties.stringPropertyNames(); whitelist= new ArrayList<>(strings); } catch (Exception e) { log.error("載入/security-whitelist.properties出錯:{}",e.getMessage()); e.printStackTrace(); } } @Autowired private TokenStore tokenStore; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestUrl = exchange.getRequest().getPath().value(); AntPathMatcher pathMatcher = new AntPathMatcher(); //白名單放行 for (String url : whitelist) { if (pathMatcher.match(url, requestUrl)) { return chain.filter(exchange); } } //檢查token是否存在 String token = getToken(exchange); if (StringUtils.isBlank(token)) { return buildReturnMono("沒有認證",exchange); } //判斷是否是有效的token OAuth2AccessToken oAuth2AccessToken; try { oAuth2AccessToken = tokenStore.readAccessToken(token); boolean expired = oAuth2AccessToken.isExpired(); if (expired) { return buildReturnMono("認證令牌已過期",exchange); } return chain.filter(exchange); } catch (InvalidTokenException e) { log.info("認證令牌無效: {}", token); return buildReturnMono("認證令牌無效",exchange); } } /** * 獲取token */ private String getToken(ServerWebExchange exchange) { String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(tokenStr)) { return null; } String token = tokenStr.split(" ")[1]; if (StringUtils.isBlank(token)) { return null; } return token; } private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); String jsonString = JSON.toJSONString(new RestErrorResponse(error)); byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return 0; } }

    // 假設已經從請求中獲得了token  
    String token = getToken(exchange);  
  
    // 如果token存在且不為空  
    if (token != null && !token.isEmpty()) {  
        // 建立一個新的ServerHttpRequest,並新增token到Authorization頭  
        ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()  
                .header("Authorization", "Bearer " + token)
                .build();  
  
        // 使用ServerWebExchangeDecorator來包裝原始的ServerWebExchange,並且使用修改後的請求  
        ServerWebExchange mutatedExchange = exchange.mutate()  
                .request(mutatedRequest)  
                .build();  

        return chain.filter(mutatedExchange);  
    }  

3.配置白名單security-whitelist.properties

/media/open/**=媒資管理公開訪問介面

4.在其他微服務模組中放行所有路由,並且直接透過SecurityContextHolder.getContext().getAuthentication()獲取物件(因為在閘道器中已經校驗完了,這裡只負責授權)

module:others

1.包裝一個SecurityUtil。SecurityContextHolder.getContext().getAuthentication().getPrincipal();即可獲得具體username,然後將username轉為實體類即可使用

@Slf4j
public class SecurityUtil {

    public static XcUser getUser() {
        try {
            Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principalObj instanceof String) {
                //取出使用者身份資訊
                String principal = principalObj.toString();
                //將json轉成物件
                XcUser user = JSON.parseObject(principal, XcUser.class);
                return user;
            }
        } catch (Exception e) {
            log.error("獲取當前登入使用者身份出錯:{}", e.getMessage());
            e.printStackTrace();
        }

        return null;
    }


    @Data
    public static class XcUser implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id;

        private String username;

        private String password;

        private String salt;

        private String name;
        private String nickname;
        private String wxUnionid;
        private String companyId;
        /**
         * 頭像
         */
        private String userpic;

        private String utype;

        private LocalDateTime birthday;

        private String sex;

        private String email;

        private String cellphone;

        private String qq;

        /**
         * 使用者狀態
         */
        private String status;

        private LocalDateTime createTime;

        private LocalDateTime updateTime;


    }


}

使用說明:上文使用了成員內部類,XcUser定義在外部類的成員位置,其不能建立獨立物件,必須依靠外部類例項.new 內部類()來建立例項

SecurityUtil.XcUser user = SecurityUtil.getUser();

Q&A:

token在gateway中傳遞下去的?

(無oauth2整合)一般情況下我們需要修改ServerWebExchange裡的ServerHttpRequest,但是ServerHttpRequest是不可變的。因此如果我們需要新增上token,就需要新建一個ServerWebExchange,重寫ServerHttpRequest。

除此以外,因為我們還會將請求傳遞給下一個GatewayFilterChain,我們可以在這個請求傳遞的時候透過修改ServerWebExchange的MutableHttpRequest:

(有oauth2整合)springsecurity的過濾器鏈會自動處理從請求中提取token,並將其附加到上下文中,下游的過濾器會透過springsecurity的API獲取他

為什麼其他微服務模組能直接透過SecurityContextHolder.getContext().getAuthentication()獲取物件?

在微服務中,每一個模組都有獨屬於自己的JVM,擁有獨屬於自己的執行緒,也就是說每個模組的SecurityContext是不一樣的。當我們使用oauth2,使用者透過上述gateway進行認證,並獲取一個jwt將其放在request中。

當一個模組接收到一個包含oauth2令牌的http請求時,他會使用這個令牌來驗證使用者身份,並且基於令牌構建一個新的SecurityContext。也就是說SecurityContext的實現其實是侷限於單個服務的單個執行緒的,跨服務的安全資訊傳遞是由oauth2進行的

相關文章