工作实战之微服务认证实现

news2024/11/14 2:17:53

目录

前言

一、钻石段位springsecurity+springsession+redis缓存

1.yml配置将session存储到redis中,实现session共享

 2.maven引入

3.原理分析

a.SessionRepositoryFilter拦截进行session存储介质的选择,可以是jdk缓存,或者数据库,也可以是redis缓存

b.SessionRepositoryRequestWrapper重写request

c.sessionRepository就是RedisOperationsSessionRepository

二、星耀段位springsecurity+oauth2+jwttoken

1.微服务架构图

2.代码实现(通过拦截实现用户信息服务间的传递)

 a.网关拿到access_token查询是否存在认证信息

b.如果存在,则把用户信息转换成jwttoken放入请求头

c. A服务从头中获取jwttoken,解析出用户信息

d.A服务处理完业务逻辑,将jwttoken从新放入请求头中

 3.疑问

总结


前言

青铜段位:单体应用,认证方式sessionId

白银段位:微服务应用,认证方式springsession+sessionId同步,存在延迟

钻石段位:微服务应用,认证方式springsession+redis分布式缓存,存在跨域问题以及CSRF攻击

星耀段位:微服务应用,认证方式springsecurity+oauth2+jwttoken+redis 方式


一、钻石段位springsecurity+springsession+redis缓存

1.yml配置将session存储到redis中,实现session共享

server:
  port: 8080
  servlet:
    session:
      timneout: 3000

spring:
  application:
    name: SpringSessionRedis
  redis:
    host: localhost
    port: 6379
    timeout: 3000
    pool:
      max-idle: 8
      min-idle: 0
      max-active: 8
      max-wait: 1
  session:
    store-type: redis
    redis:
      #用于存储在redis中key的命名空间
      flush-mode: on_save
      #session更新策略,有ON_SAVE、IMMEDIATE,前者是在调用#SessionRepository#save(org.springframework.session.Session)时,在response commit前刷新缓存,
      #后者是只要有任何更新就会刷新缓存
      namespace: 'spring:session'

 2.maven引入

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>1.0.1.RELEASE</version>
</dependency>

3.原理分析

a.SessionRepositoryFilter拦截进行session存储介质的选择,可以是jdk缓存,或者数据库,也可以是redis缓存

SessionRepositoryFilter -> doFilterInternal:
@Override
protected void doFilterInternal(HttpServletRequest request,
		HttpServletResponse response, FilterChain filterChain)
				throws ServletException, IOException {
	request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

	SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
			request, response, this.servletContext);
	SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
			wrappedRequest, response);
    //包装 request,response
	HttpServletRequest strategyRequest = this.httpSessionStrategy
			.wrapRequest(wrappedRequest, wrappedResponse);
	HttpServletResponse strategyResponse = this.httpSessionStrategy
			.wrapResponse(wrappedRequest, wrappedResponse);

	try {
		filterChain.doFilter(strategyRequest, strategyResponse);
	}
	finally {
	    //更新session
		wrappedRequest.commitSession();
	}
}

b.SessionRepositoryRequestWrapper重写request

private S getSession(String sessionId) {
	S session = SessionRepositoryFilter.this.sessionRepository
			.getSession(sessionId);
	if (session == null) {
		return null;
	}
	session.setLastAccessedTime(System.currentTimeMillis());
	return session;
}

// 重写 父类 getSession 方法
@Override
public HttpSessionWrapper getSession(boolean create) {
	HttpSessionWrapper currentSession = getCurrentSession();
	if (currentSession != null) {
		return currentSession;
	}
	//从当前请求获取sessionId
	String requestedSessionId = getRequestedSessionId();
	if (requestedSessionId != null
			&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
		S session = getSession(requestedSessionId);
		if (session != null) {
			this.requestedSessionIdValid = true;
			//对Spring session 进行包装(包装成HttpSession)
			currentSession = new HttpSessionWrapper(session, getServletContext());
			currentSession.setNew(false);
			setCurrentSession(currentSession);
			return currentSession;
		}
		else {
			// This is an invalid session id. No need to ask again if
			// request.getSession is invoked for the duration of this request
			setAttribute(INVALID_SESSION_ID_ATTR, "true");
		}
	}
	if (!create) {
		return null;
	}
	S session = SessionRepositoryFilter.this.sessionRepository.createSession();
	session.setLastAccessedTime(System.currentTimeMillis());
	//对Spring session 进行包装(包装成HttpSession)
	currentSession = new HttpSessionWrapper(session, getServletContext());
	setCurrentSession(currentSession);
	return currentSession;
}

