1.什么是单点登录(SSO)
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。
2.实现思路
总体分为多个web和一个passport项目,web的请求经过过滤器,判断本地是否有已经登录的cookie,有的话去passport验证是否正常,没有则去passport登录,passport检查是否存在已经登录的cookie,有则返回,没有则跳转登录页面完成帐号密码登录,并保存登录信息。
3.实现方案
3.1流程图
3.2关键代码
项目采用springboot+thymeleaf+mysql+redis实现
web项目过滤器:
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.cxb.sso.web.config.WebConfig;
import com.cxb.sso.web.util.HttpClientUtils;
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 过滤器,过滤所有请求,验证是否已经登录
*
* @author baixiaozheng
* @date 2020 -03-14 15:25:06
*/
@Order(1)
@WebFilter(filterName = "passportFilter", urlPatterns = "/*")
public class PassportFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String path = request.getContextPath();
String gotoURL = request.getParameter("gotoURL");
if (gotoURL == null) {
gotoURL = request.getRequestURL().toString();
}
String URL = WebConfig.SSO_SERVICE + "preLogin?setCookieURL=" + request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort() + path + "/setCookie&gotoURL=" + gotoURL;
Cookie cookie = getCookieByName(request, WebConfig.COOKIE_NAME);
if (request.getRequestURI().equals(path + "/logout")) {
doLogout( response, cookie, URL);
} else if (request.getRequestURI().equals(path + "/setCookie")) {
setCookie(request, response);
} else if (cookie != null) {
authCookie(request, response, chain, cookie, URL);
} else {
response.sendRedirect(URL);
}
}
/**
* 设置cookie
* @param request
* @param response
* @throws IOException
*/
private void setCookie(HttpServletRequest request, HttpServletResponse response) throws IOException {
Cookie cookie = new Cookie(WebConfig.COOKIE_NAME, request.getParameter("token"));
cookie.setPath("/");
cookie.setMaxAge(Integer.parseInt(request.getParameter("expiry")));
response.addCookie(cookie);
String gotoURL = request.getParameter("gotoURL");
if (gotoURL != null){
response.sendRedirect(gotoURL);
}
}
/**
* 登出
* @param response
* @param cookie
* @param URL
* @throws IOException
*/
private void doLogout(HttpServletResponse response, Cookie cookie,
String URL) throws IOException {
Map<String, String> params = new HashMap<>();
params.put("cookieName", cookie.getValue());
try {
post(params, "doLogout");
} catch (JSONException e) {
throw new RuntimeException(e);
} finally {
response.sendRedirect(URL);
}
}
/**
* 验证本地存储的cookie是否有效
* @param request
* @param response
* @param chain
* @param cookie
* @param URL
* @throws IOException
* @throws ServletException
*/
private void authCookie(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Cookie cookie,
String URL) throws IOException, ServletException {
Map<String, String> params = new HashMap<>();
params.put("cookieName", cookie.getValue());
try {
JSONObject result = post(params, "authToken");
if (result.getBoolean("error")) {
response.sendRedirect(URL);
} else {
request.setAttribute("username", result.getString("username"));
chain.doFilter(request, response);
}
} catch (JSONException e) {
response.sendRedirect(URL);
throw new RuntimeException(e);
}
}
private JSONObject post(Map<String, String> params, String method) throws JSONException {
String result = HttpClientUtils.sendHttpPostMap(WebConfig.SSO_SERVICE + method, params);
return JSONObject.parseObject(result);
}
/**
* Gets cookie by name.
*
*
* @param request the request
* @param name the name
* @return the cookie by name
*/
private Cookie getCookieByName(HttpServletRequest request, String name) {
Map<String, Cookie> cookieMap = readCookieMap(request);
if (cookieMap.containsKey(name)) {
Cookie cookie = (Cookie) cookieMap.get(name);
return cookie;
} else {
return null;
}
}
private Map<String, Cookie> readCookieMap(HttpServletRequest request) {
Map<String, Cookie> cookieMap = new HashMap<String, Cookie>();
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
cookieMap.put(cookie.getName(), cookie);
}
}
return cookieMap;
}
}
passport控制器:
import com.alibaba.fastjson.JSON;
import com.cxb.sso.passport.config.PassportConfig;
import com.cxb.sso.passport.model.User;
import com.cxb.sso.passport.redis.RedisUtil;
import com.cxb.sso.passport.service.UserService;
import com.cxb.sso.passport.util.DESUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@Controller
public class PassportController {
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserService userService;
@RequestMapping(value = "preLogin")
public String preLogin(HttpServletRequest request, Model model) {
Cookie cookie = getCookieByName(request, PassportConfig.COOKIE_NAME);
String setCookieURL = request.getParameter("setCookieURL");
String gotoURL = request.getParameter("gotoURL");
model.addAttribute("setCookieURL", setCookieURL);
model.addAttribute("gotoURL", gotoURL);
if (cookie == null) {
return "login";
} else {
String encodedToken = cookie.getValue();
String decodedToken = DESUtils.decrypt(encodedToken, PassportConfig.SECRET_KEY);
if (redisUtil.hasKey(decodedToken)) {
User user = JSON.parseObject(redisUtil.get(decodedToken).toString(), User.class);
// 判断token是否存在
if (user != null) {
if (setCookieURL != null) {
return "redirect:" + setCookieURL + "?token=" + encodedToken + "&expiry=" + cookie.getMaxAge() + "&gotoURL=" + gotoURL;
}
}
} else {
return "login";
}
}
return "login";
}
@RequestMapping(value = "authToken")
@ResponseBody
public String authToken(HttpServletRequest request) {
StringBuilder result = new StringBuilder("{");
String encodedToken = request.getParameter("cookieName");
if (encodedToken == null) {
result.append("\"error\":true,\"errorInfo\":\"Token can not be empty!\"");
} else {
String decodedToken = DESUtils.decrypt(encodedToken, PassportConfig.SECRET_KEY);
if(redisUtil.hasKey(decodedToken)) {
User user = JSON.parseObject(redisUtil.get(decodedToken).toString(), User.class);
// 判断token是否存在
if (user != null) {
result.append("\"error\":false,\"username\":").append("\"" + user.getUsername() + "\"");
}
}else {
result.append("\"error\":true,\"errorInfo\":\"Token is not found!\"");
}
}
result.append("}");
return result.toString();
}
@RequestMapping(value = "doLogout")
@ResponseBody
public String doLogout(HttpServletRequest request) {
StringBuilder result = new StringBuilder("{");
String encodedToken = request.getParameter("cookieName");
if (encodedToken == null) {
result.append("\"error\":true,\"errorInfo\":\"Token can not be empty!\"");
} else {
String decodedToken = DESUtils.decrypt(encodedToken, PassportConfig.SECRET_KEY);
redisUtil.del(decodedToken);
result.append("\"error\":false");
}
result.append("}");
return result.toString();
}
@RequestMapping(value = "doLogin")
public String doLogin(HttpServletRequest request, HttpServletResponse response, String username, String password, Model model) {
if (!userService.checkUser(username, password)) {
model.addAttribute("errorInfo","username or password is wrong!");
return "login";
} else {
String token = generateStrRecaptcha(16);
String encodedToken = DESUtils.encrypt(token, PassportConfig.SECRET_KEY);
User user = userService.getByUsernameAndPassword(username, password);
redisUtil.set(token, JSON.toJSON(user), 30 * 60);
addCookie(response, PassportConfig.COOKIE_NAME, encodedToken, PassportConfig.TOKEN_TIMEOUT);
String setCookieURL = request.getParameter("setCookieURL");
String gotoURL = request.getParameter("gotoURL");
return "redirect:" + setCookieURL + "?token=" + encodedToken + "&expiry=" + PassportConfig.TOKEN_TIMEOUT + "&gotoURL=" + gotoURL;
}
}
/**
* 生成随机字符串(含大小写数字)
*/
public static String generateStrRecaptcha(int length) {
Random r = new Random(System.currentTimeMillis());
StringBuffer sf = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = r.nextInt(3);
long result = 0;
switch (number) {
case 0:
result = Math.round(Math.random() * 25 + 65);
sf.append(String.valueOf((char) result));
break;
case 1:
result = Math.round(Math.random() * 25 + 97);
sf.append(String.valueOf((char) result));
break;
case 2:
sf.append(String.valueOf(new Random().nextInt(10)));
break;
}
}
return sf.toString();
}
public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
if (maxAge > 0) {
cookie.setMaxAge(maxAge);
}
response.addCookie(cookie);
}
public Cookie getCookieByName(HttpServletRequest request, String name) {
Map<String, Cookie> cookieMap = readCookieMap(request);
if (cookieMap.containsKey(name)) {
Cookie cookie = (Cookie) cookieMap.get(name);
return cookie;
} else {
return null;
}
}
private Map<String, Cookie> readCookieMap(HttpServletRequest request) {
Map<String, Cookie> cookieMap = new HashMap<String, Cookie>();
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for (Cookie cookie : cookies) {
cookieMap.put(cookie.getName(), cookie);
}
}
return cookieMap;
}
}
登录页面login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Login</title>
<link rel="stylesheet" th:href="@{/css/auth.css}" media="all">
</head>
<body>
<span style="color: red">[[${errorInfo}]]</span>
<div class="lowin">
<div class="lowin-brand">
<img th:src="@{/img/kodinger.jpg}" alt="logo">
</div>
<div class="lowin-wrapper">
<div class="lowin-box lowin-login">
<div class="lowin-box-inner">
<form th:action="@{/doLogin}" method="post">
<input type="hidden" name="action" value="login" />
<input type="hidden" name="gotoURL" th:value="${param.gotoURL}" />
<input type="hidden" name="setCookieURL" th:value="${param.setCookieURL}" />
<p>登录</p>
<div class="lowin-group">
<label>用户名</label>
<input type="text" name="username" class="lowin-input">
</div>
<div class="lowin-group password-group">
<label>密码</label>
<input type="password" name="password" class="lowin-input">
</div>
<button class="lowin-btn login-btn">
登录
</button>
</form>
</div>
</div>
</div>
<div style="position:absolute;right:0px;bottom:0px;">
<footer class="lowin-footer">
Design By @itskodinger.
</footer>
</div>
</div>
</body>
</html>
4.验证
4.1 配置本机hosts文件,添加
127.0.0.1 a.com
127.0.0.1 b.com
127.0.0.1 passport.com