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));
    }

}
相关推荐
virus59451 小时前
悟空CRM mybatis-3.5.3-mapper.dtd错误解决方案
java·开发语言·mybatis
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
NE_STOP2 小时前
spring6-注解式开发
spring
没差c2 小时前
springboot集成flyway
java·spring boot·后端
三水不滴2 小时前
Redis 过期删除与内存淘汰机制
数据库·经验分享·redis·笔记·后端·缓存
时艰.3 小时前
Java 并发编程之 CAS 与 Atomic 原子操作类
java·开发语言
编程彩机3 小时前
互联网大厂Java面试:从Java SE到大数据场景的技术深度解析
java·大数据·spring boot·面试·spark·java se·互联网大厂
笨蛋不要掉眼泪3 小时前
Spring Boot集成LangChain4j:与大模型对话的极速入门
java·人工智能·后端·spring·langchain
Yvonne爱编码3 小时前
JAVA数据结构 DAY3-List接口
java·开发语言·windows·python
像少年啦飞驰点、4 小时前
零基础入门 Spring Boot:从“Hello World”到可上线微服务的完整学习指南
java·spring boot·微服务·编程入门·后端开发