c.sessionRepository就是RedisOperationsSessionRepository

二、星耀段位springsecurity+oauth2+jwttoken

1.微服务架构图

1.前端发起系统登录,网关接收到登录请求,转发到认证服务,认证服务通过生成一个access_token返回给前端,前端保存到前端缓存中,同时后端保存access_token与用户的信息到redis缓存

2. 当前端再次拿access_token发起请求时,网关首先从redis缓存中查询是否有认证信息,如果有,则拿到用户信息,生成jwttoken,放入请求头中,往后传

3.A服务从请求头中拿到jwttoken,解析出用户信息放入applicationContext中,供服务使用

4.A服务调用B服务,将jwttoken又放入请求头中,B服务拿到jwttoken解析用户信息,同样放入applicationcontext中,供服务使用

2.代码实现(通过拦截实现用户信息服务间的传递)

 a.网关拿到access_token查询是否存在认证信息

@Component
public class GetUserDetailsFilter implements HelperFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(GetUserDetailsFilter.class);

    private static final String BEARER_PREFIX = "Bearer ";

    private GetUserDetailsService getUserDetailsService;

    private ReadonlyRedisTokenStore tokenStore;

    private GatewayHelperProperties helperProperties;


    public GetUserDetailsFilter(GetUserDetailsService getUserDetailsService
            , ReadonlyRedisTokenStore tokenStore,GatewayHelperProperties helperProperties) {
        this.getUserDetailsService = getUserDetailsService;
        this.tokenStore = tokenStore;
        this.helperProperties=helperProperties;

    }

    @Override
    public int filterOrder() {
        return 40;
    }

    @Override
    public boolean shouldFilter(RequestContext context) {
        return context.getCustomUserDetails() == null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public boolean run(RequestContext context) {
        //校验客户端是否带有token
        String accessToken = context.request.accessToken;
        if (ObjectUtils.isEmpty(accessToken)) {
            context.response.setStatus(CheckState.PERMISSION_ACCESS_TOKEN_NULL);
            context.response.setMessage("Access_token is empty, Please login and set access_token by HTTP header 'Authorization'");
            return Boolean.FALSE;
        }
        //判断是否是被踢下线用户
        if (org.springframework.util.StringUtils.startsWithIgnoreCase(accessToken, BEARER_PREFIX)) {
            accessToken = accessToken.substring(BEARER_PREFIX.length());
        }
        boolean exists = tokenStore.existsOfflineAccessToken(accessToken);
        if (exists) {
            LOGGER.warn("已在其他地方登录,记录了下线token");
            //下线token存在,这是被踢下去的用户
            context.response.setStatus(CheckState.PERMISSION_ACCESS_TOKEN_OFFLINE);
            context.response.setMessage("Access_token is offline, Please login again");
            return Boolean.FALSE;
        }

        //验证用户是否token已经过期
        CustomUserDetailsWithResult result = getUserDetailsService.getUserDetails(accessToken);
        if (result.getCustomUserDetails() == null) {
            context.response.setStatus(result.getState());
            context.response.setMessage(result.getMessage());
        }
}

b.如果存在,则把用户信息转换成jwttoken放入请求头

@Component
public class AddJwtFilter implements HelperFilter {

    private final ObjectMapper objectMapper = new ObjectMapper();

    private Signer jwtSigner;

    public AddJwtFilter(Signer jwtSigner) {
        this.jwtSigner = jwtSigner;
    }

    @Override
    public int filterOrder() {
        return 50;
    }

    @Override
    public boolean shouldFilter(RequestContext context) {
        return true;
    }

    @Override
    public boolean run(RequestContext context) {
        try {
            String token = objectMapper.writeValueAsString(context.getCustomUserDetails());
            String jwt = "Bearer " + JwtHelper.encode(token, jwtSigner).getEncoded();
            context.response.setJwt(jwt);
            return true;
        } catch (JsonProcessingException e) {
            context.response.setStatus(CheckState.EXCEPTION_GATEWAY_HELPER);
            context.response.setMessage("gateway helper error happened: " + e.toString());
            return false;
        }
    }

}

c. A服务从头中获取jwttoken,解析出用户信息

public class JwtTokenFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
       
        try {
            //解析认证信息
            Authentication authentication = this.tokenExtractor.extract(httpRequest);

            if (authentication == null) {
                if (this.isAuthenticated()) {
                    LOGGER.debug("Clearing security context.");
                    SecurityContextHolder.clearContext();
                }
                LOGGER.debug("No Jwt token in request, will continue chain.");
                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "No Jwt token in request.");
                return;
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    needsDetails.setDetails(new OAuth2AuthenticationDetails(httpRequest));
                }
                //再次认证
                Authentication authResult = this.authenticate(authentication);
                LOGGER.debug("Authentication success: {}", authResult);
                //放入上下文中
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
            chain.doFilter(request, response);
        } catch (OAuth2Exception e) {
            LOGGER.debug("Authentication request failed: ", e);
            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token.");
        } finally {
            SecurityContextHolder.clearContext();
        }
    }


    protected Authentication authenticate(Authentication authentication) {
        if (authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        } else {
            String token = (String) authentication.getPrincipal();
            OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
            if (auth == null) {
                throw new InvalidTokenException("Invalid token: " + token);
            } else {
                if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
                    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
                    if (!details.equals(auth.getDetails())) {
                        details.setDecodedDetails(auth.getDetails());
                    }
                }

                auth.setDetails(authentication.getDetails());
                auth.setAuthenticated(true);
                return auth;
            }
        }
    }

}

