Single-Sign-On即单点登录,从一个页面登录,就可以访问其他互信系统,避免用户重新登录,例如登录了百度,则百度云等其他百度旗下其他网站均可免登录访问。在整个业务服务群中起到了辅助、集成的作用。
SSO体系结构
-
客户端调用模块
-
认证中心
认证中心是SSO系统的核心,它先验证用户是否有权限访问系统,如果有让用户访问系统资源;如果没有,提供一个登录页面。
-
用户管理系统和用户数据获取接口
用于为认证中心提供用户验证数据支持
-
令牌 token
用户验证期间,生成token,保存,如果用户再次登入其他授信系统,则直接验证token信息是否正确以确定用户是否进入资源访问,无需第二次重复登录。
-
整体流程:客户端调用应用系统,通过同一的认证中心进行信息验证,认证中心通过访问数据库接口,进行用户信息核对,生成token允许用户访问应用系统资源。
SSO认证流程
-
获取唯一标识token
-
验证该标识是否存在且有效,若不存在,或者token无效,均重定向到指定登录页,生成token;
-
再次验证,判断用户是否进入系统
SSO系统的必要模块
例如创建一个user表及其对应实体,SSOServer 代码演示,创建服务端loginServlet,用于获取request用户信息、认证用户、生成token操作
@WebServlet("/login")
public class UserLoginServlet extends HttpServlet{
private static final long serialVersionUID = 1L;
private UserAccountService accountService;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//获得原始请求的URL并保存传递,登录成功时,让浏览器再次跳转到该URL
String origURL =req.getParameter("origURL");
req.setAttribute("origURL", origURL);//目的在于在整个会话中,把原始的请求地址保存到req中
req.getRequestDispatcher("WEB-INFO/view/login.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String account=req.getParameter("account");
String password=req.getParameter("password");
String origURL=req.getParameter("origURL");
//按照account查找用户
User user=null;
try{
user=accountService.findUserByAccout(account);
if(user!=null)
{
if(user.getPassword()==password)//.equals(password)
{
//1、生成token java UUID随机生成
String token=UUID.randomUUID().toString().replace("-", "").toLowerCase();
//2、将token存到全局唯一数据结构中
TokenUserData.addToken(token, user);
//3、写入cookie
Cookie tokenCookie=new Cookie("token", token);
tokenCookie.setPath("/");//localhost:8091 同一个域,不同路径;
tokenCookie.setHttpOnly(true);
resp.addCookie(tokenCookie);
//4、跳转请求
if(origURL!=null)
{
origURL="login_success";
}else{
origURL=URLDecoder.decode(origURL,"utf-8");
}
resp.sendRedirect(origURL);
}else
{
backToLoginPage(req, resp, account, origURL, "密码不正确");
}
}else
{
this.backToLoginPage(req, resp, account, origURL, "用户不存在");
}
}catch(Exception e)
{
e.printStackTrace();
backToLoginPage(req, resp, account, origURL, "系统错误");
}
}
private void backToLoginPage(HttpServletRequest req,
HttpServletResponse resp, String account, String origURL,
String errInfo) throws ServletException, IOException {
req.setAttribute("origURL", origURL);
req.setAttribute("account", account);
req.setAttribute("errInfo", errInfo);
req.getRequestDispatcher("/WEB-INF/view/login.jsp").forward(req, resp);
}
SSO客户端模块Filter
public class SSOFilter implements Filter {
// SSO Server登录页面URL
private static final String SSO_LOGIN_URL = "/server/login",
SSO_VALIDATE_URL = <a target=_blank href="http://localhost:8080/server/validate">http://localhost:8080/server/validate</a>;
// 拦截操作
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 从请求中提取token-已从服务端将token信息写入cookie
String token = CookieUtil.getCookie(req, "token");
// 本次请求的完整路径
String origUrl = req.getRequestURL().toString();
String queryStr = req.getQueryString();
if (queryStr != null) {
origUrl += "?" + queryStr;
}
// token 不存在,跳转到SSOServer用户登录页
if (token == null) {
resp.sendRedirect(SSO_LOGIN_URL + "?origUrl="
+ URLEncoder.encode(origUrl, "utf-8"));
} else { // token存在,验证有效性
URL validateUrl = new URL(SSO_VALIDATE_URL + "?token=" + token);
HttpURLConnection conn = (HttpURLConnection) validateUrl
.openConnection();
conn.connect();
InputStream is = conn.getInputStream();
byte[] buffer = new byte[is.available()];
is.read(buffer);
String ret = new String(buffer);
if (ret.length() == 0) { // 返回空字符串,表示 token无效
resp.sendRedirect(SSO_LOGIN_URL + "?origUrl="
+ URLEncoder.encode(origUrl, "utf-8"));
} else {
String[] tmp = ret.split(";");
User user = new User();
for (int i = 0; i < tmp.length; ++i) {
String[] attrs = tmp[i].split("=");
switch (attrs[0]) {
case "id":
user.setId(Integer.parseInt(attrs[1]));
break;
case "name":
user.setName(attrs[1]);
break;
case "account":
user.setAccount(attrs[1]);
break;
}
}
request.setAttribute("user", user);
chain.doFilter(request, response);
}
}
}
客户端调用模块:直接在客户端的web.xml中配置上面SSOClient的Filter,对所有的jsp页面进行拦截;而SSOClient通过从cookie获取服务器写入的token进行认证;并保留生成的token信息,实现单点登录。
<filter>
<filter-name>sso</filter-name>
<filter-class>demo.sso.client.SSOFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sso</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
本文介绍的是同域下单点登录的实现思路和验证流程,接下来会对其他应用场景的SSO应用进行学习,敬请期待。