首先贴出国内大神的开源SSO框架 KISSO
http://git.oschina.net/baomidou/kisso
废话不多说,开始~
贴一张KISSO文档上的原理图
其实就是用户在登录业务系统的时候。看一下本地是否有Cookie
如果没有Cookie。访问SSO项目。SSO也没有Cookie的话。
进行登录。登录成功将加密的Token写入Cookie
回传给业务系统。通过几次的加密。认证,双方认证OK后
在业务系统域名下写入Cookie。完成登录
一、KISSO服务器端集成(springMVC+redis)
1、maven依赖增加
<!-- kisso begin -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kisso</artifactId>
<version>3.6.10</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk14</artifactId>
<version>1.50</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.46</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.0</version>
</dependency>
<!-- kisso end -->
2、web.xml(这里使用过滤器方式,不使用spring-mvc拦截器方式)
over.url:这里参数为不需要过滤器过滤的方法。
<!-- kisso -->
<context-param>
<param-name>kissoConfigLocation</param-name>
<param-value>classpath:properties/sso.properties</param-value>
</context-param>
<listener>
<listener-class>com.baomidou.kisso.web.KissoConfigListener</listener-class>
</listener>
<!-- SSOFilter -->
<filter>
<filter-name>SSOFilter</filter-name>
<filter-class>com.baomidou.kisso.web.filter.SSOFilter</filter-class>
<init-param>
<param-name>over.url</param-name>
<param-value>/phoneSms;/PhoneSmsCode;</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SSOFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
其实就是看本地有没有Cookie 。没有就跳转到SSO项目
public class SSOClientFilter implements Filter {
private static final Logger logger = Logger.getLogger("SSOFilter");
private static String OVERURL = null;
public SSOClientFilter() {
}
public void init(FilterConfig config) throws ServletException {
OVERURL = config.getInitParameter("over.url");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
boolean isOver = HttpUtil.inContainURL(req, OVERURL);
if(!isOver) {
Token token = SSOHelper.getToken(req);
if(token == null) {
logger.fine("logout. request url:" + req.getRequestURL());
SSOProperties prop = SSOConfig.getSSOProperties();
String retStr = prop.get("sso.defined.proxyloginurl");
// String retUrl = HttpUtil.getQueryString(req, "UTF-8");
// this.logger.fine("loginAgain redirect pageUrl.." + retUrl);
res.sendRedirect(HttpUtil.encodeRetURL(prop.get("sso.login.url"), "ReturnURL", retStr));
return;
}
SsoUser user = (SsoUser) req.getSession().getAttribute(UserConstants.LOGIN_USER);
if(user == null){
SSOHelper.logout(req,res);
return;
}
req.setAttribute("SSOTokenAttr", token);
}
chain.doFilter(request, response);
}
public void destroy() {
OVERURL = null;
}
}
3、KISSO的配置文件 sso.properties
这里具体就不贴出来了。 查阅一下文档写的非常详细
################ SSOConfig file #################
sso.role=应用名
sso.secretkey=秘钥
sso.cookie.domain=cookie存储的位置
sso.login.url=登录的url
#cookie setting
# crossdomain secretkey
sso.authcookie.secretkey=跨域对称加密的秘钥
#cache
sso.cache.class=缓存实现类。实现SSOCache接口
sso.cache.expires=有效时间
# userConfig defined
sso.defined.my_public_key=业务系统的公钥(校验业务系统)
sso.defined.sso_private_key=SSO系统的私钥(加密使用)
4、服务端控制层
@Controller
public class SsoController {
private static Logger log = Logger.getLogger(SsoController.class);
@Autowired
private SsoUserService ssoUserService;
protected String redirectTo(String url) {
StringBuffer rto = new StringBuffer("redirect:");
rto.append(url);
return rto.toString();
}
/**
* 登录 (注解跳过权限验证)
*/
@Login(action = Action.Skip)
@RequestMapping("/login")
public String login(RedirectAttributesModelMap modelMap, Model model, HttpServletRequest request, HttpServletResponse response) {
log.info("登录 (注解跳过权限验证)");
String returnUrl = request.getParameter(SSOConfig.getInstance().getParamReturl());
Token token = SSOHelper.getToken(request);
if (token == null) {
/**
* 正常登录 需要过滤sql及脚本注入
*/
WafRequestWrapper wr = new WafRequestWrapper(request);
String loginUser = wr.getParameter("loginUser");
String loginPass = wr.getParameter("loginPass");
//定义校验失败标识符
boolean falg = false;
//定义校验失败的字符串
String falgStr = "";
if (loginUser != null && !"".equals(loginUser)) {
SsoUser user = ssoUserService.findByName(loginUser);
if(user != null){
if(user.getPassword().equals(Md5Util.MD5Encode(loginPass))){
/*
* 设置登录 Cookie
* 最后一个参数 true 时添加 cookie 同时销毁当前 JSESSIONID 创建信任的 JSESSIONID
*/
SSOToken st = new SSOToken(request, user.getId()+"");
// st.setData("jjc看源码哦");
//记住密码就设置
//SSOConfig.getInstance().setCookieMaxage(604800);
SSOHelper.setSSOCookie(request, response, st, true);
}else{//证明密码不匹配
falg = true;
falgStr = "密码不正确";
}
}else{//证明没有用户名
falg = true;
falgStr = "用户名不正确";
}
} else {
falg = true;
falgStr = "";
}
if(falg){//校验失败。跳转回登录页
model.addAttribute("msg", falgStr);
if (StringUtils.isNotEmpty(returnUrl)) {
model.addAttribute("ReturnURL", returnUrl);
}
model.addAttribute("loginUser", loginUser);
model.addAttribute("loginPass", loginPass);
return "login";
}else{//校验成功,重定向到业务系统
// 重定向到指定地址 returnUrl
if (StringUtils.isEmpty(returnUrl)) {
returnUrl = "/index.html";
} else {
returnUrl = HttpUtil.decodeURL(returnUrl);
}
return redirectTo(returnUrl);
}
} else {
if (StringUtils.isEmpty(returnUrl)) {
returnUrl = "/index.html";
}
return redirectTo(returnUrl);
}
}
@ResponseBody
@RequestMapping("/replylogin")
public void replylogin(HttpServletRequest request, HttpServletResponse response) {
log.info("SSO回复子系统");
StringBuffer replyData = new StringBuffer();
replyData.append(request.getParameter("callback")).append("({\"msg\":\"");
Token token = SSOHelper.getToken(request);
if (token != null) {
String askData = request.getParameter("askData");
if (askData != null && !"".equals(askData)) {
/**
*
* 用户自定义配置获取
*
* <p>
* 由于不确定性,kisso 提倡,用户自己定义配置。
* </p>
*
*/
SSOProperties prop = SSOConfig.getSSOProperties();
//下面开始验证票据,签名新的票据每一步都必须有。
AuthToken at = SSOHelper.replyCiphertext(request, askData);
if (at != null) {
//1、业务系统公钥验证签名合法性(此处要支持多个跨域端,取 authToken 的 app 名找到对应系统公钥验证签名)
at = at.verify(prop.get("sso.defined." + at.getApp() + "_public_key"));
if (at != null) {
//at.getUuid() 作为 key 设置 authToken 至分布式缓存中,然后 sso 系统二次验证
//at.setData(data); 设置自定义信息,当然你也可以直接 at.setData(token.jsonToken()); 把当前 SSOToken 传过去。
at.setUid(token.getUid());//设置绑定用户ID
at.setTime(token.getTime());//设置登录时间
//2、SSO 的私钥签名
at.sign(prop.get("sso.defined.sso_private_key"));
//3、生成回复密文票据
replyData.append(at.encryptAuthToken());
} else {
//非法签名, 可以重定向至无权限界面,自己处理
replyData.append("-2");
}
} else {
//非法签名, 可以重定向至无权限界面,自己处理
replyData.append("-2");
}
}
} else {
// 未登录
replyData.append("-1");
}
try {
replyData.append("\"})");
AjaxHelper.outPrint(response, replyData.toString(), "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 统一退出,调用对外提供退出的所有接口
*/
@RequestMapping("/logout")
public String logout(HttpServletRequest request,HttpServletResponse response) {
SSOHelper.clearLogin(request, response);
return "logout";
}
/**
*/
@RequestMapping("/index")
public String index(HttpServletRequest request,HttpServletResponse response) {
return "index";
}
}
5、redis集成(退出使用,具体请查阅官方文档)
public class SSORedisCaChe implements SSOCache {
StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) WebApplicationContextHelper.getBean("stringRedisTemplate");
@Override
public Token get(String s, int i) {
if(exists(s)){
String result = null;
ValueOperations operations = stringRedisTemplate.opsForValue();
result = (String)operations.get(s);
result = result.replaceAll("\u0000" , "");
Token token = SSOConfig.getInstance().getParser().parseObject(result, Token.class);
return token;
}
return null;
}
@Override
public boolean set(String s, Token token, int i) {
boolean result = false;
try {
delete(s);
ValueOperations valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(s,token.jsonToken(),i);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@Override
public boolean delete(String s) {
boolean result = false;
try{
if (exists(s)) {
stringRedisTemplate.delete(s);
return true;
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
public boolean exists(final String key) {
return stringRedisTemplate.hasKey(key);
}
}
二、客户端集成
1、maven一致
2、web.xml
SSOClientFilter 客户端自定义的过滤器。模仿kisso的过滤器
<!-- kisso -->
<context-param>
<param-name>kissoConfigLocation</param-name>
<param-value>classpath:properties/sso.properties</param-value>
</context-param>
<listener>
<listener-class>com.baomidou.kisso.web.KissoConfigListener</listener-class>
</listener>
<!-- SSOFilter -->
<filter>
<filter-name>SSOClientFilter</filter-name>
<filter-class>com.hc360.yunxin.filter.SSOClientFilter</filter-class>
<init-param>
<param-name>over.url</param-name>
<param-value>/login;/verify;/resources/;/code;/proxylogin;/oklogin;/accountsecurity/mailboxBound;/accountsecurity/mailboxBoundUpdate</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SSOClientFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、sso.properties
################ SSOConfig file #################
sso.role=业务系统名字 , 拼接使用
sso.secretkey=加密秘钥
sso.cookie.domain=写入cookie地址
sso.login.url=登录url-》跳转SSO
sso.logout.url=登出url
# crossdomain secretkey
sso.authcookie.secretkey=
#cache
sso.cache.class=缓存
sso.cache.expires=失效时间 秒单位
# userConfig defined
sso.defined.proxyloginurl=sso回复客户端的地址
sso.defined.askurl=sso认证地址
sso.defined.oklogin=客户端写入cookie的action
sso.defined.clientIndex=客户端自己的主页
sso.defined.clientTimeout=自定义超时链接
sso.defined.my_private_key=业务系统的私钥
sso.defined.my_public_key=业务系统的公钥
sso.defined.sso_public_key=sso系统的公钥
4、客户端Controller
@Controller
public class SsoController {
@Autowired
private SsoUserService ssoUserService;
protected String redirectTo(String url) {
StringBuffer rto = new StringBuffer("redirect:");
rto.append(url);
return rto.toString();
}
@RequestMapping("/index")
public String index(Model model, HttpServletRequest request,HttpServletResponse response) throws UnsupportedEncodingException {
Token token = SSOHelper.getToken(request);
response.setCharacterEncoding("UTF-8");
request.setCharacterEncoding("UTF-8");
if (token == null) {
/**
* 重定向至代理跨域地址页
*/
return redirectTo("http://sso.test.com:8081/kisso_crossdomain_sso/login.html?ReturnURL=http%3A%2F%2Fmy.web.com%3A8082%2F/kisso_crossdomain_my/proxylogin.html");
} else {
// model.addAttribute("userId", token.getUid());
SsoUser SessionssoUser = (SsoUser) request.getSession().getAttribute(UserConstants.LOGIN_USER);
model.addAttribute("user", ssoUserService.findByID(SessionssoUser.getId()));
}
return "index";
}
/**
* 跨域登录
*/
@RequestMapping("/proxylogin")
public String proxylogin(Model model,HttpServletRequest request,HttpServletResponse response) {
/**
*
* 用户自定义配置获取
*
* <p>
* 由于不确定性,kisso 提倡,用户自己定义配置。
* </p>
*
*/
SSOProperties prop = SSOConfig.getSSOProperties();
//业务系统私钥签名 authToken 自动设置临时会话 cookie 授权后自动销毁
AuthToken at = SSOHelper.askCiphertext(request, response, prop.get("sso.defined.my_private_key"));
//at.getUuid() 作为 key 设置 authToken 至分布式缓存中,然后 sso 系统二次验证
//askurl 询问 sso 是否登录地址
model.addAttribute("askurl", prop.get("sso.defined.askurl"));
//askTxt 询问 token 密文
model.addAttribute("askData", at.encryptAuthToken());
//my 确定是否登录地址
model.addAttribute("okurl", prop.get("sso.defined.oklogin"));
return "proxylogin";
}
/**
* 跨域登录成功
*/
@ResponseBody
@RequestMapping("/oklogin")
public void oklogin( HttpServletRequest request,HttpServletResponse response) {
SSOProperties prop = SSOConfig.getSSOProperties();
//String returl = prop.get("sso.defined.clientTimeout");
String returl =prop.get("sso.logout.url");
/*
* <p>
* 回复密文是否存在
* </p>
* <p>
* SSO 公钥验证回复密文是否正确
* </p>
* <p>
* 设置 业务系统自己的 Cookie
* </p>
*/
String replyTxt = request.getParameter("replyTxt");
if (replyTxt != null && !"".equals(replyTxt)) {
AuthToken at = SSOHelper.ok(request, response, replyTxt, prop.get("sso.defined.my_public_key"),
prop.get("sso.defined.sso_public_key"));
if (at != null) {
returl = prop.get("sso.defined.clientIndex");
SSOToken st = new SSOToken();
st.setUid(at.getUid());
st.setTime(at.getTime());
// st.setData(at.getData());
/*
* 设置 true 时添加 cookie 同时销毁当前 JSESSIONID 创建信任的 JSESSIONID
*/
SSOHelper.setSSOCookie(request, response, st, true);
//设置完Cookie 设置Session
request.getSession().setAttribute(UserConstants.LOGIN_USER, ssoUserService.findByID(Integer.parseInt(at.getUid())));
}
}
try {
AjaxHelper.outPrint(response, "{\"returl\":\"" + returl + "\"}", "UTF-8");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 跨域登录超时
*/
@RequestMapping("/timeout")
public String timeout() {
return "timeout";
}
/*
*
* 如果实现 SSOCache 缓存, kisso 自动缓存 token 退出只需要 SSOHelper.clearLogin(request, response);
*
* 自动清理 token 缓存信息, 同时各个系统都会自动退出。 建议这么!!退出更优雅。。。
*
* --------------- 悲剧的开启 ---------------
*
* 如果你不这么干那么您只能挨个不同域退出一遍,最终全站退出。
*
*/
@RequestMapping("/logout")
public String logout( HttpServletRequest request,HttpServletResponse response) {
/**
* <p>
* SSO 退出,清空退出状态即可
* </p>
*
* <p>
* 子系统退出 SSOHelper.logout(request, response); 注意 sso.properties 包含 退出到
* SSO 的地址 , 属性 sso.logout.url 的配置
* </p>
*/
SSOHelper.clearLogin(request, response);
return "redirect:/index";
}
}
5、redis与服务一致
6、客户端发送跨域,请求SSO认证的静态页面
<body>
<script type="text/javascript">
function proxyLogin(askurl, askData, okurl) {
var killAjax = true;
// setTimeout(function() {
// checkajaxkill();
// }, 30000);
var ajaxCall = jQuery.getJSON(askurl + "?callback=?", {askData:askData}, function(d){
killAjax = false;
if(d.msg == "-1"){
window.location.href = SSOLoginUrl;
}else{
jQuery.post(okurl, {replyTxt:d.msg} , function(e) {
window.location.href = e.returl;
}, "json");
}
});
// function checkajaxkill(){
// if(killAjax){
// ajaxCall.abort();
// window.location.href = SSOTimeout;
// }
// }
}
proxyLogin("$!{askurl}", "$!{askData}", "$!{okurl}");
</script>
<div align="center" style="margin-top: 180px;">
<img src="resources/img/loading.gif"> 页面正在加载中,请稍候……
</div>
</body>
登录总结流程:
1、用户访问业务系统,通过自定义拦截器跳转到SSO系统进行认证。
2、在SSO系统登陆成功后。进行浏览器重定向
3、重定向到业务系统的Controller
4、业务系统的Controller将自己的私钥加密临时会话Cookie信息,发送给SSO进行认证。(认证时需要使用JSONP方式跨域发送请求 KISSO框架处理方式)
5、SSO系统首先会看看自己本地是否有Cookie,如果有的话将加密信息进行解密,拿到信息中业务系统的名字,拼接配置文件的配置。拿到业务系统公钥进行校验。 成功后在使用SSO系统的私钥进行加密信息返回
6、JSONP消息拿到后,使用业务系统的公钥以及SSO系统的公钥校验回传的信息是否正确。无问题后。将本地设置好Cookie uid 并跳转业务系统
登出流程总结:
根据KISSO的接口自定义Cache通过集成Redis保存加密Token
通过以下方法进行登出
SSOHelarLogin(request, response);
原理:
将Redis中的数据清除后,其他系统在操作时通过KISSO的过滤器会通过自定义cache读取缓存信息,如数据清除。将清除其他系统本地的Cookie。