Spring Cloud Gateway 整合Spring Security

做了一個Spring Cloud項目,網關採用 Spring Cloud Gateway,想要用 Spring Security 進行權限校驗,由於 Spring Cloud Gateway 採用 webflux ,所以平時用的 mcv 配置是無效的,本文實現了 webflu 下的登陸校驗。

1. Security配置

這裏先貼出配置類,方便了解大致情況。

其中涉及到的三個處理器均爲自定義

package com.shop.jz.gateway.security.config;

import com.shop.jz.gateway.security.constants.Constants;
import com.shop.jz.gateway.security.handler.AuthenticationFailureHandler;
import com.shop.jz.gateway.security.handler.AuthenticationSuccessHandler;
import com.shop.jz.gateway.security.handler.ShopHttpBasicServerAuthenticationEntryPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;

/**
 * @author:JZ
 * @date:2020/5/21
 */
@Slf4j
@EnableWebFluxSecurity    // 開啓WebFluxSecurity,必須要添加
public class SecurityConfig {

    private String permitUrls = "/gateway/login1,/test1";

    /**
     * 鑑權成功處理器
     */
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    /**
     * 登陸驗證失敗處理器
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 未登錄訪問資源時的處理類,若無此處理類,前端頁面會彈出登錄窗口
     */
    @Autowired
    private ShopHttpBasicServerAuthenticationEntryPoint shopHttpBasicServerAuthenticationEntryPoint;


    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
        log.info("不進行權限校驗url:{}", this.permitUrls);
        httpSecurity.authorizeExchange()
                .pathMatchers(this.permitUrls.split(",")).permitAll()
                .anyExchange().authenticated().and()
                .httpBasic().and()
                .formLogin().loginPage(Constants.LOGIN_URL)                    // 登陸地址
                .authenticationSuccessHandler(authenticationSuccessHandler)    // 設置鑑權成功處理器
                .authenticationFailureHandler(authenticationFailureHandler)    // 設置登陸驗證失敗處理器
                .and().exceptionHandling().authenticationEntryPoint(shopHttpBasicServerAuthenticationEntryPoint)
                .and().csrf().disable()                          // 必須支持跨域
                .logout().logoutUrl(Constants.LOGOUT_URL);       // 退出登陸地址

        return httpSecurity.build();
    }

	// 密碼加密方式
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

2. 自定義 UserDetails

在Security中用戶信息需存放在 UserDetails 中,UserDetails 是一個接口,可以使用Security已經實現的 org.springframework.security.core.userdetails.User,也可以實現 UserDetails 接口自定義用戶信息類。

package com.shop.jz.gateway.security.model;

import com.jz.shop.user.dto.UserDto;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

/**
 * @author:JZ
 * @date:2020/5/17
 */
@Data
public class LoginUser implements UserDetails {

    /**
     * token
     */
    private String token;

    /**
     * login time
     */
    private Long loginTime;

    /**
     * expire time
     */
    private Long expireTime;

    /**
     * Login IP address
     */
    private String ip;

    /**
     * location
     */
    private String location;

    /**
     * Browser type
     */
    private String browser;

    /**
     * operating system
     */
    private String os;

    /**
     * 用戶名
     */
    private String userName;

    /**
     * 賬號密碼
     */
    private String userPwd;

    /**
     * 權限列表
     */
    private Set<String> permissions;

    public LoginUser() {}

    public LoginUser(String userName, String userPwd, Set<String> permissions) {
        this.userName = userName;
        this.userPwd = userPwd;
        this.permissions = permissions;
    }

    public LoginUser getLoginUser() {
        return this;
    }

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

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

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

