微服务集成springsecurity实现认证

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进行的

相关推荐
沿途欣赏i5 天前
SpringSecurity剖析
springsecurity
筱顾大牛1 个月前
SpringSecurity实现登录功能实战!!!
spring boot·intellij-idea·登录·springsecurity
天启A1 个月前
实习记录day03:尝试写一个接口
软件工程日报
朝暮7822 个月前
SpringSecurity入门
spring boot·spring·springsecurity
Black_Cat_yyds2 个月前
SpringSecurity--DelegatingFilterProxy工作流程
filter·springsecurity
冯宝宝^2 个月前
SpringSecurity通用权限管理系统
vue.js·人工智能·spring boot·spring·springsecurity
Black_Cat_yyds2 个月前
会话固定攻击
运维·安全·springsecurity
慕木兮人可4 个月前
SpringBoot2.0.x旧版集成Swagger UI报错Unable to infer base url...解决办法
java·spring boot·cas·springsecurity·swagger-ui
徐子元竟然被占了!!5 个月前
SpringSecurity登录时在哪里调用我们自定义的UserDetailsServiceImpl
java·spring·springsecurity