d.A服务处理完业务逻辑,将jwttoken从新放入请求头中


public class JwtRequestInterceptor implements FeignRequestInterceptor {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final String OAUTH_TOKEN_PREFIX = "Bearer ";
    private Signer signer;

    private CoreProperties coreProperties;

    public JwtRequestInterceptor(CoreProperties coreProperties) {
        this.coreProperties = coreProperties;
    }

    @PostConstruct
    private void init() {
        signer = new MacSigner(coreProperties.getOauthJwtKey());
    }

    @Override
    public int getOrder() {
        return -1000;
    }

    @Override
    public void apply(RequestTemplate template) {
        String token = null;
        try {
            if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
                    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
                    if (StringUtils.isNoneBlank(details.getTokenType(), details.getTokenValue())) {
                        token = details.getTokenType() + " " + details.getTokenValue();
                    } else if (details.getDecodedDetails() instanceof CustomUserDetails) {
                        token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(details.getDecodedDetails()), signer).getEncoded();
                    }
                } else if (authentication.getPrincipal() instanceof CustomUserDetails) {
                    token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(authentication.getPrincipal()), signer).getEncoded();
                }
            }

            if (token == null) {
                LOGGER.debug("Feign request set Header Jwt_Token, no member token found, use AnonymousUser default.");
                token = OAUTH_TOKEN_PREFIX + JwtHelper.encode(OBJECT_MAPPER.writeValueAsString(DetailsHelper.getAnonymousDetails()), signer).getEncoded();
            }
        } catch (Exception e) {
            LOGGER.error("generate jwt token failed {}", e.getMessage());
        }

        template.header(RequestVariableHolder.HEADER_JWT, token);
        setLabel(template);
    }

    private void setLabel(RequestTemplate template) {
        String label = RequestVariableHolder.LABEL.get();
        if (label != null && label.trim().length() > 0) {
            template.header(RequestVariableHolder.HEADER_LABEL, label);
        }

    }
}

 3.疑问

1.jwttoken,如果只用jwttoken保存用户信息,客户端保存即可,服务无需保存,那么退出注销怎么做?

2.上述架构,前端与服务集群后端使用access_token认证,后端保存了access_token与用户信息在redis缓存,服务间用jwttoken传递认证信息,优势在哪里?


总结

这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
更适用于移动端:当客户端是非浏览器平台时,cookie是不支持的,采用token认证方式会简单很多
无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Leedcode 二分查找 理解1