    /**
     * Whether the account has not expired, which cannot be verified
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * Specifies whether the user is unlocked. Locked users cannot authenticate
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * Indicates whether the user's credentials (passwords) have expired, which prevents authentication
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * Available, disabled users cannot authenticate
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

3. 自定義獲取用戶信息

WebFlux 中Security通過調用 ReactiveUserDetailsService 接口的實現類獲取用戶信息,與 MVC 中的 UserDetailsService 不同。

package com.shop.jz.gateway.security.service;

import com.jz.shop.commons.execptions.BaseException;
import com.jz.shop.user.api.UserApi;
import com.jz.shop.user.dto.UserDto;
import com.shop.jz.gateway.security.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Slf4j
@Service
public class ShopUserDetailsService implements ReactiveUserDetailsService {

    @Autowired
    private UserApi userApi;   // 自定義實現的用戶信息查詢的feign接口

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        try {
            UserDto user = this.userApi.getUserInfoByUsername(username);
            LoginUser loginUser = new LoginUser(user.getUserName(), user.getPassword(), null);
            return Mono.just(loginUser);
        } catch (BaseException baseException) {
            log.warn(baseException.getMsg());
        }
        return Mono.error(new UsernameNotFoundException("User Not Found"));
    }
}

4. 鑑權成功處理器

當用戶名和密碼通過校驗後會進入 WebFilterChainServerAuthenticationSuccessHandler ,我們可以重寫 onAuthenticationSuccess 方法實現自定義返回信息

package com.shop.jz.gateway.security.handler;

import com.alibaba.fastjson.JSON;
import com.jz.shop.commons.model.Result;
import com.shop.jz.gateway.security.constants.Constants;
import com.shop.jz.gateway.security.model.LoginUser;
import com.shop.jz.gateway.security.service.TokenService;
import com.shop.jz.gateway.security.utils.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;

/**
 * 鑑權成功處理器
 * @author:JZ
 * @date:2020/5/21
 */
@Slf4j
@Component
public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {

    @Autowired
    private TokenService tokenService;

    public AuthenticationSuccessHandler() {}

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        ServerWebExchange exchange = webFilterExchange.getExchange();
        ServerHttpResponse response = exchange.getResponse();
        log.info("用戶:{} 登陸成功");
        // 設置返回信息
        HttpHeaders headers = response.getHeaders();
        headers.add("Content-Type", "application/json; charset=UTF-8");
        String responseJson = JSON.toJSONString(Result.success());
        DataBuffer dataBuffer = null;
        try {
            dataBuffer = response.bufferFactory().wrap(responseJson.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return response.writeWith(Mono.just(dataBuffer));
    }

}

5. 登陸驗證失敗處理器

當賬號密碼或權限驗證異常時,會進入該處理器。

package com.shop.jz.gateway.security.handler;

import com.alibaba.fastjson.JSON;
import com.jz.shop.commons.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;

/**
 * @author:JZ
 * @date:2020/5/21
 */
@Slf4j
@Component
public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
        log.warn("鑑權失敗");
        ServerWebExchange exchange = webFilterExchange.getExchange();
        ServerHttpResponse response = exchange.getResponse();
        // 設置返回信息
        HttpHeaders headers = response.getHeaders();
        headers.add("Content-Type", "application/json; charset=UTF-8");
        String responseJson = JSON.toJSONString(Result.fail("鑑權失敗"));
        DataBuffer dataBuffer = null;
        try {
            dataBuffer = response.bufferFactory().wrap(responseJson.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
        }
        return response.writeWith(Mono.just(dataBuffer));
    }

}

6. 未登錄訪問資源時的處理器

package com.shop.jz.gateway.security.handler;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author:JZ
 * @date:2020/5/21
 */
@Slf4j
@Component
public class ShopHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {

    private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
    private static final String DEFAULT_REALM = "Realm";
    private static String WWW_AUTHENTICATE_FORMAT = "Basic realm=\"%s\"";
    private String headerValue = createHeaderValue("Realm");

    public ShopHttpBasicServerAuthenticationEntryPoint() {}



    public void setRealm(String realm) {
        this.headerValue = createHeaderValue(realm);
    }

    private static String createHeaderValue(String realm) {
        Assert.notNull(realm, "realm cannot be null");
        return String.format(WWW_AUTHENTICATE_FORMAT, new Object[]{realm});
    }

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
        response.getHeaders().set(HttpHeaders.AUTHORIZATION, this.headerValue);
        JSONObject result = new JSONObject();
        result.put("code", "000000");
        result.put("msg", "未登錄鑑權");
        DataBuffer dataBuffer = response.bufferFactory().wrap(result.toJSONString().getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章