目录
- 单点登录 SSO(Single Sign On)
- 什么是单点登录?
- 实现方式
- 开发技术
- 单点登录实现流程
- 实现案例
- 看效果
前言:
由于考虑到cookie的安全性问题,就有了下面这个版本的sso
单点登录 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
- Session
单点登录实现流程
- 用户在登录时,登录成功以后得到当前sessionid
- 将用户信息存储在redis里面,设置有效时间30分钟,以key-value形式,sessionid作为key,登录成功后的用户信息作为value
- 访问时通过拦截器拦截请求,判断当前sessionid是否在redis里,再则延长寿命,不再提示身份过期
- 完成登录验证后,放行执行访问请求
实现案例
实现效果:使用nginx做轮询分发请求,在任何一个服务登录成功以后,在访问其他服务时就不需要再去登录
- 1,首先创建一个boot项目
- 2,导入pom依赖
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-redis</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.session</groupId> | |
<artifactId>spring-session-data-redis</artifactId> | |
</dependency> |
3,配置核心文件
注意:这里只有一个配置文件,若想启动两个端口。就在编辑页面的 VM OPTIONS 里配置如下 -Dserver.port=8082
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 |
4,编写用户类
package com.gxhh.redis_session.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_session.web; | |
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.gxhh.redis_session.bean.User; | |
import com.gxhh.redis_session.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 | |
**/ | |
public class LoginController { | |
RedisTemplate redisTemplate; | |
CookieUtil CookieUtil; | |
String port; | |
/** | |
* 登录接口 | |
* @param user User对象 | |
* @return 提示信息 | |
* @throws JsonProcessingException | |
*/ | |
public String login(HttpServletRequest request, HttpServletResponse response, User user) throws JsonProcessingException { | |
System.out.println(user); | |
ValueOperations ops = redisTemplate.opsForValue(); | |
String s = request.getSession().getId(); | |
if (redisTemplate.hasKey(s)) {//登录过 | |
return "重复登录"; | |
} else {//未登录 | |
if ("sso".equals(user.getUsername()) && "123456".equals(user.getPwd())) { | |
ObjectMapper om = new ObjectMapper(); | |
ops.set(s, om.writeValueAsString(user));//将凭证存入Redis | |
redisTemplate.expire(s, 30, TimeUnit.MINUTES);//设置过期时间,30分钟 | |
return "登录成功"; | |
}else { | |
return "登录失败!"; | |
} | |
} | |
} | |
/** | |
* 退出接口 | |
* @return | |
* @throws JsonProcessingException | |
*/ | |
"/logout", produces = "text/html;charset=utf-8") | (value =|
public String logout(HttpServletRequest request, HttpServletResponse response, User user) throws JsonProcessingException { | |
System.out.println(user); | |
if(redisTemplate.delete(request.getSession().getId())){ | |
request.getSession().invalidate(); | |
return "成功退出,请登录!"; | |
}else { | |
return "系统异常!"; | |
} | |
} | |
/** | |
* 测试接口 | |
* @param | |
* @return | |
*/ | |
public String hello(){ | |
return "hello 我是端口"+port; | |
} | |
} |
6,配置WebMVC拦截器,拦截所有请求,只放行登录接口
package com.gxhh.redis_session.config; | |
import com.gxhh.redis_session.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 | |
**/ | |
public class WebMVCConfig implements WebMvcConfigurer { | |
LoginInterceptor loginInterceptor; | |
public void addInterceptors(InterceptorRegistry registry) { | |
registry.addInterceptor(loginInterceptor) | |
.addPathPatterns("/**")//需要拦截的路径 | |
.excludePathPatterns("/doLogin","/login.html") ;//排除/doLogin路径 | |
} | |
} |
7,配置请求拦截器
package com.gxhh.redis_session.interceptor; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.gxhh.redis_session.utils.CookieUtil; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.data.redis.core.RedisTemplate; | |
import org.springframework.data.redis.core.ValueOperations; | |
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: 用户登录拦截器,校验session,身份验证 | |
* @Create: 2022-07-09 19:50 | |
* @Version 1.0 | |
**/ | |
public class LoginInterceptor implements HandlerInterceptor { | |
RedisTemplate redisTemplate; | |
CookieUtil CookieUtil; | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
System.out.println("拦截到请求:"+request.getRequestURI()); | |
System.out.println("当前令牌:"+request.getSession().getId()); | |
String s = request.getSession().getId(); | |
System.out.println("登录状态:"+redisTemplate.hasKey(s)); | |
if (redisTemplate.hasKey(s)) {//延长登录状态 | |
redisTemplate.expire(s, 30, TimeUnit.MINUTES);//设置过期时间,30分钟 | |
return true; | |
}else {//身份过期 | |
response.setContentType("text/html;charset=utf-8"); | |
PrintWriter out = response.getWriter(); | |
out.write("身份过期,非法请求"); | |
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; | |
#} | |
} |
看效果
先访问测试接口:
然后再登录:
访问测试接口:
关闭浏览器后访问: