【SSO】单点登录系统
一、單點登錄系統介紹
對于一個開發項目來說,每個項目都必不可少要有登錄的這個功能。但是隨著項目的變大,變大,再變大。系統可能會被拆分成多個小系統,咱們就拿支付寶和淘寶來說,咱們在淘寶上購物,然后就可以直接連接到自己的支付寶,這個過程不需要我們再次登錄系統,自動就完成了跳轉。這個操作就是小編這次向大家介紹的——單點登錄。
1.1 什么是單點登錄?
SSO英文全稱Single Sign On,單點登錄。SSO是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。它包括可以將這次主要的登錄映射到其他應用中用于同一個用戶的登錄的機制。它是目前比較流行的企業業務整合的解決方案之一。
可以看出,咱們使用了單點登錄系統,在分布式架構APP中,咱們的系統和系統之間的跳轉就可以達到無縫鏈接了。用戶體驗度非常好,用戶根本沒有覺察出是多個系統。這個就達到了我們的目的,即提高了性能,又增加了我們系統之間跳轉的靈活度。
1.2 為什么使用單點登錄
下面小編從一個技術小白做訂餐系統,一步一步演變框架。
首先呢,小編做了一個java Web的項目,這個項目用戶進入訂餐界面,選擇要定的飯后,提交的時候,會判斷是否登錄。如果沒有登錄,就會跳轉到用戶登錄界面,然后進行登錄的判斷;如果已經登錄了,就可以直接提交信息了。
這個小系統非常的簡單,操作也很流暢。但是它有一個適應范圍:非高并發使用流暢。
當我們的這個系統有了很高的并發,就像美團外賣一樣,每天的用戶很多。我們能做的就是把我們的APP多發布到幾個tomcat上,然后通過Nginx反向代理來均分權重。
隨之而來的就是Session共享問題了: 用戶1登錄后,nginx給他分配到tomcat1上,session被存儲在tomcat1上。當下次登錄可能分配到tomcat2上,這樣還需要重新登錄。
解決Session共享的問題:
1. tomcat有一個session同步方案,就是一個傳播機制,打個比方有A B C 3臺tomcat,這3臺tomcat的user信息都在session中并且保持一致,如果其中一臺的user信息變化了,那么就會傳播至另外兩臺,則實現同步,這樣做沒問題,但是僅僅只是在做tomcat集群的時候tomcat很少的時候會用,一旦集群增大,有100臺,那么就互相傳播吧,傳播是需要性能損耗的,那么整個網站的性能就會被拉低,形成網絡風暴。推薦節點數量不要超過5個。
2、分布式架構。拆分成多個子系統。獨立建立一個單點登錄系統,登錄后,把用戶信息存儲到redis,把key值存儲到cookie中,當其他系統需要用戶信息的時候,就可以通過讀取redis中的信息,如果redis中存在,就直接使用。如果不存在就跳轉到單點登錄系統進行登錄。單點登錄系統是使用redis模擬Session,實現Session的統一管理。
二、單點登錄系統的實現
2.1 環境準備
eclipse
redis
2.2 單點登錄流程圖
這個是簡單的單點登錄流程圖,就拿淘寶來說,當我們進入淘寶首頁的時候是沒有登錄的,點擊登錄的時候,會跳轉到用戶登錄界面。此時的用戶登錄界面就是咱們SSO系統的一部分,根據登錄的要求,會接收用戶名和密碼,然后根據用戶名查詢密碼是否正確。
如果不正確就跳轉到登錄頁,提示不正確;
如果正確就要進行以下步驟:
生成一個uuid,作為token;
把用戶信息序列化存儲到redis,存儲的key為token,存儲成功后,返回token;
把token存儲到cookie;
判斷是否有回調url,如果有,跳轉到指定url;如果沒有,跳轉到系統首頁;
2.3 登錄邏輯實現
【DAO層】
單表查詢,可以直接使用Mybatis逆向工程產生的代碼。
【Service層 】
參數:
用戶名:String username
密碼:String password
返回值:E3Result,包裝token
業務邏輯 :
判斷用戶名密碼是否正確。
登錄成功后生成token。Token相當于原來的jsessionid,字符串,可以使用uuid。
把用戶信息保存到redis。Key就是token,value就是TbUser對象轉換成json。
使用String類型保存Session信息??梢允褂谩扒熬Y:token”為key
設置key的過期時間。模擬Session的過期時間。一般半個小時。
返回e3Result包裝token。
public e3Result login(String username, String password) {
// 1、判斷用戶名密碼是否正確。
TbUserExample example = new TbUserExample();
Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
//查詢用戶信息
List<TbUser> list = userMapper.selectByExample(example);
if (list == null || list.size() == 0) {
return e3Result.build(400, "用戶名或密碼錯誤");
}
TbUser user = list.get(0);
//校驗密碼
if (!user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))) {
return e3Result.build(400, "用戶名或密碼錯誤");
}
// 2、登錄成功后生成token。Token相當于原來的jsessionid,字符串,可以使用uuid。
String token = UUID.randomUUID().toString();
// 3、把用戶信息保存到redis。Key就是token,value就是TbUser對象轉換成json。
// 4、使用String類型保存Session信息??梢允褂谩扒熬Y:token”為key
user.setPassword(null);
jedisClient.set(USER_INFO + ":" + token, JsonUtils.objectToJson(user));
// 5、設置key的過期時間。模擬Session的過期時間。一般半個小時。
jedisClient.expire(USER_INFO + ":" + token, SESSION_EXPIRE);
// 6、返回e3Result包裝token。
return e3Result.ok(token);
}
發布服務:
<!-- 使用dubbo發布服務 -->
<!-- 提供方應用信息,用于計算依賴關系 -->
<dubbo:application name="e3-sso"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<!-- 用dubbo協議在20880端口暴露服務 -->
<dubbo:protocol name="dubbo" port="20883"/>
<!-- 聲明需要暴露的服務接口 -->
<dubbo:service interface="cn.e3mall.sso.service.LoginService" ref="loginServiceImpl" timeout="300000"/>
【Controller層】
功能分析:
請求的url:/user/login
請求的方法:POST
參數:username、password,表單提交的數據。可以使用方法的形參接收。
返回值:json數據,使用e3Result包含一個token。
業務邏輯:
接收兩個參數。
調用Service進行登錄。
從返回結果中取token,寫入cookie。Cookie要跨域。
引用服務:
<!-- 引用dubbo服務 -->
<dubbo:application name="e3-sso-web"/>
<dubbo:registry protocol="zookeeper" address="192.168.25.128:2181"/>
<dubbo:reference interface="cn.e3mall.sso.service.TokenService" id="tokenServiceImpl"/>
LoginController:
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@Value("${COOKIE_TOKEN_KEY}")
private String COOKIE_TOKEN_KEY;
@RequestMapping(value="/user/login", method=RequestMethod.POST)
@ResponseBody
public E3Result login(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
// 1、接收兩個參數。
// 2、調用Service進行登錄。
E3Result result = loginService.login(username, password);
// 3、從返回結果中取token,寫入cookie。Cookie要跨域。
String token = result.getData().toString();
CookieUtils.setCookie(request, response, COOKIE_TOKEN_KEY, token);
// 4、響應數據。Json數據。e3Result,其中包含Token。
return result;
}
}
2.4通過token查詢用戶信息
【功能分析】
請求的url:/user/token/{token}
參數:String token需要從url中取。
返回值:json數據。使用e3Result包裝Tbuser對象。
業務邏輯:
從url中取參數。
根據token查詢redis。
如果查詢不到數據。返回用戶已經過期。
如果查詢到數據,說明用戶已經登錄。
需要重置key的過期時間。
把json數據轉換成TbUser對象,然后使用e3Result包裝并返回。
【DAO層】
使用JedisClient對象。
【Service層】
resource.properties:
#SESSION在redis的過期時間 SESSION_EXPIRE=1800
實現類:
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private JedisClient jedisClient;
@Value("${SESSION_EXPIRE}")
private Integer SESSION_EXPIRE;
@Override
public E3Result getUserByToken(String token) {
// 根據token查詢redis
String json = jedisClient.get("SESSION"+token);
if(StringUtils.isBlank(json)){
// 如果查詢不到數據,返回用戶已經過期
return E3Result.build(400, "用戶登錄已經過期,請重新登錄");
}
// 如果查詢到數據,說明用戶已經登錄
// 需要重置key的過期時間
jedisClient.expire("SESSION"+token, SESSION_EXPIRE);
// 把json數據轉換成user對象,然后使用e3Result包裝并返回。
TbUser user = JsonUtils.jsonToPojo(json, TbUser.class);
return E3Result.ok(user);
}
}
【Controller層】
請求的url:/user/token/{token}
參數:String token需要從url中取。
返回值:json數據。使用e3Result包裝Tbuser對象。
Controller
public class TokenController {
@Autowired
private TokenService tokenService;
@RequestMapping("/user/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token,String callback){
E3Result result = tokenService.getUserByToken(token);
// 響應結果之前,判斷是否為jsonp請求
if(StringUtils.isNotBlank(callback)){
// 把結果封裝成一個js語句響應
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
}
}
2.5 首頁展示用戶名
功能分析:
當用戶登錄成功后,在cookie中有token信息。
從cookie中取token根據token查詢用戶信息。
把用戶名展示到首頁。
問題:服務接口在sso系統中。Sso.e3.com(localhost:8088),在首頁顯示用戶名稱,首頁的域名是www.e3.com(localhost:8082),使用ajax請求跨域了。而JS不可以跨域請求數據。
什么是跨域:
域名不同
域名相同,端口號不同
解決js的跨域問題可以使用jsonp。
Jsonp不是新技術,跨域的解決方案。使用js的特性繞過跨域請求。JS可以跨域加載js文件。
2.6 Jsonp原理
2.7 Jsonp實現
【客戶端】
var E3MALL = {
checkLogin : function(){
var _ticket = $.cookie("token");
if(!_ticket){
return ;
}
$.ajax({
url : "http://localhost:8087/user/token/" + _ticket,
dataType : "jsonp",
type : "GET",
success : function(data){
if(data.status == 200){
var username = data.data.username;
var html = username + ",歡迎來到宜立方購物網!<a class="link-logout">[退出]</a>";
$("#loginbar").html(html);
}
}
});
}
}
$(function(){
// 查看是否已經登錄,如果已經登錄查詢登錄信息
E3MALL.checkLogin();
});
【服務端】
功能分析:
接收callback參數,取回調的js的方法名。
業務邏輯處理。
響應結果,拼接一個js語句。
實現方法一:
@RequestMapping(value="/user/token/{token}",produces=MediaType.APPLICATION_JSON_UTF8_VALUE"application/json;charset=utf-8")
@ResponseBody
public String getUserByToken(@PathVariable String token, String callback) {
E3Result result = tokenService.getUserByToken(token);
//響應結果之前,判斷是否為jsonp請求
if (StringUtils.isNotBlank(callback)) {
//把結果封裝成一個js語句響應
return callback + "(" + JsonUtils.objectToJson(result) + ");";
}
return JsonUtils.objectToJson(result);
}
如果spring是4.1以上的版本,可以使用方法二:
@RequestMapping(value="/user/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token, String callback) {
E3Result result = tokenService.getUserByToken(token);
//響應結果之前,判斷是否為jsonp請求
if (StringUtils.isNotBlank(callback)) {
//把結果封裝成一個js語句響應
MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result);
mappingJacksonValue.setJsonpFunction(callback);
return mappingJacksonValue;
}
return result;
}
參考博文:https://blog.csdn.net/kisscatforever/article/details/76409250
總結
以上是生活随笔為你收集整理的【SSO】单点登录系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Filter(过滤器) 和 interc
- 下一篇: css grid布局