當一個網站系統比較大型的時候,咱們一般採用面向服務的編程,採用分佈式的編程。各個子系統共同來實現一個大的系統,這時候登陸註冊功能的實現也面臨着一些問題。java
1、WHAT?程序員
SSO是什麼?redis
sso是單點登陸系統,即單獨的一個登陸功能子系統,能夠實現分佈式系統的一次登陸其餘系統免登陸的實現。數據庫
SSO是在多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。編程
它包括能夠將此次主要的登陸映射到其餘應用中用於同一個用戶的登陸的機制(說到底就是提供登陸註冊接口,供其餘系統調用,其餘系統在每次使用的是調用接口,查看登陸狀態,若是登陸了,則不需登陸,更新session的時間,若是沒有登陸提示登陸)json
它是目前比較流行的企業業務整合的解決方案之一。tomcat
2、WHY?安全
當一個系統很大的時候,咱們須要進行服務器的集羣部署,須要多個服務器共同做用。當把一套代碼部署在不一樣的服務器上時,由於分佈式系統是由多個子系統(工程)構成,每一個工程都有本身的獨立的session,而此時若是session不共享,用戶在使用系統的時候一旦跳轉到另一個工程,就提示用戶名登陸,這是個很煩躁的事情。服務器
如上圖,用戶1訪問了購物車信息,登陸了信息,當訪問訂單系統時又提示登陸系統,當訪問安全系統的時候也提示登陸。這是多麼煩躁的事情。session
3、解決方案
針對上述問題的解決方案有兩種:
能夠配置tomcat的session共享。配置tomcat集羣。Tomcat配置好集羣后,會不停的向集羣中其餘的tomcat廣播本身的session信息。其餘的tomcat作session同步。能夠保證全部的tomcatsession中的內容都是一致的。
優勢:不用修改代碼就能夠實現session共享。
缺點:tomcat 是全局session複製,集羣內每一個tomcat的session徹底同步(也就是任什麼時候候都徹底同樣的) 在大規模應用的時候,用戶過多,集羣內tomcat數量過多,session的全局複製會致使集羣性能降低, 所以,tomcat的數量不能太多,5個如下爲好。
不能解決分佈式工程的session共享問題。例如支付寶和淘寶單點登陸的問題。
第二種方案
實現單點登陸系統,提供服務接口。把session數據存放在redis。
Redis能夠設置key的生存時間、訪問速度快效率高。
優勢:redis存取速度快,不會出現多個節點session複製的問題。效率高。
缺點:須要程序員開發。
上圖的意思即:當用戶須要訪問會員中心時,須要訪問論壇時,須要訪問訂單系統時,都須要去調用sso判斷下該用戶是否登陸。
4、HOW?
下面來看下sso系統具體是如何實現的:
(1)校驗、註冊、登陸接口的編寫文檔,詳見博文sso接口文檔
(2)根據接口文檔進行編寫接口的實現。
校驗接口的需求分析:
編碼的分析步驟以下:首先考慮是否有傳入的參數,其次返回值是什麼,第三對哪張表進行操做,第四開始編寫框架層dao,service,controller
接收url中的兩個參數:一個是要校驗的內容,一個是要校驗的數據類型。
type爲類型,可選參數一、二、3分別表明username、phone、email
返回:TaotaoResult。Json格式的數據,須要支持jsonp。
請求的url:http://sso.taotao.com/user/check/{param}/{type}
要查詢的表:
tb_user
單表查詢。可使用逆向工程生成的代碼。
接收兩個參數:內容、內容類型。根據內容類型查詢tb_user表返回Taotaoresult對象。Data屬性值:返回數據,true:數據可用,false:數據不可用
@Service public class UserServiceImpl implements UserService {
@Autowired private TbUserMapper userMapper;
@Override public TaotaoResult checkData(String content, Integer type) { //建立查詢條件 TbUserExample example = new TbUserExample(); Criteria criteria = example.createCriteria(); //對數據進行校驗:一、二、3分別表明username、phone、email //用戶名校驗 if (1 == type) { criteria.andUsernameEqualTo(content); //電話校驗 } else if ( 2 == type) { criteria.andPhoneEqualTo(content); //email校驗 } else { criteria.andEmailEqualTo(content); } //執行查詢 List<TbUser> list = userMapper.selectByExample(example); if (list == null || list.size() == 0) { return TaotaoResult.ok(true); } return TaotaoResult.ok(false); }
} |
從url中接收兩個參數,調用Service進行校驗,在調用Service以前,先對參數進行校驗,例如type必須是一、二、3其中之一。返回TaotaoResult。須要支持jsonp。
@Controller @RequestMapping("/user") public class UserController {
@Autowired private UserService userService;
@RequestMapping("/check/{param}/{type}") @ResponseBody public Object checkData(@PathVariable String param, @PathVariable Integer type, String callback) {
TaotaoResult result = null;
//參數有效性校驗 if (StringUtils.isBlank(param)) { result = TaotaoResult.build(400, "校驗內容不能爲空"); } if (type == null) { result = TaotaoResult.build(400, "校驗內容類型不能爲空"); } if (type != 1 && type != 2 && type != 3 ) { result = TaotaoResult.build(400, "校驗內容類型錯誤"); } //校驗出錯 if (null != result) { if (null != callback) { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue; } else { return result; } } //調用服務 try { result = userService.checkData(param, type);
} catch (Exception e) { result = TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); }
if (null != callback) { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue; } else { return result; } }
} |
請求方法爲post,客戶端提交表單。包含
username //用戶名
password //密碼
phone //手機號
email //郵箱
參數。
接收參數調用mapper向user表中添加記錄。返回taotaoResult對象。若是成功200失敗400異常500.
可使用逆向工程生成代碼
接收TbUser對象,補全user的屬性。向tb_user表插入記錄。返回taoTaoResult。
@Override public TaotaoResult createUser(TbUser user) { user.setUpdated(new Date()); user.setCreated(new Date()); //md5加密 user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes())); userMapper.insert(user); return TaotaoResult.ok(); } |
接收提交的數據用戶名、密碼、電話、郵件。使用pojo接收。使用TbUser。調用Service向表中添加記錄。返回TaotaoResult.
//建立用戶 @RequestMapping("/register") public TaotaoResult createUser(TbUser user) {
try { TaotaoResult result = userService.createUser(user); return result; } catch (Exception e) { return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); } } |
表單的content-type:application/x-www-form-urlencoded; charset=UTF-8
表單的內容:
是一個post請求,包含用戶和密碼。接收用戶名和密碼,到數據庫中查詢,根據用戶名查詢用戶信息,查到以後進行密碼比對,須要對密碼進行md5加密後進行比對。比對成功後說明登陸成功,須要生成一個token可使用UUID。須要把用戶信息寫入redis,key就是token,value就是用戶信息。返回token字符串。
查詢數據庫tb_user表。根據用戶名查詢用戶信息。
接收兩個參數用戶名、密碼。調用dao層查詢用戶信息。生成token,把用戶信息寫入redis。返回token。使用TaotaoResult包裝。
/** * 用戶登陸 * <p>Title: userLogin</p> * <p>Description: </p> * @param username * @param password * @return * @see com.taotao.sso.service.UserService#userLogin(java.lang.String, java.lang.String) */ @Override public TaotaoResult userLogin(String username, String password) {
TbUserExample example = new TbUserExample(); Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(username); List<TbUser> list = userMapper.selectByExample(example); //若是沒有此用戶名 if (null == list || list.size() == 0) { return TaotaoResult.build(400, "用戶名或密碼錯誤"); } TbUser user = list.get(0); //比對密碼 if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(user.getPassword())) { return TaotaoResult.build(400, "用戶名或密碼錯誤"); } //生成token String token = UUID.randomUUID().toString(); //保存用戶以前,把用戶對象中的密碼清空。 user.setPassword(null); //把用戶信息寫入redis jedisClient.set(REDIS_USER_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user)); //設置session的過時時間 jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE); //返回token return TaotaoResult.ok(token); } |
接收表單,包含用戶、密碼。調用Service進行登陸返回TaotaoResult。
//用戶登陸 @RequestMapping(value="/login", method=RequestMethod.POST) @ResponseBody public TaotaoResult userLogin(String username, String password) { try {
TaotaoResult result = userService.userLogin(username, password); return result; } catch (Exception e) { e.printStackTrace(); return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); } } |
根據token判斷用戶是否登陸或者session是否過時。接收token,根據token到redis中取用戶信息。判斷token字符串是否對應用戶信息,若是不對應說明token非法或者session已過時。取到了說明用戶就是正常的登陸狀態。返回用戶信息,同時重置用戶的過時時間。
使用JedisClient實現類。
接收token,調用dao,到redis中查詢token對應的用戶信息。返回用戶信息並更新過時時間。
@Override public TaotaoResult getUserByToken(String token) {
//根據token從redis中查詢用戶信息 String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token); //判斷是否爲空 if (StringUtils.isBlank(json)) { return TaotaoResult.build(400, "此session已通過期,請從新登陸"); } //更新過時時間 jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE); //返回用戶信息 return TaotaoResult.ok(JsonUtils.jsonToPojo(json, TbUser.class)); } |
接收token調用Service返回用戶信息,使用TaotaoResult包裝。
請求的url:
http://sso.taotao.com/user/token/{token}
@RequestMapping("/token/{token}") @ResponseBody public Object getUserByToken(@PathVariable String token, String callback) { TaotaoResult result = null; try { result = userService.getUserByToken(token); } catch (Exception e) { e.printStackTrace(); result = TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); }
//判斷是否爲jsonp調用 if (StringUtils.isBlank(callback)) { return result; } else { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue; }
} |