前言
因为涛哥刚进入公司,易哥在开发一个系统的迭代版本。
现在有一个问题是考虑到在分布式部署时,移动端和PC sessionid的不同要实行单点登录和数据的共享。
让我想想该如何实现,首先我就想到的是用redis,然后又去考虑什么异地登录的情况。
结果回来通宵用jwt+redis+cookie集成实现了,刚开始想的也是直接在地址栏输入然后后台界面校验登录状态。
后来发现思维有点混乱,想想倒不如重新想想。
然后自己理清思路,决定用uuid+redis+cookie来实现。
最后也就有了现在的最终版。
做了无数代码修改和程序调试终于成功了,真是功夫不负有心人啊!
单点登录 SSO(Single Sign On)
什么是单点登录?
单点登录的英文名叫做:Single Sign On(简称SSO),指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的系统。简而言之,多个系统,统一登陆。
我们可以这样理解,在一个服务模块登录后,其他模块无需再登录
实现方式
- session广播机制实现(老方法) 当模块较多时,比较浪费资源;数据冗余,存在多份一样的数据 session默认过期时间30分钟
- 基于cookie+redis实现
在项目中任何一个模块登录后,把数据放到两个地方 redis:key:生成唯一随机值(ip、用户id等) value:用户数据 cookie:存放redis生成的key值放到cookie 访问其他模块,发送请求带着cookie进行发送,服务器获取cookie值,在redis中查询,根据key进行查询,如果找到就是登录状态- 分布式session方式实现单点登录
流程运行:(1) 用户第一次登录时,将会话信息(用户Id和用户信息),比如以用户Id为Key,写入分布式Session;(2) 用户再次登录时,获取分布式Session,是否有会话信息,如果没有则调到登录页;(3) 一般采用Cache中间件实现,建议使用Redis,因此它有持久化功能,方便分布式Session宕机后,可以从持久化存储中加载会话信息;(4) 存入会话时,可以设置会话保持的时间,比如15分钟,超过后自动超时;结合Cache中间件,实现的分布式Session,可以很好的模拟Session会话。- token验证
在项目某个模块进行登录,登录之后,按照jwt规则生成字待串,把登录之后用户包含到生成字符串里面,把字符串返回
(1)可以把宁符串通过cookie返回
(2)把字符串通过地址栏返回前端收到token之后将token存储在自己的请求头之中或者url后面,这样每次请求都可以带着token请求。再去访问项目其他模块,获取地址栏或者请求头里面的token,根据宇符串获职用户信息。同时为了设置失效时间,可以将token放在redis中,设置失效时间,判断过期。- CAS 中央认证服务
开发技术
- SpringBoot
- Redis
- Cookie
涛哥单点登录实现流程
- 用户在登录时,登录成功以后由uuid生成token访问令牌
- 将访问令牌存在cookie中,cookie设置有效时间30分钟
- 将用户信息存储在redis里面,设置有效时间30分钟,以key-value形式,token作为key,登录成功后的用户信息作为value
- 访问时通过拦截器拦截请求,获取token是否为null,为null为第一次登录
- 将token拿到redis里面去比对是否存在该键若存在完成登录验证后,放行执行访问请求
实现案例
实现效果:使用nginx做轮询分发请求,在任何一个服务登录成功以后,在访问其他服务时就不需要再去登录
1,首先创建一个boot项目
2,导入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3,配置核心文件
注意这里是分布式就需要启动两个服务,这里就有两个配置文件
server:
port: 8081
## redis
#session存储类型
spring:
application:
name: redis_cookie
redis:
host: 127.0.0.1
port: 6379
#没用就填空
password:
jedis:
pool:
#连接池最大连接数
max-active: 8
#阻塞时间 (负表示没有)
max-wait: -1
#最大空闲连接
max-idle: 8
#最小空闲连接
min-idle: 0
#连接超时时间
timeout: 30000
database: 0
server:
port: 8082
## redis
#session存储类型
spring:
application:
name: redis_cookie
redis:
host: 127.0.0.1
port: 6379
#没用就填空
password:
jedis:
pool:
#连接池最大连接数
max-active: 8
#阻塞时间 (负表示没有)
max-wait: -1
#最大空闲连接
max-idle: 8
#最小空闲连接
min-idle: 0
#连接超时时间
timeout: 30000
database: 0
4,编写用户类
package com.gxhh.redis_cookie.bean;
/**
* @Program: LoginDemo
* @ClassName User
* @Description: 用户类
* @Author: liutao
* @Create: 2022/7/8 16:04
* @Version: 1.0
*/
public class User {
private String username;
private String pwd;
public User() {
}
public User(String username, String pwd) {
this.username = username;
this.pwd = pwd;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
5,编写登录接口和业务逻辑
package com.gxhh.redis_cookie.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxhh.redis_cookie.bean.User;
import com.gxhh.redis_cookie.utils.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @Program: redis_cookie
* @ClassName LoginController
* @Author: liutao
* @Description: 用户登录和测试接口
* @Create: 2022-07-09 19:50
* @Version 1.0
**/
@RestController
public class LoginController {
@Autowired
RedisTemplate redisTemplate;
@Autowired
CookieUtil CookieUtil;
@Value("${server.port}")
String port;
/**
* 登录接口
* @param user User对象
* @return 提示信息
* @throws JsonProcessingException
*/
@PostMapping(value = "/doLogin", produces = "text/html;charset=utf-8")
public String login(HttpServletRequest request, HttpServletResponse response, User user) throws JsonProcessingException,NullPointerException {
System.out.println(user);
ValueOperations ops = redisTemplate.opsForValue();
String s = CookieUtil.getCookieValue(request, "token");
System.out.println("token-->"+s);
if (s!=null&&!s.equals("")){
if (redisTemplate.hasKey(s)) {//登录过
return "重复登录";
} else {
return "信息异常,不允许登录";
}
}else {
if ("sso".equals(user.getUsername()) && "123456".equals(user.getPwd())) {
String uuid = UUID.randomUUID().toString();
CookieUtil.addCookie(response,"token",uuid,60*30);//将token存入cookie设置过期时间,30分钟
ObjectMapper om = new ObjectMapper();
ops.set(uuid,om.writeValueAsString(user));//将凭证存入Redis
redisTemplate.expire(uuid,30, TimeUnit.MINUTES);//设置过期时间,30分钟
return "登录成功";
}
}
return "登录失败";
}
/**
* 退出接口
* @return
* @throws JsonProcessingException
*/
@RequestMapping (value = "/logout", produces = "text/html;charset=utf-8")
public String logout(HttpServletRequest request, HttpServletResponse response, User user) throws JsonProcessingException {
System.out.println(user);
String s = CookieUtil.getCookieValue(request, "token");
if(redisTemplate.delete(s)){
CookieUtil.removeCookie(request,response,"token");
return "成功退出,请登录!";
}else {
return "系统异常!";
}
}
/**
* 测试接口
* @param
* @return
*/
@GetMapping("/hello")
public String hello(){
return "hello 我是端口"+port;
}
}
6,配置WebMVC拦截器,拦截所有请求,只放行登录接口
package com.gxhh.redis_cookie.config;
import com.gxhh.redis_cookie.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Program: redis_cookie
* @ClassName WebMVCConfig
* @Author: liutao
* @Description: WebMVC拦截器
* @Create: 2022-07-09 19:50
* @Version 1.0
**/
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//需要拦截的路径
.excludePathPatterns("/doLogin");//排除/doLogin路径
}
}
7,配置请求拦截器,封装需要使用的CookieUtil工具类
package com.gxhh.redis_cookie.interceptor;
import com.gxhh.redis_cookie.utils.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
/**
* @Program: redis_cookie
* @ClassName LoginInterceptor
* @Author: liutao
* @Description: 用户登录拦截器,校验token,身份验证
* @Create: 2022-07-09 19:50
* @Version 1.0
**/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
RedisTemplate redisTemplate;
@Autowired
CookieUtil CookieUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截到请求:"+request.getRequestURI());
String token = CookieUtil.getCookieValue(request, "token");
Cookie cookie = CookieUtil.getCookie(request,"token");
System.out.println("当前令牌:"+token);
// //先判断token是否失效
if (token==null){
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("请先登录");
return false;
}
System.out.println("登录状态:"+redisTemplate.hasKey(token));
if (redisTemplate.hasKey(token)) {//延长登录状态
redisTemplate.expire(token, 30, TimeUnit.MINUTES);//设置过期时间,30分钟
return true;
}else {//身份过期
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("身份过期,非法请求");
return false;
}
}
}
package com.gxhh.redis_cookie.utils;
import org.springframework.stereotype.Service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Program: redis_cookie
* @ClassName CookieUtil
* @Description: cookie工具类
* @Author: liutao
* @Create: 2022/7/9 4:09
* @Version: 1.0
**/
@Service
public class CookieUtil {
/**
* @Description: 生成指定键值的cookie
* @Param: [request,response,name,value,maxAge]
* @Return:
* @Author: liutao
* @Create: 2022/7/9 21:46
**/
public void addCookie(HttpServletRequest request, HttpServletResponse response, String name, String value, int maxAge){
Cookie[] cookies = request.getCookies();
Cookie cookie = new Cookie(name,value);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
response.addCookie(cookie);
}
/**
* @Description: 通过设置的键值获取对应的Cookie
* @Param: [request,name]
* @Return: javax.servlet.http.Cookie
* @Author: liutao
* @Create: 2022/7/9 21:40
**/
public Cookie getCookie(HttpServletRequest request, String name) {
String cookieValue = null;
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length > 0){
for (Cookie cookie : cookies){
if(cookie.getName().equals(name)) {
return cookie;
}
}
}
return null;
}
/**
* @Description: 根据键值获取存入cookie的值
* @Param: [request,name]
* @Return: java.lang.String
* @Author: liuato
* @Create: 2022/7/9 20:14
**/
public String getCookieValue(HttpServletRequest request,String cookieName) {
String cookieValue = null;
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length > 0){
for (Cookie cookie : cookies){
if(cookie.getName().equals(cookieName)) {
cookieValue = cookie.getValue();
}
}
}
return cookieValue;
}
/**
* @Description: 根据键值移除cookie通过设置有效时间为“0”
* @Param: [request,name]
* @Return: java.lang.String
* @Author: liuato
* @Create: 2022/7/9 20:14
**/
public boolean removeCookie(HttpServletRequest request,HttpServletResponse response,String name){
Cookie cookie = getCookie(request, name);
if(cookie!=null) {
cookie.setMaxAge(0);
response.addCookie(cookie);
return true;
}
return false;
}
}
8,nginx分发轮询配置
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream mysvr{
server localhost:8081;
server localhost:8082;
}
server {
listen 8052;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
# location / {
# root html;
# index index.html index.htm;
# proxy_pass http://localhost:8011;
# }
location / {
# root html;
# index index.html index.htm;
proxy_pass http://mysvr;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
9,启动两个服务服务,以及redis+nginx服务
接下来我们来看看效果
然后在访问测试接口
我们登录系统再看效果
然后在访问测试接口
退出登录后
再次访问测试接口
相关推荐
涛哥博客 - SpringBoot集成redis+cookie实现分布式单点登录
结尾
到这里结束了,涛哥也是第一次接触分布式。欢迎阅读访问,评论,点赞三连。