Spring Cloud Gateway 整合Spring Security

做了一个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));
    }

}
相关推荐
全靠bug跑10 分钟前
Spring Cloud OpenFeign 实战三部曲:快速集成 · 连接池优化 · 客户端抽取
java·spring boot·openfeign
Evan芙26 分钟前
搭建nexus服务,实现本地仓库、代理仓库
java·nginx·tomcat
乂爻yiyao33 分钟前
Java LTS版本重要升级特性对照表
java·开发语言
原来是好奇心1 小时前
深入Spring Boot源码(六):Actuator端点与监控机制深度解析
java·开发语言·源码·springboot
北城以北88881 小时前
Spring定时任务与Spring MVC拦截器
spring boot·spring·mvc
Victor3561 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易1 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
叠叠乐1 小时前
robot_state_publisher 参数
java·前端·算法
Kiri霧1 小时前
Range循环和切片
前端·后端·学习·golang
过期动态1 小时前
JDBC高级篇:优化、封装与事务全流程指南
android·java·开发语言·数据库·python·mysql