单点登录SSO
单点登录:SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。
单点登录解决的问题:分布式session的共享问题,简单的说是解决了一个服务器一登陆的问题,实现多功能系统的一次登陆可访问多个服务的问题.
环境的搭配
需要的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
1.redis集群的搭建
2.在java中的配置
server:
port: 10000
spring:
datasource:
username: root
url: jdbc:mysql://localhost:3306/taotao?characterEncoding=utf8&serverTimezone=UTC
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
# redis.cluster集群的节点
cluster:
nodes:
# 这里写你的redis集群的ip和端口号以","分割
jedis:
pool:
# 连接池最大数量
max-active: 10
# 连接池最小空闲连接
min-idle: 1
max-idle: 2
# 连接池最大阻塞时间
max-wait: -1
password: xxxxx
host: redis集群的ip地址
timeout: 1000
commandTimeout: 5000
3.在java中的配置文件
package com.sso.taotaossostudying;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.util.HashSet;
import java.util.Set;
@Configuration
@ConditionalOnClass({JedisCluster.class})
@Component
public class RedisConfig {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.commandTimeout}")
private int commandTimeout;
@Bean
public JedisCluster getJedisCluster() {
String[] cNodes = clusterNodes.split(",");
Set<HostAndPort> nodes = new HashSet<>();
//分割出集群节点
for (String node : cNodes) {
String[] hp = node.split(":");
nodes.add(new HostAndPort(hp[0], Integer.parseInt(hp[1])));
}
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//创建集群对象。没有密码的请使用这一个
// JedisCluster jedisCluster = new JedisCluster(nodes,commandTimeout);
//有密码的请使用这一个。 我这里是redis有密码的所以我使用的这一个
return new JedisCluster(nodes,commandTimeout,commandTimeout,5,password, jedisPoolConfig);
}
}
ps:需要注意的是上面解决了jedis和jediscluster的一些问题得到的配置,redis集群加密的配置.
单点登陆流程图
代码实现方面
登陆界面的实现
@Controller
public class LoginUriController {
@RequestMapping("/user/login")
public String uritoLogin(HttpServletRequest req, @RequestParam(required = false) String url , Model model){
try {
req.setCharacterEncoding("utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println(url);
//把请求的uri放进来用于登陆成功后返回登陆时的页面
model.addAttribute("url",url);
return "login";
}
}
需要注意的是:
url和uri的区别:
1.url在java中指是请求的全路径,uri指的是请求lujing,及handler上面的拦截路径
2.URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。而URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
一定要获得请求的路径
获得请求的路径,通过location.href=xxx.xxx.com,来返回登陆处的功能,用户体验更好一些,
京东,淘宝都是这样做的.以参数的方式来获得从哪里得到发起请求的地址url,然后登陆后重新回到登陆处,比如去购物车去付款发现没登陆账号,然后去登陆,登陆后重新回到支付的页面进行购物.
前端部分
<!DOCTYPE html>
<html lang="en" xmlns:th="http://thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>登陆界面</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<body>
<form action="/login">
用户名: <input type="text" name="username" > </br>
密码: <input type="password" name="password" > </br>
<input type="submit"> <a href="register.html">注册</a>
</form>
</body>
<script>
//注意这里的thymeleaf的取值方式
var url = $("#uri").val();
console.log(url)
function login() {
var data = $("form").serialize();
console.log(data);
$.getJSON("/login",data,function (ret) {
if(ret.status == 200){
if(url == undefined){
location.href = "http://localhost:10001";
}else{
location.href = url;
}
}else{
alert(ret.msg);
}
})
}
$("form").submit( function () {
login();
return false;
} );
</script>
</html>
需要注意的是:
1.注意ajax的使用
2.登陆按钮的禁止跳转
3.前端做非空和非法判断
登陆信息的处理
controller层的处理
@RequestMapping("/login")
public SSOResult login(String username, String password, HttpServletResponse resp){
return loginService.login(username,password,resp);
}
需要注意的是:
要把用户信息保存在session或cookie中,需要用到HttpServletResponse的对象
service层对信息的处理
1.校验用户信息的正确性(即密码的校验需要连接数据库),正确后生成token(UUID)
2.把用户信息封装成对象(可以用md5堆成加密对用户信息加密),然后放到redis中
注意:最好不要把密码放进去,因为上面已经验证过密码的正确性,所以最好就是把用户不隐私的信息放进去即可.token做key,用户对像做value
3.把token放到cookie中即可.
注意:cookie是在分布式的一级域名下,达到系统共享,访问其他服务,能从cookie中拿到token,然后去redis中拿用户信息.
4.需要注意的还有就是cookie和redis的有效期的设置,redis对内存要求比较高,这样的话我们对redis处理应该细致化.cookie中的token能否有效取决于redis中(token,用户对象)是否存在.不存在就失效或连接超时,需要重新登陆.
@Override
public SSOResult login(String username, String password, HttpServletResponse resp) {
/**
* 去数据库查找数据
*/
if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return new SSOResult("用户名或密码不能为空",500);
}
if(StringUtils.isBlank(username)){
return new SSOResult("用户名不能为空",500);
}
if(StringUtils.equals(username,"zahngsan") && !StringUtils.equals(password,"123456")){
return new SSOResult("密码不正确",500);
}
/**
* 登陆成功后封装数据
*/
//设置token
String token = UUID.randomUUID().toString();
UserInfo userInfo= new UserInfo();
userInfo.setNickname("laozhang");
userInfo.setUsername("zhangsan");
String user = JSON.toJSONString(userInfo);
//一般情况下不把password放在redis中,不安全
// userInfo.setPassword("123456");
jedis.setex(token,60,user);
/**
* 将token放入cookie,cookie的domain一般是一级域名下
* 如果不知道domain是什么,启动一下项目,去application中找一下就行
* 只是把token放到cookie中,然后其他服务用到,只能拿token去redis中拿数据,
* 如果redis的keyshixiao了,那就要重新返回登陆界面重新登陆
*/
Cookie tokenCookie = new Cookie("token",token);
tokenCookie.setDomain("localhost");
tokenCookie.setMaxAge(60);
resp.addCookie(tokenCookie);
return new SSOResult("登陆成功",200);
}
总结:这里,面最后的返回码和上边前端相吻合,通过后处理后 返回到登陆时的界面.
到此简单的登陆就做好了,对单点登陆应用是在拦截器中的使用
登陆的效果实现
在门户系统中开登陆的口
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="http://localhost:10000/user/login">登陆</a>
</body>
</html>
服务到sso做上边的登陆处理之后回到门户
比如订单的处理
看订单我们需要有用户信息,然后验证通过后才能看订单,这样我们就要在拦截器中来让用户的看订单请求去sso拿用户信息
请求接口前端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--请求查看订单页面-->
<a href="/order/all" >查看订单</a>
</body>
</html>
拦截这个路径处理用户身份信息,这是我们只有能唯一得到的时存储用户信息的cookie
拦截器的配置
@SpringBootApplication
public class SsoDoorApplication implements WebMvcConfigurer
@Autowired
private OrderInterceptor orderInterceptor;
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(orderInterceptor).addPathPatterns("/order/**");
}
public static void main(String[] args) {
SpringApplication.run(SsoDoorApplication.class, args);
}
}
在拦截器中的请求
@Component
public class OrderInterceptor implements HandlerInterceptor {
private String url = "http://localhost:10000/user/login";
private String getDataUrl = "http://localhost:10000/get_user_by_token";
@Autowired
private RestTemplate restTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StringBuffer requestURL = request.getRequestURL();
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
/**
* 拿到cookie获得token
* 两种结果cookie过期,然后重定向到登陆界面
* token存在,拿着token去sso服务要用户信息
*/
Cookie[] cookies = request.getCookies();
String token = null;
String restUri = url + "?" + requestURL;
if (cookies == null) {
writer.println("<script> alert('你还没登陆,请登陆!');location.href='" + restUri + "'</script>");
} else {
for (Cookie cookie : cookies) {
/**
*拿到token
*/
if (cookie.getName().equals("token")) {
token = cookie.getValue();
break;
} else {
continue;
}
}
}
if (token == null) {
writer.println("<script> alert('会话超时,请重新登陆!');location.href='" + restUri + "'</script>");
return false;
}
/**
* 判断token是否存在
*/
if (token != null) {
/**
* 存在的话就发起请求去sso拿信息
*/
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(getDataUrl+"token="+token);
CloseableHttpResponse resp = httpClient.execute(httpGet);
HttpEntity entity = resp.getEntity();
String userInfo = EntityUtils.toString(entity);
UserInfo user = JSON.parseObject(userInfo, UserInfo.class);
/**
* 两种情况,一种是redis中的key过期了,一种是获得了用户的信息
* 没有获得告诉用户,会话超时请重新登陆
* 正常情况下让它通过去该处理的controller
*/
if (userInfo != null) {
return true;
}else {
writer.println("<script> alert('会话超时,请重新登陆!');location.href='" + url + "'</script>");
return false;
}
}
return false;
}
}
上面需要注意的时情况的划分,然后就是带参数的script语句的拼写
还有就是HttpClient的使用,省时间url直接写上去
获得信息的接口(sso中)
@RequestMapping("/get_user_by_token")
public UserInfo getData(@RequestParam String token){
return loginService.getData(token);
}
service中
/**
* 本文档用于查找用户信息,从redis中查找,如果没有返回null
* @param token
* @return
*/
@Override
public UserInfo getData(String token) {
String user = jedis.get(token);
UserInfo userInfo = JSON.parseObject(user, UserInfo.class);
if(userInfo != null){
return userInfo;
}
return null;
}
所有的处理结束后
zhengque放行,不zhengque就返回就登陆界面
zhengque后的简单处理
@Controller
public class OrderController {
@RequestMapping("/order/all")
public String toOrder(){
return "order";
}
}
前端展示xiaog
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>订单页面</title>
</head>
<body>
<pre>
1.香蕉50斤
2.苹果50斤
3.三只松鼠50包
4.提子50斤
5.瓜子50斤
共计250
</pre>
</body>
</html