1.什么是单点登录?
- 单点登录(Single Sign On),简称为 SSO
SSO是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
- 相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。
2.如何实现单点登录
- 我的想法是,创建一个用户微服务,所有的用户认证都在这里进行处理,类似上面说的认证中心

- 服务端保存用户信息VS客户端保存用户信息

- cookie默认是当前域名有效,所以需要扩大cookie作用域

- 流程如下

3.核心代码
- 先整理一下思路流程

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public LoginUserVo login(UserInfo userInfo) {
LambdaQueryWrapper<UserInfo> wrapper = Wrappers.<UserInfo>lambdaQuery();
wrapper.eq(UserInfo::getLoginName, userInfo.getLoginName())
.eq(UserInfo::getPasswd, MD5.encrypt(userInfo.getPasswd()));
UserInfo one = baseMapper.selectOne(wrapper);
if (one == null) {
return null;
}
LoginUserVo loginUserVo = new LoginUserVo();
String token = saveUserAuthenticationInfo(one);
loginUserVo.setNickName(one.getNickName());
loginUserVo.setToken(token);
return loginUserVo;
}
@Override
public String saveUserAuthenticationInfo(UserInfo userInfo) {
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(RedisConstants.USER_LOGIN_PREFIX + token, JsonUtils.toStr(userInfo), 7, TimeUnit.DAYS);
return token;
}
@Override
public void logout(String token) {
removeUserAuthenticationInfo(token);
}
private void removeUserAuthenticationInfo(String token) {
redisTemplate.delete(RedisConstants.USER_LOGIN_PREFIX + token);
}
}
4.现在有一个问题,当我们把用户信息存储在Redis中了,也就是以后每一个需要验证的自愿请求都需要与Redis建立连接查询数据,这有点浪费系统资源了!如何解决呢?
- 解决方案:因为我的所有资源访问都是需要经过网关的,所以我直接在网关层根据请求头的token,去Redis查询,然后校验.成功则将用户详细信息透传到后面的微服务,不成功直接打回前端
- Gateway+数据透传
核心代码:
package com.qbb.qmall.gateway.filter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.qbb.qmall.common.constants.RedisConstants;
import com.qbb.qmall.common.result.Result;
import com.qbb.qmall.common.result.ResultCodeEnum;
import com.qbb.qmall.common.util.IpUtil;
import com.qbb.qmall.common.util.JsonUtils;
import com.qbb.qmall.gateway.properties.AuthProperties;
import com.qbb.qmall.model.user.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
@Slf4j
@Component
public class UserAuthFilter implements GlobalFilter {
public static void main(String[] args) throws InterruptedException {
Flux<Long> just = Flux.interval(Duration.ofMillis(3000));
just.subscribe(System.out::println);
just.subscribe(System.out::println);
just.subscribe(System.out::println);
Thread.sleep(100000);
}
@Autowired
private AuthProperties authProperties;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
log.info("UserAuthFilter 开始拦截,请求路径:{}", request.getURI());
List<String> anyoneurls = authProperties.getAnyoneurls();
for (String anyoneurl : anyoneurls) {
boolean match = antPathMatcher.match(anyoneurl, path);
if (match) {
return chain.filter(exchange);
}
}
List<String> denyurls = authProperties.getDenyurls();
for (String denyurl : denyurls) {
boolean match = antPathMatcher.match(denyurl, path);
if (match) {
Result<String> result = Result.build("", ResultCodeEnum.FORBIDDEN);
String toStr = JsonUtils.toStr(result);
DataBuffer buffer = response
.bufferFactory().wrap(toStr.getBytes(StandardCharsets.UTF_8));
Publisher body = Mono.just(buffer);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(body);
}
}
List<String> authurls = authProperties.getAuthurls();
for (String authurl : authurls) {
boolean match = antPathMatcher.match(authurl, path);
if (match) {
boolean check = validateToken(request);
if (!check) {
return redirectLoginPage(request, response);
}
}
}
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return chain.filter(exchange);
} else {
boolean validateToken = validateToken(request);
if (!validateToken) {
return redirectLoginPage(request, response);
} else {
UserInfo userInfo = getTokenRedisValue(token, IpUtil.getGatwayIpAddress(request));
ServerHttpRequest newRequest = exchange.getRequest().mutate().header("UserId", userInfo.getId().toString()).build();
ServerWebExchange newExchange = exchange
.mutate()
.request(newRequest)
.response(response)
.build();
return chain.filter(newExchange);
}
}
}
private Mono<Void> redirectLoginPage(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FOUND);
String originUrl = request.getURI().toString();
URI uri = URI.create(authProperties.getLoginPath() + "?originUrl=" + originUrl);
response.getHeaders().setLocation(uri);
ResponseCookie cookie = ResponseCookie.from("token", "QIUQIU&LL")
.maxAge(0L)
.domain(".gmall.com")
.build();
response.addCookie(cookie);
return response.setComplete();
}
private boolean validateToken(ServerHttpRequest request) {
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return false;
} else {
String ipAddress = IpUtil.getGatwayIpAddress(request);
UserInfo userInfo = getTokenRedisValue(token, ipAddress);
if (userInfo == null) {
return false;
}
return true;
}
}
private UserInfo getTokenRedisValue(String token, String ipAddress) {
String json = redisTemplate.opsForValue()
.get(RedisConstants.USER_LOGIN_PREFIX + token);
if (StringUtils.isEmpty(json)) {
return null;
}
UserInfo userInfo = JsonUtils.strToObj(json, new TypeReference<UserInfo>() {
});
if (!userInfo.getIpAddress().equals(ipAddress)) {
return null;
}
return userInfo;
}
private String getToken(ServerHttpRequest request) {
String token = "";
HttpCookie cookie = request.getCookies().getFirst("token");
if (cookie != null) {
token = cookie.getValue();
} else {
token = request.getHeaders().getFirst("token");
}
return token;
}
}
app:
auth:
anyoneurls:
- /js/**
- /css/**
- /img/**
- /static/**
denyurls:
- /rpc/inner/**
authurls:
- /order.html
loginPath: http://passport.gmall.com/login.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通