基于Sa-Token实现微服务之前的单点登录
修改配置文件,准备好四个域名
127.0.0.1 auth.server.com
127.0.0.1 user.server.com
127.0.0.1 third.server.com
127.0.0.1 eureka.server.com
注册中心:eureka-server服务
pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.1.1</version>
</dependency>
<!--web项目驱动-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot-start-version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.0</version>
</dependency>
配置web项目启动类
/**
* @description: Eureka 服务端注册中心:剔除数据源操作
* @author: GuoTong
* @createTime: 2023-06-26 21:50
* @since JDK 1.8 OR 11
**/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
配置跨域支持和静态资源过滤
/**
* @description: SpringBoot-Web配置
* @author: GuoTong
* @createTime: 2023-06-05 15:37
* @since JDK 1.8 OR 11
**/
@Configuration
public class SpringBootConfig implements WebMvcConfigurer {
/**
* Description: 添加全局跨域CORS处理
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许证书
.allowCredentials(true)
// 设置允许的方法
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
/**
* Description: 静态资源过滤
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//ClassPath:/Static/** 静态资源释放
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
//释放swagger
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
//释放webjars
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
配置文件基础配置
server:
port: 10086
spring:
application:
name: eureka-server
security:
user:
name: eureka
password: eureka
mvc:
static-path-pattern: classpath:/static/**
eureka:
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
register-with-eureka: false #自己不向注册中心注册自己
fetch-registry: false # 自己是注册中心
instance:
hostname: 127.0.0.1
prefer-ip-address: true
启动注册中心验证
准备Sa-Token认证中心server
https://sa-token.cc/doc.html#/ 可以自己参考官方网站定制
引入依赖
knife4j / mysql / mybatis / sa-token / commons / thymeleaf / loadbalancer / bootstrap /eureka-client / lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--Knife4j(增强Swagger)-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--Mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-version}</version>
</dependency>
<!--Mybatis-plus 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.verison}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.verison}</version>
</dependency>
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.33.0</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>1.33.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.33.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--新版的移除了Ribbon的负载策略,所需改用新版的loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>${spring-cloud-starter-version}</version>
</dependency>
<!-- bootstrap最高级启动配置读取 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-starter-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.1</version>
</dependency>
核心Sa-Token的接口
/**
* @description: 统一认证中心 SSO-Server用于对外开放接口:
* @author: GuoTong
* @createTime: 2022-11-26 23:03
* @since JDK 1.8 OR 11
**/
@RestController
public class SsoServerController {
private AuthLoginUserService authLoginUserService;
/**
* Description: 构造器注入
*/
public SsoServerController(AuthLoginUserService authLoginUserService) {
this.authLoginUserService = authLoginUserService;
}
/*
* /*
* SSO-Server端:处理所有SSO相关请求
* http://{host}:{port}/sso/auth -- 单点登录授权地址,接受参数:redirect=授权重定向地址
* http://{host}:{port}/sso/doLogin -- 账号密码登录接口,接受参数:name、pwd
* http://{host}:{port}/sso/checkTicket -- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
* http://{host}:{port}/sso/signout -- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.serverDister();
}
/**
* 配置SSO相关参数
*/
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置:未登录时返回的View
sso.setNotLoginView(() -> new ModelAndView("sa-login.html"));
// 配置:登录处理函数
sso.setDoLoginHandle((name, pwd) -> {
QueryWrapper<AuthLoginUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", name);
queryWrapper.eq("password", pwd);
AuthLoginUser user = authLoginUserService.getOne(queryWrapper);
if (user != null) {
StpUtil.login(user.getId());
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
});
}
}
Http对外接口--简单几个示例
/**
* 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
* <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p>
*
* @author kong
*/
@RestController
public class H5Controller {
@Autowired
private AuthLoginUserService authLoginUserService;
/**
* 获取 redirectUrl
*/
@RequestMapping("/sso/getRedirectUrl")
private Object getRedirectUrl(String redirect, String mode) {
// 未登录情况下,返回 code=401
if (StpUtil.isLogin() == false) {
return SaResult.code(401);
}
// 已登录情况下,构建 redirectUrl
if (SaSsoConsts.MODE_SIMPLE.equals(mode)) {
// 模式一
SaSsoUtil.checkRedirectUrl(SaFoxUtil.decoderUrl(redirect));
return SaResult.data(redirect);
} else {
// 模式二或模式三
String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
return SaResult.data(redirectUrl);
}
}
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
return authLoginUserService.queryUserNameAndPassword(name, pwd);
}
@RequestMapping(value = "isLogin", method = RequestMethod.GET)
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
@RequestMapping(value = "tokenInfo", method = RequestMethod.GET)
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
@RequestMapping(value = "logout", method = RequestMethod.GET)
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
Sa-Token的拦截器,权限
/**
* @description: 获取当前账号权限码集合
* @author: GuoTong
* @createTime: 2022-11-29 20:24
* @since JDK 1.8 OR 11
**/
@Component
@Slf4j
public class StpInterfaceImpl implements StpInterface {
@Autowired
private AuthLoginUserService authLoginUserService;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
AuthLoginUser user = null;
try {
user = authLoginUserService.getById(loginId.toString());
} catch (Exception e) {
log.info("Id无效--->{}", loginId);
}
assert user != null;
String rule = user.getRule();
return Collections.singletonList(rule);
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
AuthLoginUser user = null;
try {
user = authLoginUserService.getById(loginId.toString());
} catch (Exception e) {
log.info("Id无效--->{}", loginId);
}
assert user != null;
String rule = user.getRule();
return Collections.singletonList(rule);
}
}
自定义拦截器,校验Sa-Token登录
/**
* @description: 授权认证:HandlerInterceptor需要注册拦截地址哦!!!
* @author: GuoTong
* @createTime: 2022-11-05 15:40
* @since JDK 1.8 OR 11
**/
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
String requestURI = httpServletRequest.getRequestURI();
// 判断是否是认证中心对外认证接口
if (requestURI.contains("/sso")) {
return true;
}
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有SkipTokenByJWT注释,有则跳过认证
if (method.isAnnotationPresent(SkipTokenByJWT.class)) {
SkipTokenByJWT SkipTokenByJWT = method.getAnnotation(SkipTokenByJWT.class);
if (SkipTokenByJWT.required()) {
return true;
}
}
// 否则需要登陆
if (!StpUtil.isLogin()) {
throw new NotLoginException(ContextCommonMsg.ERROR_MSG_4);
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(NeedTokenByJWT.class)) {
NeedTokenByJWT needTokenByJWT = method.getAnnotation(NeedTokenByJWT.class);
if (needTokenByJWT.required()) {
List<String> permissionList = StpUtil.getPermissionList();
// 查看当前用户是否包含当前接口的权限
if (!permissionList.contains(needTokenByJWT.rule())) {
throw new NotLoginException(ContextCommonMsg.ERROR_MSG_3);
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
全局异常捕获
/**
* @description: 全局异常处理:
* @author: GuoTong
* @createTime: 2022-11-27 11:17
* @since JDK 1.8 OR 11
**/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
log.error("服务执行异常---->{}", e.getMessage());
return SaResult.error(e.getMessage());
}
@ExceptionHandler(value = NotLoginException.class)
public SaResult handlerException(NotLoginException e) {
log.error("没有登陆---->{}", e.getMessage());
return SaResult.error(e.getMessage());
}
@ExceptionHandler(value = SQLException.class)
public SaResult msgMySQLExecuteError(Exception e) {
e.printStackTrace();
log.error("Mysql执行异常");
String message = e.getMessage();
return SaResult.error(message);
}
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public SaResult msgNotFind(Exception e) {
e.printStackTrace();
log.error("请求错误");
String message = e.getMessage();
return SaResult.error("请求内容未传递" + message);
}
}
Springboot配置-跨域-注册拦截器-配置静态资源过滤-RestTemplate负载均衡
/**
* @description: SpringBoot-Web配置
* @author: GuoTong
* @createTime: 2023-06-05 15:37
* @since JDK 1.8 OR 11
**/
@Configuration
public class SpringBootConfig implements WebMvcConfigurer {
/**
* Description: 增加拦截器
*
* @param registry
* @author: GuoTong
* @date: 2022-11-30 13:56:44
* @return:void
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/authLoginUser/**");
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* Description: 添加全局跨域CORS处理
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许证书
.allowCredentials(true)
// 设置允许的方法
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
/**
* Description: 静态资源过滤
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//ClassPath:/Static/** 静态资源释放
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
//释放swagger
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
//释放webjars
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
。。。。其余的东西详见项目。。。。。
https://gitee.com/gtnotgod/sa-token-sso-system.git
演示
认证中心登录页地址
接口管理认证中心用户
A系统USER-SERVICE
依赖
<!--lombok-实体类简化依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${Sa-Token-version}</version>
</dependency>
<!-- Sa-Token 插件:整合SSO -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-sso</artifactId>
<version>${Sa-Token-version}</version>
</dependency>
<!-- Sa-Token 整合redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>${Sa-Token-version}</version>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-alone-redis</artifactId>
<version>${Sa-Token-version}</version>
</dependency>
<!--Open feign 服务间通讯HTTP-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud-starter-version}</version>
</dependency>
<!--Http内置的JDKHttpURLConnection替换为OkHttp-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>${feign-okhttp-version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastJson-version}</version>
</dependency>
<!--web项目驱动-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot-start-version}</version>
</dependency>
<!--Knife4j(增强Swagger)-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- bootstrap最高级启动配置读取 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-starter-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
核心 SSO-Client 端认证接口
/**
* @description: 创建 SSO-Client 端认证接口
* @author: GuoTong
* @createTime: 2022-11-27 15:32
* @since JDK 1.8 OR 11
**/
@RestController
public class SSOClientController {
/*
* SSO-Client端:处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址,接受参数:back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
// 首页
@RequestMapping("/")
public String index() {
String str = "<h2>Sa-Token SSO-Client</h2>" +
"<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">Login</a> " +
"<a href='/sso/logout?back=self'>Logout</a></p>";
return str;
}
}
拦截器和认证中心的AuthenticationInterceptor一致
核心全局异常处理
@ExceptionHandler(value = NotLoginException.class)
public Resp handlerException(NotLoginException e,
HttpServletRequest request,
HttpServletResponse response) {
log.error("没有登陆---->{}", e.getMessage());
try {
response.sendRedirect("/sso/login?back=" + request.getRequestURL());
} catch (IOException ex) {
log.error("转到认证中心失败---->{}", ex.getMessage());
}
return Resp.error(e.getMessage());
}
核心配置
spring:
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password: 123456
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 20
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 10000
# 连接池中的最大空闲连接
max-idle: 3
# 连接池中的最小空闲连接
min-idle: 0
jackson:
date-format: yyyy-MM-dd HH:mm:ss
mvc:
pathmatch:
matching-strategy: ant_path_matcher #Springboot2.6以上需要手动设置
static-path-pattern: classpath:/static/**
main:
allow-bean-definition-overriding: true # 重复定义bean的问题
logging:
level:
root: info
org.springframework: info
# sa-token配置
sa-token:
# SSO-相关配置
sso:
# SSO-Server端 统一认证地址
auth-url: http://localhost:15001/sso/auth
# 是否打开单点注销接口
is-slo: true
# 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
alone-redis:
# Redis数据库索引
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password: 123456
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
forest:
log-enabled: false # 关闭 forest 请求日志打印
。。。。其余的东西详见项目。。。。。
https://gitee.com/gtnotgod/sa-token-sso-system.git
演示
被重定向到认证中心去了
登录访问后
同理准备好B服务THIRD-PARTY-SERVICE
选择注销A服务和B服务的其中一个;前提是没有登录多个账号
注销A服务 http://user.server.com:13601/sso/logout
注销B服务http://third.server.com:14302/sso/logout
A服务feign调用B服务
具体接口代码:
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("/queryOne/{id}")
public Resp<Object> selectOne(@PathVariable("id") String id) {
return Resp.Ok(this.openFeignRPCMySQlService.selectOne(id));
}
fegin接口
/**
* @description: OpenFegin调用third-party-service服务
* @FeignClient(name = "third-party-service") name指定调用的服务名
* @author: GuoTong
* @createTime: 2022-12-02 19:39
* @since JDK 1.8 OR 11
**/
@FeignClient(name = "third-party-service")
public interface OpenFeignRPCMySQlService {
/**
* Description: feign调用third-party-service服务的接口
*
* @author: GuoTong
* @date: 2022-12-02 20:56:28
* @return:
*/
@GetMapping(value = "/hello/getOne2/{id}", produces = "application/json;charset=utf-8")
Resp<Object> selectOne(@PathVariable("id") String id);
}
重定向认证中心
登录A系统
fegin调用成功B系统
A系统已经登录,B系统未登录,访问B系统接口fegin调用A
不再重定向到认证中心,A已经登录,B已完成单点,校验已持有登录状态
注销A系统
B系统再调用本系统的接口
已经重定向到认证中心
fegin调用A系统 :http://third.server.com:14302/hello/queryOne/100011
依旧重定向到认证中心
到此单点登录演示实现完毕;
个人项目地址,在gitee:https://gitee.com/gtnotgod/sa-token-sso-system.git
tip: 更多Sa-Token使用教程参考官方地址:https://sa-token.cc/doc.html#/
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
Java入门到入坟
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!