单点登录

单点登录

1. 传统的web登录

1.1 方案描述

session+jsessionId
在服务器端创建了一个session以后,针对这个session会产生一个唯一的标识(sessionId),服务器端会将sessionId通过cookie的方式保存在浏览器中,这个cookie是会话级别的cookie。

1.2 存在的问题

在集群环境下存在session共享的问题

1.3 解决方案

将session在多个服务器间进行共享

  1. session复制:session复制会导致另外一个问题,session风暴,严重影响服务器的性能
  2. JWT令牌:第二部分不要携带敏感数据。缺点:JWT令牌太长了,影响数据传输的效率
  3. redis存储:token(键)+用户信息(值)

2. 单点登录

用户只需要登录一次就可以访问所有的应用系统

2.1 实现方案

  1. JWT令牌
  2. Redis存储

2.2 使用Redis存储的代码实现

@Override
public UserLoginSuccessVo login(UserLoginDTO userLoginDTO) {
	// 根据用户名查询数据库,获取用户数据
	LambdaQueryWrapper<UserInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
	lambdaQueryWrapper.eq(UserInfo::getLoginName, userLoginDTO.getLoginName());
	UserInfo userInfo = userInfoMapper.selectOne(lambdaQueryWrapper);

	// 判断用户是否存在
	if (userInfo == null){
		throw new GmallException(ResultCodeEnum.USER_LOGIN_ERROR);
	}

	// 判断密码是否正确,先对用户输入的密码进行md5加密,然后与数据库中的密码进行比较
	String md5passwd = DigestUtils.md5DigestAsHex(userLoginDTO.getPasswd().getBytes(StandardCharsets.UTF_8));
	if (!md5passwd.equals(userInfo.getPasswd())){
		throw new GmallException(ResultCodeEnum.USER_LOGIN_ERROR);
	}

	// 生成token
	String token = UUID.randomUUID().toString().replace("-", "");

	// 把token和用户信息保存到redis中
	redisTemplate.opsForValue().set(GmallConstant.REDIS_USER_LOGIN_PREFIX + token, JSON.toJSONString(userInfo),
									30, TimeUnit.MINUTES);

	// 构建响应数据进行返回
	UserLoginSuccessVo userLoginSuccessVo = new UserLoginSuccessVo();
	userLoginSuccessVo.setToken(token);
	userLoginSuccessVo.setNickName(userInfo.getNickName());

	// 返回
	return userLoginSuccessVo;
}

3. 全局过滤器

当用户访问受保护的接口(要求用户必须登录),需要校验用户是否登录。如果登录了,才允许用户访问,未登录就把用户踢回到登录页面。
实现思路:

  1. 在指定的微服务中添加拦截器
  2. 在Gateway中使用全局过滤器
@Slf4j
@Component
public class UserAuthFilter implements GlobalFilter, Ordered {
	/**
     * 拦截所有的请求
     * @param exchange : 封装了请求对象和响应对象
     * @param chain : 过滤器链,通过它可以实现放行操作
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("UserAuthFilter....全局过滤器执行了");
		return chain.filter(exchange);   // 放行操作
    }

    @Override
    public int getOrder() {  // 定义过滤器的顺序,数字越小,优先级越高
        return -1;
    }
}

3.1 不需要用户登录就可以访问的资源,直接放行

  1. 在application.yml文件中定义不需要登录认证的资源路径规则
app:
  auth:
    noauthurl:
      - /css/**
      - /img/**
      - /js/**
  1. 定义实体属性类
@Data
@ConfigurationProperties(prefix = "app.auth")
public class AuthUrlProperties {
    /**
     * 定义不需要认证的资源路径规则
     */
    private List<String> noauthurl;
}

在启动类上添加@EnableConfigurationProperties(value = AuthUrlProperties.class)
3. 代码实现,可以使用AntPathMatcher路径匹配器进行路径规则判断

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	// 获取真实的资源请求路径
	String path = exchange.getRequest().getURI().getPath();
	/**
         * 并不是所有的资源都需要用户登录后才可以访问
         * 如果访问的是不需要用户登录就可以访问的资源(静态资源),那么应该放行
         * 1. 定义不需要验证登录的资源路径
         * 2. 获取真实请求的资源路径
         * 3. 判断真实请求的资源路径是否满足定义的不需要验证登录的资源路径规则,如果满足直接放行
         */
	List<String> noauthurl = authUrlProperties.getNoauthurl();
	for (String urlPattern : noauthurl){
		if (antPathMatcher.match(urlPattern, path)){  // 匹配成功后直接放行
			// log.info("请求的资源路径为:{}", path);
			return chain.filter(exchange);
		}
	}
	// 放行
    return chain.filter(exchange);
}

3.2 需要用户登录才可以访问的资源,进行登录验证。已登录直接放行,未登录重定向到登录页面。

  1. 在application.yml文件中定义需要登录认证的资源路径规则