一个up的理解 一、二分查找基础例题 力扣https://leetcode.cn/problems/binary-search/ 二、二分查找模板问题 带搜索区间分为3个部分&#xff1a; 1、[mid]&#xff0c;直接返回 2、[left&#xff0c;mid-1]&#xff0c;设置边界right mid - 1 3、[mid1,right]&#x…

【相关分析-高阶绘图】MATLAB实现皮尔逊相关分析-散点直方图

虽然皮尔逊相关分析很常见,但如何更好的展现相关性、散点分布、柱状分布,以提升研究结果的美感和冲击感呢?本文拟通过MATLAB绘制包含散点分布、柱状分布、线性展示的散点直方图,有助于审稿人眼前一亮。 1、Pearson相关系数原理 Pearson相关系数(Pearson Correlation Co…

Zookeeper3.5.7版本——单机部署(linux环境-centos7)

目录一、Zookeeper3.5.7官网下载1.1、官网下载地址1.2、下载步骤二、jdk11安装&#xff08;Zookeeper需要jdk支持&#xff09;三、Zookeeper3.5.7安装3.1、安装3.2、配置修改3.3、操作 Zookeeper四、配置文件参数解读一、Zookeeper3.5.7官网下载 1.1、官网下载地址 官网下载地…

基本面向对象编程-计算机基本功能实现_

《C/S项目实训》实验报告 实验名称&#xff1a; 基本面向对象编程-计算机基本功能实现_ 一、实验目的 通过综合实践项目&#xff0c;理解Java 程序设计是如何体现面向对象编程基本思想&#xff0c;掌握OOP方法&#xff0c;掌握事件触发、消息响应机制。进一步巩固面向对…

T_SQL和SQL的区别

一. SQL Server和T-SQL的区别&#xff08;⭐T-SQL 包含了 SQL&#xff09;SQL Server是结构化查询语言,是目前关系型数据库管理系统中使用最广泛的查询语言T-SQL是标准SQL语言的扩展,是SQL Server的核心,在SQL的的基础上添加了变量,运算符,函数和流程控制等&#xff0c;Microso…

AutoCAD通过handle id选择实体

获得实体的handle id。注意是handle id 不是id&#xff0c;方法有2种&#xff1a;方法&#xff08;a&#xff09;&#xff1a;通过ArxDeg插件&#xff08;ObjectARX附带的源码编译得到&#xff1a;\samples\database\ARXDBG&#xff09;查找&#xff1a;此handle id本来就是16进…

【知识图谱论文】知识图谱嵌入的对比学习

文章题目&#xff1a; KGE-CL: Contrastive Learning of Knowledge Graph Embeddings时间&#xff1a;2021 摘要 学习知识图的嵌入在人工智能中至关重要&#xff0c;可以使各种下游应用受益&#xff0c;如推荐和问答。近年来&#xff0c;人们对知识图嵌入进行了大量的研究。然…

linux下安装java

文章目录linux下安装java1.下载对应的linux JDK包&#xff0c;这里使用jdk82.上传 jdk-8-linux-x64.tar.gz 到linux3.解压下载的jdk4.编辑配置文件&#xff0c;配置环境变量5.刷新配置文件6.最后检查JDK安装是否成功linux下安装java 阿里云弄的服务器centos7没有安装java&…

显示接口测试

背景需求两个显示器连接到一台PC&#xff0c;期望每台显示器可以单独显示&#xff0c;在一台显示器显示时&#xff0c;另外一台显示器同PC的连接断开&#xff0c;即系统下查看到连接状态为disconnected。同时在显示器上图形化显示当前显示器编号。如下图&#xff0c;期望当显示…

【pygame游戏】Python实现蔡徐坤大战篮球游戏【附源码】

前言 话说在前面&#xff0c;我不是小黑子~&#x1f60f; 本文章纯属技术交流~娱乐 前几天我获得了一个坤坤打篮球的游戏&#xff0c;也给大家分享一下吧~ 好吧&#xff0c;其实并不是这样的游戏&#xff0c;往下慢慢看吧。 准备工作 开发环境 Python版本&#xff1a;3.7.8 …

PhotoShop基础使用

49&#xff1a;图片分类 1&#xff1a;像素图 特点&#xff1a;放大后可见&#xff0c;右一个个色块&#xff08;像素&#xff09;组合而成。 优点&#xff1a;容量小&#xff0c;纯天然 JPG、JPEG、png、GIF 2&#xff1a;矢量图 面向对象图像 绘图图像 特点&#xff1a;不…

动态网站开发讲课笔记03:HTTP协议

文章目录零、本节学习目标一、HTTP概述&#xff08;一&#xff09;HTTP的概念1、HTTP的概念2、HTTP协议的特点&#xff08;1&#xff09;C/S模式&#xff08;2&#xff09;简单快速&#xff08;3&#xff09;灵活&#xff08;4&#xff09;无状态&#xff08;二&#xff09;HTT…

VS Code 配置 Python + 配置 Flask 环境及其虚拟环境

本文目录一、Python的下载二、拓展库安装三、编写案例运行调试四、部分使用优化五、配置 Flask 环境总结&#xff1a;一些小技巧1、找到原来安装过的python路径位置一、Python的下载 这里推荐使用国内源进行下载&#xff0c;国外源一般都比较慢。 跳转链接&#xff1a;https:…

redis 未授权访问漏洞

redis 未授权访问漏洞 目录 redis 未授权访问漏洞 漏洞描述 漏洞原因&#xff1a; 漏洞危害 漏洞复现&#xff1a; 漏洞复现 写webshell: 写计划任务&#xff1a;centos默认在/var/spool/cron 写ssh公钥实现ssh登录&#xff1a; 漏洞描述&#xff1a; Redis默认情况下…

UNet-肝脏肿瘤图像语义分割

目录 一. 语义分割 二. 数据集 三. 数据增强 图像数据处理步骤 CT图像增强方法 &#xff1a;windowing方法 直方图均衡化 获取掩膜图像深度 在肿瘤CT图中提取肿瘤 保存肿瘤数据 四. 数据加载 数据批处理 ​编辑​编辑 数据集加载 五. UNet神经网络模型搭建 单张图片…

【优化】性能优化Springboot 项目配置内置Tomcat使用Http11AprProtocol(AIO)

Springboot 项目配置内置tomcat使用Http11AprProtocol(AIO) Windows版本 1.下载Springboot对应版本tomcat包 下载地址 Apache Tomcat - Apache Tomcat 9 Software Downloads 找到bin目录下 tcnative-1.dll 文件 2 放到jdk的bin目录下 Linux版本 在Springboot中内嵌的Tomcat默…

实验三、数字PID控制器的设计

实验三、数字PID控制器的设计 --- 直流闭环调速实验 一、实验目的 1&#xff0e;理解晶闸管直流单闭环调速系统的数学模型和工作原理;. 2. 掌握PID控制器参数对控制系统性能的影响; 3. 能够运用MATLAB/Simulink软件对控制系统进行正确建模并对模块进行正确的参数设置; 4.…

如何获取或设置CANoe以太网网卡信息(GET篇)

CAPL提供了一系列函数用来操作CANoe网卡。但是,但是,首先需要明确一点,不管是获取网卡信息,还是设置网卡信息,只能访问CAPL程序所在的节点下的网卡,而不是节点所在的以太网通道下的所有网卡 关于第一张图中,Class节点下,有三个网卡:Ethernet1、VLAN 1.100、VLAN 1.200…

Malware Dev 02 - Windows SDDL 后门利用之 SCManager

写在最前 如果你是信息安全爱好者&#xff0c;如果你想考一些证书来提升自己的能力&#xff0c;那么欢迎大家来我的 Discord 频道 Northern Bay。邀请链接在这里&#xff1a; https://discord.gg/9XvvuFq9Wb我拥有 OSCP&#xff0c;OSEP&#xff0c;OSWE&#xff0c;OSED&…

关于tf.gather函数batch_dims参数用法的理解

关于tf.gather函数batch_dims参数用法的理解0 前言1. 不考虑batch_dims2. 批处理(考虑batch_dims)2.1 batch_dims12.2 batch_dims02.3 batch_dims>22.4 batch_dims再降为12.5 再将axis降为12.6 batch_dims<02.7 batch_dims总结3. 补充4. 参数和返回值5. 其他相关论述6. 附…