做了一个Spring Cloud项目,网关采用 Spring Cloud Gateway,想要用 Spring Security 进行权限校验,由于 Spring Cloud Gateway 采用 webflux ,所以平时用的 mvc 配置是无效的,本文实现了 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));
    }

}

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