app:
  auth:
    authurl:
      - /order/**
      - /skill/**
  1. 定义实体属性类
@Data
@ConfigurationProperties(prefix = "app.auth")
public class AuthUrlProperties {
    /**
     * 需要登录认证的资源路径规则
     */
    private List<String> authurl;
}
  1. 代码实现
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	// 获取真实的资源请求路径
	String path = exchange.getRequest().getURI().getPath();
	/**
     * 对那些需要登录验证的请求路径,进行登录验证。如果已登录直接放行,未登录重定向到登录页面
     */
	List<String> authurl = authUrlProperties.getAuthurl();
	for (String authurlPattern : authurl){
		if (antPathMatcher.match(authurlPattern, path)){    // 验证登录操作
			// 获取token
			String token = getUserToken(exchange);
			if (!StringUtils.isEmpty(token)){
				// 根据token从redis中查询用户信息
				String userInfoJson = redisTemplate.opsForValue().get(GmallConstant.REDIS_USER_LOGIN_PREFIX + token);
				if (!StringUtils.isEmpty(userInfoJson)){    // 可以查询到数据,直接放行,并进行用户id透传
					return userIdThrought(userInfoJson, exchange, chain);
				} else {
					// 查询不到数据,重定向到登录页面
					return locationUrl(exchange, path);
				}
			}else{
				return locationUrl(exchange, path);
			}
		}
	}
	return chain.filter(exchange);
}

// 获取token数据
private String getUserToken(ServerWebExchange exchange) {
	/**
         * 前端传递token有两种方式:
         * 1.通过cookie进行传递
         * 2.通过header进行传递
         */
	ServerHttpRequest request = exchange.getRequest();
	MultiValueMap<String, HttpCookie> cookies = request.getCookies();
	HttpCookie httpCookie = cookies.getFirst("token");
	String token = "";
	if (httpCookie != null){
		token = httpCookie.getValue();
	}else{
		token = request.getHeaders().getFirst("token");
	}
	return token;
}

// 重定向到登录页面,通过设置302状态码和在header中添加location实现
private Mono<Void> locationUrl(ServerWebExchange exchange, String path) {
	ServerHttpResponse response = exchange.getResponse();
	response.setStatusCode(HttpStatus.FOUND);  // 设置302状态码
	response.getHeaders().add("location", authUrlProperties.getLoginPageUrl() + "?originUrl=" + path);

	// 清空cookie中的token
	ResponseCookie responseCookie = ResponseCookie.from("token", "1").
		domain(".gmall.com").maxAge(0).path("/").build();
	response.addCookie(responseCookie);
	return response.setComplete();
}

// 用户id透传
private Mono<Void> userIdThrought(String userInfoJson, ServerWebExchange exchange, GatewayFilterChain chain){
	// 获取用户id
	UserInfo userInfo = JSON.parseObject(userInfoJson, UserInfo.class);
	Long userId = userInfo.getId();
	// 重新构建request对象
	ServerHttpRequest request = exchange.getRequest();
	ServerHttpRequest serverHttpRequest = request.mutate().header("userId", userId.toString()).build();
	// 重新构建exchange对象
	ServerHttpResponse response = exchange.getResponse();
	ServerWebExchange serverWebExchange = exchange.mutate().request(serverHttpRequest).response(response).build();
	return chain.filter(serverWebExchange);
}

3.3 普通请求,可以登陆也可以不登录。登录就需要id透传,没有登录放行

String token = getUserToken(exchange);
if (StringUtils.isEmpty(token)){
	return chain.filter(exchange);
}else{
	// 验证token的合法性
	String userInfoJson = redisTemplate.opsForValue().get(GmallConstant.REDIS_USER_LOGIN_PREFIX + token);
	if (!StringUtils.isEmpty(userInfoJson)){
		// 放行,并进行id透传
		return userIdThrought(userInfoJson, exchange, chain);
	}else{
		// 伪造的token
		return locationUrl(exchange, path);
	}
}

在用户信息为空的情况下会发生重定向次数过多的问题。
解决方案:在重定向到登录页面时,删除cookie中token的值
代码实现:

ResponseCookie responseCookie = ResponseCookie.from("token", "1").
                domain(".gmall.com").maxAge(0).path("/").build();
response.addCookie(responseCookie);

4. 用户id透传

问题:微服务如何获取当前登录的用户id?
方案一:在每一个微服务中单独获取Cookie或Header中的token,然后根据token从redis中查询用户信息获得用户id。缺点:在每个微服务中都需要编写相同的代码,繁琐,重复率高。
方案二:在服务网关中获取token,从redis中查询用户信息,并进行用户id透传
方案二代码实现:

private Mono<Void> userIdThrought(String userInfoJson, ServerWebExchange exchange, GatewayFilterChain chain){
        // 获取用户id
        UserInfo userInfo = JSON.parseObject(userInfoJson, UserInfo.class);
        Long userId = userInfo.getId();
        // 重新构建request对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest serverHttpRequest = request.mutate().header("userId", userId.toString()).build();
        // 重新构建exchange对象
        ServerHttpResponse response = exchange.getResponse();
        ServerWebExchange serverWebExchange = exchange.mutate().request(serverHttpRequest).response(response).build();
        return chain.filter(serverWebExchange);
    }
posted @ 2023-09-12 22:49  摆烂ing  阅读(37)  评论(0)    收藏  举报