首先,在搞代碼之前,我們要了解什麼是JWT?
這個一開始我也不太明白,後來經過百度閱讀看了很多文章才明白,其實JWT就像一個工具,可以對字符串進行簽名(加密)和解籤(解密)以及驗證字符串。這裏只是簡單的對JWT主要功能描述一下就行,更具體的瞭解JWT需要自行百度。
第二:Token爲什麼要存放在Redis?存儲格式又是怎麼樣的?
當用戶登錄成功時,將Token放在Redis,同時設置該Redis key的過期時間 = JWT Token過期時間,那麼等這個存放某Token的key過期之後,Redis會自動刪除這個Redis key(那我就不用刻意的維護Token)。
第三:SSO登陸邏輯(這是根據自己的實際項目繪製的邏輯圖,若有問題勿噴。嘿嘿!)
SSO註銷登錄邏輯:
效果展示:
1.子系統A點擊登錄:
SSO驗證登錄成功後,將token存到Redis,如圖示例:
然後SSO使用ajax(jsonp)發送命令給所有子系統,讓他們添加token到本地Cookie中,然後重定向回子系統
如圖:登陸成功
查看Cookie有沒有Token?
子系統A(10.1.8.12)
子系統B(10.1.0.192);
以上是在子系統A(10.1.8.12)上完成了登錄成功,根據SSO的定義,那麼,我訪問子系統B(10.1.0.192)就應該是免登錄的,接下來驗證一下,訪問子系統B(10.1.0.192)
可以看到,在子系統A上登錄之後,在子系統B上就免登陸了,也就是成功實現了跨域SSO單點登陸
功能演示到此完畢。接下來看看我是如何實現SSO跨域單點登錄的。
1.子系統訪問受保護資源,子系統攔截器攔截請求,如下:
public class LoginFilter implements Filter {
private final Gson gson = new Gson();
String NBC_SSO_LOGIN_URL = PropertiesUtils.getProperty("NBC_SSO_SERVER_LOGIN_URL");
String TokenLogin_HttpURL = PropertiesUtils.getProperty("NBC_SSO_SERVER_HTTPURL_FOR_TOKEN_CHECK");
String NBC_USER_TOKEN = PropertiesUtils.getProperty("NBC_SSO_CLIENT_COOKIENAME_FOR_TOKEN");
String NBC_SSO_CLIENT_COOKIE_TIMEOUT = PropertiesUtils.getProperty("NBC_SSO_CLIENT_COOKIE_TIMEOUT");
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
System.out.println("============ doFilter LoginFilter ============");
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
HttpSession session = request.getSession();
ServletContext application = session.getServletContext();
boolean IsLogin = false;
Object UID = session.getAttribute("UID");
if(UID!=null && application.getAttribute(UID.toString())!=null && application.getAttribute(UID.toString()).equals(session.getId())) {
IsLogin = true;
}
if(UID!=null) {
System.err.println("=============application uid:"+application.getAttribute(UID.toString()));
}
System.err.println("IsLogin:"+IsLogin);
//已登陸則放行
if(IsLogin) {
chain.doFilter(request, response);
}else {
//校驗配置文件
boolean error = false;
StringBuffer errorInfo = new StringBuffer("");;
String errorURL = request.getContextPath() +"/error/commonError";
String[] arr1 = new String[] {NBC_USER_TOKEN,NBC_SSO_LOGIN_URL,NBC_SSO_CLIENT_COOKIE_TIMEOUT};
String[] arr2 = new String[] {"NBC_SSO_CLIENT_COOKIENAME_FOR_TOKEN","NBC_SSO_SERVER_LOGIN_URL","NBC_SSO_CLIENT_COOKIE_TIMEOUT"};
for(int i=0;i<arr1.length;i++) {
if(arr1[i]==null || arr1[i].equals("")) {
error = true;
errorInfo.append(errorInfo.toString().trim().equals("")?arr2[i]:("※"+arr2[i]));
}
}
if(error) {
session.setAttribute("error", errorInfo);
response.sendRedirect(errorURL);
}
//配置SSO登陸鏈接
String returnRUL = request.getRequestURL().toString();
String ssoLoginUrl = "";
try {
if(returnRUL!=null && !returnRUL.equals("")) {
returnRUL = URLEncoder.encode(returnRUL, "UTF-8");
if(NBC_SSO_LOGIN_URL.indexOf("?")>0) {
ssoLoginUrl = NBC_SSO_LOGIN_URL+"&returnRUL="+returnRUL;
}else {
ssoLoginUrl = NBC_SSO_LOGIN_URL+"?returnRUL="+returnRUL;
}
}
}catch(Exception e) {
e.printStackTrace();
}
System.err.println("登陸攔截");
//若請求中有token參數,則校驗token有效性
String token = req.getParameter("token");
if(token!=null && !token.equals("")) {
SSOCheckResult checkResult = SsoCheck.checkToken(token, TokenLogin_HttpURL, request, response);
System.err.println("tokenOK 0 ?"+checkResult.isSuccess());
System.err.println(gson.toJson(checkResult));
if(checkResult.isSuccess()) {
session.setAttribute("IsLogin",true);
NBC_ID_UsersVO user = checkResult.getUser();
if(user!=null) {
System.err.println("校驗Cookie token,token有效,返回用戶信息userInfo:"+gson.toJson(user));
String fuserId = String.valueOf(user.getFuserID());
session.setAttribute("UID", fuserId);
session.setAttribute("UserName",user.getUserName_CHS() );
application.setAttribute(fuserId, session.getId());
}
chain.doFilter(request, response);
}else {
CookieUtils.clearCookie(NBC_USER_TOKEN, request, response);
response.sendRedirect(ssoLoginUrl);
}
}else {
SSOCheckResult checkResult = SsoCheck.checkToken(null, TokenLogin_HttpURL, request, response);
System.err.println("tokenOK 1 ?"+checkResult.isSuccess());
System.err.println(gson.toJson(checkResult));
if(checkResult.isSuccess()) {
token = checkResult.getToken();
session.setAttribute("IsLogin",true);
NBC_ID_UsersVO user = checkResult.getUser();
if(user!=null) {
System.err.println("校驗Cookie token,token有效,返回用戶信息userInfo:"+gson.toJson(user));
String fuserId = String.valueOf(user.getFuserID());
session.setAttribute("UID", fuserId);
session.setAttribute("UserName",user.getUserName_CHS() );
application.setAttribute(fuserId, session.getId());
}
// boolean existCookie = CookieUtils.existCookie(NBC_USER_TOKEN,request);
// if(!existCookie) {
// CookieUtils.addCookie(NBC_USER_TOKEN, token, Integer.parseInt(NBC_SSO_CLIENT_COOKIE_TIMEOUT) , request, response);
// }
chain.doFilter(request, response);
}else {
//clear cookie and redirect to login
response.sendRedirect(ssoLoginUrl);
}
}
}
}
@Override
public void destroy() {
System.out.println("============ Destroy Filter LoginFilter ============");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("============ Init Filter LoginFilter ============");
}
}
以上攔截器中涉及到使用HttpURLConnection訪問SSO校驗Token有效性。SsoCheck.java
public class SsoCheck {
private static final Gson gson = new Gson();
private static String NBC_USER_TOKEN = PropertiesUtils.getProperty("NBC_SSO_CLIENT_COOKIENAME_FOR_TOKEN");
public static SSOCheckResult checkToken(String token, String httpURL,HttpServletRequest request, HttpServletResponse response) {
System.err.println("=============== SSOCheckResult checkToken ===============");
String clientIp = ComputerInfoUtil.getIP(request);
SSOCheckResult result = new SSOCheckResult(false,500,"未知錯誤",null,token);
//如果不存在token參數,則在Cookie中查找
if(token==null || token.equals("")) {
//Cookie中是否有Token
Cookie[] cookies = request.getCookies();
if(cookies!=null && cookies.length>0) {
for(Cookie cookie : cookies) {
if(cookie.getName().equals(NBC_USER_TOKEN)) {
token = cookie.getValue();
}
}
}
}
if(token==null || token.equals("")) {
return new SSOCheckResult(false,404,"參數錯誤[token]");
}
result.setToken(token);
String encoding = "UTF-8";
String returnURL = request.getRequestURL().toString();
try {
//創建URL對象
URL url = new URL(httpURL);
//創建連接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// true -- will setting parameters
conn.setDoOutput(true);
// true--will allow read in from
conn.setDoInput(true);
// will not use caches
conn.setUseCaches(false);
// setting serialized
//conn.setRequestProperty("Content-type", "application/x-java-serialized-object");
conn.setRequestProperty("Content-type", "application/json; charset=UTF-8");
conn.setRequestProperty("Charsert", "UTF-8");
conn.setRequestMethod("POST");// default is GET
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("Charsert", "UTF-8");
// 1 min
conn.setConnectTimeout(60000);
// 1 min
conn.setReadTimeout(60000);
//send args
conn.addRequestProperty("token",(token==null)?"":URLEncoder.encode( token, "UTF-8"));
conn.addRequestProperty("clientIp",(clientIp==null)?"":URLEncoder.encode( clientIp, "UTF-8"));
conn.addRequestProperty("returnURL",returnURL==null?"":URLEncoder.encode( returnURL, "UTF-8"));
// connect to server (tcp)
conn.connect();
//獲得響應碼
int code = conn.getResponseCode();//200代表Http OK
String TokenOK = (conn.getHeaderField("TokenOK")==null)?"":conn.getHeaderField("TokenOK");
String userInfo = (conn.getHeaderField("userInfo")==null)?"":conn.getHeaderField("userInfo");
result.setErrorCode(code);
if(code==HttpURLConnection.HTTP_OK && TokenOK.equals("true")){
userInfo = userInfo.equals("")?userInfo:URLDecoder.decode(userInfo, encoding);
userInfo = userInfo.replace("\"", "").replace("\\", "\"");
NBC_ID_UsersVO user = new NBC_ID_UsersVO();
if(!userInfo.equals("")) {
user = gson.fromJson(userInfo, NBC_ID_UsersVO.class);
}
result = new SSOCheckResult(true,0,"",user,token);
}
//關閉連接
conn.disconnect();
}catch(Exception e) {
e.printStackTrace();
result.setSuccess(false);
result.setErrorCode(500);
result.setErrorMsg("服務器異常:"+e.getMessage());
}
return result;
}
}
好了,子系統的邏輯已經處理好,接下來就差SSO端來校驗子系統發出的Http校驗Token請求了。
那麼接下來,需要創建SSO認證中心項目。
創建springboot項目,pom.xml主要引入JWT、Jedis、HttpClient等依賴,以下是我的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<packaging>war</packaging>
<groupId>com.nbc</groupId>
<artifactId>com.nbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>NBC_SSO</name>
<description>Spring Boot SSO Demo</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<activiti.version>5.23.0-RC1</activiti.version><!--5.22.0 / 5.23.0-RC1 -->
<codehaus.jackson.version>1.9.4</codehaus.jackson.version>
<gson.version>2.8.2</gson.version>
<c3p0.version>0.9.5.2</c3p0.version>
<fasterxml.jackson.version>2.7.5</fasterxml.jackson.version>
<hibernate.version>5.1.0.Final</hibernate.version>
<hibernate.core.version>5.2.0.Final</hibernate.core.version>
<hibernate.ehcache.version>5.2.0.Final</hibernate.ehcache.version>
<hibernate.annotations.version>5.1.0.Final</hibernate.annotations.version>
<hibernate.jpa.api.version>1.0.1.Final</hibernate.jpa.api.version>
<httpclient.version>4.5.2</httpclient.version>
<juniversalchardet.version>1.0.3</juniversalchardet.version>
<jedis.version>2.9.0</jedis.version>
<junit.version>4.12</junit.version>
<jstl.version>1.2</jstl.version>
<jwt.version>3.4.0</jwt.version>
<jackson.version>2.9.9</jackson.version>
<net.sf.json.version>2.4</net.sf.json.version>
<org.json.version>20190722</org.json.version>
<poi.version>3.12</poi.version>
<pdf2dom.version>1.7</pdf2dom.version>
<pdfbox.version>2.0.12</pdfbox.version>
<spring.version>5.2.0.BUILD-SNAPSHOT</spring.version><!--4.3.24.RELEASE 5.2.0.BUILD-SNAPSHOT -->
<sqlserver.version>4.0</sqlserver.version>
<servlet.api.version>4.0.0</servlet.api.version>
<xdocreport.version>1.0.5</xdocreport.version>
</properties>
<dependencies>
<!-- 單元測試 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!-- JSONObject-->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${org.json.version}</version>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- pdfbox -->
<dependency>
<groupId>net.sf.cssbox</groupId>
<artifactId>pdf2dom</artifactId>
<version>${pdf2dom.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<!-- net.sf.json -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>${net.sf.json.version}</version>
<classifier>jdk15</classifier>
</dependency>
<!-- jwt 官網依賴 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- sqlserver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>${sqlserver.version}</version>
</dependency>
<!-- c3p0 有自動回收空閒連接功能 -->
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- for JPA, use hibernate-entitymanager instead of hibernate-core -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-osgi</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-proxool</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-infinispan</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- jsp相關 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jsp-api</artifactId>
</dependency>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<finalName>nbs-sso</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
相關依賴自行拷貝到你的項目。
如何整合Springmvc和Hibernate這個我就不多說,自己搞(因爲我的SSO已經在用了,只能展示部分核心代碼)。當然,需要完整的SSO代碼也可以聯繫我,但要有償喔(Q:2225629171)
SSO登陸模塊:
@RequestMapping(value="/login",method=RequestMethod.GET)
public String to_login(Model model,HttpServletRequest req,HttpSession session) {
System.err.println("================ SSO Login ==============");
String returnURL = "";
try {
returnURL = req.getParameter("returnURL");
if(returnURL!=null && !returnURL.equals("")) {
returnURL = URLDecoder.decode(returnURL, "UTF-8");
}
}catch(Exception e) {
e.printStackTrace();
}
System.err.println("returnURL:"+returnURL);
model.addAttribute("user",new z_CF_UsersVO());
model.addAttribute("returnURL",returnURL);
return "login";
}
@RequestMapping(value="/login",method=RequestMethod.POST,produces = "application/json; charset=utf-8")
@ResponseBody
public String do_login(Model model,HttpServletRequest req,HttpServletResponse resp,HttpSession session)
throws Exception {
JSONObject json = new JSONObject();
String sessionId = session.getId();
String ipv4 = ComputerInfoUtil.getIP(req);
String login_method = req.getParameter("login_method");
String username = req.getParameter("username");
String password = req.getParameter("password");
//1.校驗參數
System.err.println("執行校驗參數");
String[] params = new String[] {login_method,username,password};
boolean paramOK = loginService.checkParam(params);
if(!paramOK) {
json.put("success", false);
json.put("info", "參數錯誤:"+gson.toJson(params));
return json.toString();
}
//2.執行登陸校驗
PublicModel<LoginResult> result = loginService.login(sessionId,ipv4,login_method,username,password);
if(!result.isSuccess()) {
json.put("success", false);
json.put("info", "執行登陸校驗時發生異常:"+result.getErrorMsg());
return json.toString();
}
LoginResult loginResult = result.getObj();
if(loginResult==null) {
json.put("success", false);
json.put("info", "執行登陸校驗時發生異常:loginResult=null");
return json.toString();
}
System.err.println("loginResult.getCode():"+loginResult.getCode());
if(!String.valueOf(loginResult.getCode()).equals(String.valueOf(LoginCode.First_Login)) && !String.valueOf(loginResult.getCode()).equals(String.valueOf(LoginCode.Permit_Login))) {
System.err.println("========= 返回錯誤信息 ==========");
json.put("success", true);
json.put("loginResult", gson.toJson(loginResult));
return json.toString();
}
//登陸有效
//判斷Redis服務是否已啓動
System.err.println("查詢Redis服務是否已啓動");
boolean IsStarted = jedisClient.IsRedisStarted();
if(!IsStarted) {
json.put("success", false);
json.put("info", "Redis服務未啓動,請及時聯繫管理員,謝謝您的配合!");
return json.toString();
}
System.err.println("Redis服務已啓動");
//獲取新的用戶登陸信息
z_CF_UsersVO currentUser = new z_CF_UsersVO();//將部分信息封裝返回
currentUser.setFuserID(loginResult.getFuserId());
currentUser.setUserName_EN(loginResult.getUserName_EN());
currentUser.setUserName_CHS(loginResult.getUserName_CHS());
Map<String,String> claims = new HashMap<String,String>();
claims.put("fuserId", String.valueOf(loginResult.getFuserId()));
claims.put("userInfo",gson.toJson(currentUser));
String newToken = JwtUtil.genToken(claims,Long.parseLong(NBC_SSO_SERVER_JWT_TOKEN_EXPIRE));
currentUser.setToken(newToken);
//註銷該用戶在其他地方的登錄信息
System.err.println("註銷該用戶舊的登錄信息");
String key = "";
String old_userInfo = "";
try {
if(currentUser!=null) {
key = NBC_SSO_SERVER_JEDIS_KEY_PREFIX+String.valueOf(currentUser.getFuserID());
if(key!=null && !key.equals("")) {
old_userInfo = jedisClient.get(key);
}
System.err.println("old_userInfo:"+old_userInfo);
if(old_userInfo!=null && !old_userInfo.equals("")) {
jedisClient.del(key);
}
}
}catch(Exception e) {
System.err.println("註銷用戶登陸信息時發生異常:"+e.getMessage());
e.printStackTrace();
}
//註冊該用戶最新的登陸信息
if (newToken != null && key!=null && !key.equals("")) {
jedisClient.set(key , gson.toJson(currentUser));
jedisClient.expire(key,Integer.parseInt(NBC_SSO_SERVER_JEDIS_KEY_TIMEOUT));
}
System.err.println("newToken:"+newToken);
json.put("success", true);
json.put("loginResult", gson.toJson(loginResult));
json.put("token", newToken);
System.err.println("返回數據:"+json.toString());
return json.toString();
}
SSO校驗中心:(SsoCheckController.java)
@Controller
@RequestMapping("/ssocheck")
public class SsoCheckController {
private static final Gson gson = new Gson();
private static String NBC_SSO_SERVER_REGEX_OF_IP = PropertiesUtils.getProperty("NBC_SSO_SERVER_REGEX_OF_IP");
private static String NBC_SSO_SERVER_JEDIS_KEY_PREFIX = PropertiesUtils.getProperty("NBC_SSO_SERVER_JEDIS_KEY_PREFIX");
@Autowired
private JedisClient jedisClient;
@Autowired
private LogService logService;
@RequestMapping(value = "/tokencheck")
public void ssocheck(HttpServletRequest request, HttpServletResponse response,HttpSession session) {
System.err.println("=============== server tokencheck ===============");
String sessionId = session.getId();
String token = request.getHeader("token");
System.err.println("tokencheck token:"+token);
String clientIp = request.getHeader("clientIp");
String returnURL = request.getHeader("returnURL");
String errorURL = request.getContextPath()+"/error/commonError";
String loginURL = request.getContextPath()+"/login?returnURL="+returnURL;
String sonSysIp = ComputerInfoUtil.getIP(request);
JSONObject json = new JSONObject();
List<String> event = new ArrayList<String>();
event.add("客戶端請求SSO認證中心校驗Token");
try {
//returnURL = getDecoderString(returnURL,encoding);
if(token==null || token.equals("")) {
event.add("警告:token參數爲空,服務器已重定向頁面至登陸頁面:"+loginURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}
Pattern pattern = Pattern.compile(NBC_SSO_SERVER_REGEX_OF_IP);
if(clientIp==null || clientIp.equals("")) {
event.add("警告:clientIp參數爲空,服務器已重定向頁面至登陸頁面:"+loginURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}else if(!pattern.matcher(clientIp).matches()){
event.add("警告:clientIp參數非法,該參數源自子系統ipv4:"+sonSysIp);
event.add("服務器已重定向頁面至登陸頁面:"+loginURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}
String encoding = "UTF-8";
token = getDecoderString(token,encoding);
event.add("請求參數Token:"+token);
boolean tokenOK = JwtUtil.checkToken(token);
System.err.println("校驗Token:"+((tokenOK==true)?"有效":"無效"));
if(!tokenOK) {
event.add("警告:Token認證不通過,該客戶端存在僞造Token的可能性,請及時做好應對措施。");
event.add("服務器已重定向頁面至登陸頁面:"+loginURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}
String redis_userInfo = "";
String fuserId = JwtUtil.getClaim("fuserId", token);
if(fuserId!=null) {
String key = NBC_SSO_SERVER_JEDIS_KEY_PREFIX+AesUtil.decode(fuserId);
System.err.println("key:"+key);
redis_userInfo = jedisClient.get(key);
}
System.err.println("redis_userInfo:"+redis_userInfo);
if(redis_userInfo==null || redis_userInfo.equals("")) {
//token不存在於服務器redis
event.add("警告:Token不存在於服務器,服務器已重定向頁面至登陸頁面:"+loginURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}
//token有效並且服務器有該用戶登陸信息
//校驗token和登陸信息的token是否一致
z_CF_UsersVO user = gson.fromJson(redis_userInfo, z_CF_UsersVO.class);
if(!token.equals(user.getToken())) {
event.add("token有效並且服務器有該用戶登陸信息,但token和服務器的token不一致(可能是該用戶已在其他地方登陸)");
logService.ssocheckToLog(sessionId,clientIp, false, event);
response.sendRedirect(loginURL);
return;
}
String TokenOK1 = "true";
String userInfo = AesUtil.decode(JwtUtil.getClaim("userInfo", token));
String userInfo1 = (userInfo==null)?"":URLEncoder.encode(gson.toJson(userInfo), encoding);
json.put("TokenOK", TokenOK1);
json.put("userInfo", userInfo);
event.add("Token認證通過,即將返回信息給客戶端:"+json.toString());
logService.ssocheckToLog(sessionId,clientIp, true, event);
response.setHeader("TokenOK", TokenOK1);
response.setHeader("userInfo",userInfo1);
}catch(Exception e) {
try {
String info = "SSO認證中心校驗Token時發生異常:"+e.getMessage();
//checktoken記錄日誌
event = new ArrayList<String>();
event.add(info);
event.add("服務器已重定向頁面至錯誤頁面:"+errorURL);
logService.ssocheckToLog(sessionId,clientIp, false, event);
request.getSession().setAttribute("error", info);
response.sendRedirect(errorURL);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
e.printStackTrace();
}
}
private String getEncoderString(String str,String encoding) {
String result = "";
try {
if(str!=null && !str.equals("") && encoding!=null && !encoding.equals("")) {
result = URLEncoder.encode( str, encoding);
}
}catch(Exception e) {
e.printStackTrace();
}
return result;
}
private String getDecoderString(String str,String encoding) {
String result = "";
try {
if(str!=null && !str.equals("") && encoding!=null && !encoding.equals("")) {
result = URLDecoder.decode( str, encoding);
}
}catch(Exception e) {
e.printStackTrace();
}
return result;
}
}
JWT工具類:
@Slf4j
public class JwtUtil {
// public static void main(String[] args) {
// long expire = 1000 * 60;
// Map<String, String> map = new HashMap<String, String>();
// map.put("fuserId", "2");
// map.put("username", "admin");
// String token = genToken(map,expire);
// System.err.println(token);
// Map<String, String> claims = getClaims(token);
// for(String key : claims.keySet()) {
// //System.err.println(key);
// //System.err.println(key+":"+AesUtil.decode(claims.get(key)));
// }
// //String claim = getClaim("fuserId",token);
// //System.err.println(AesUtil.decode(claim));
//
// }
private static final Gson gson = new Gson();
//密鑰
private static String SECRET = readSecret();;
//發行人
private static String ISSUER = "NBC_USER";
public static String getClaim(String claimName, String token) {
String[] params = new String[] {claimName,token};
for(String param : params) {
if(param==null || param.equals("")) {
return null;
}
}
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//解密
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> map = jwt.getClaims();
Map<String, String> resultMap = new HashMap<>();
map.forEach((k,v) -> resultMap.put(k, v.asString()));
return resultMap.get(claimName);
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
public static Map<String, String> getClaims(String token) {
if(token==null || token.equals("")) {
return null;
}
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//解密
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> map = jwt.getClaims();
Map<String, String> resultMap = new HashMap<>();
map.forEach((k,v) -> resultMap.put(k,v.asString()));
return resultMap;
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 生成jwt
*/
public static String genToken(Map<String, String> claims,long expire) {
try {
//過期時間
Date expireDate = new Date(System.currentTimeMillis() + expire);
//使用HMAC256進行加密
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//創建jwt
JWTCreator.Builder builder = JWT.create()
.withIssuer(ISSUER)//發行人
.withExpiresAt(expireDate); //過期時間點
//傳入參數
if(claims!=null) {
claims.forEach((key,value)-> {
String encodeValue = AesUtil.encode(value);
builder.withClaim(key, encodeValue);
});
}
//簽名加密
return builder.sign(algorithm);
} catch (Exception e) {
//log.info("jwt 生成問題");
}
return null;
}
/**
* 解密jwt並驗證是否正確
*/
public static boolean checkToken (String token) {
if(token==null || token.equals("")) {
return false;
}
try {
//使用hmac256加密算法
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer(ISSUER)
.build();
DecodedJWT decodedJWT = verifier.verify(token);
//獲得token的頭部,載荷和簽名,只對比頭部和載荷
String [] headPayload = token.split("\\.");
//獲得jwt解密後頭部
String header = decodedJWT.getHeader();
//獲得jwt解密後載荷
String payload = decodedJWT.getPayload();
return (header.equals(headPayload[0]) && payload.equals(headPayload[1]));
} catch (Exception e) {
//log.info("jwt解密出現錯誤,jwt或私鑰或簽證人不正確");
e.printStackTrace();
}
return false;
}
/**
* 讀取密鑰
* @return 密鑰信息
*/
private static String readSecret() {
String secret = "";
try {
Properties properties = new Properties();
InputStream inputStream = ClassLoader.getSystemResourceAsStream("jwt.properties");//加載resource目錄下的配置文件
properties.load(inputStream);
secret = properties.getProperty("JWT_SECRET");
}catch(Exception e) {
e.printStackTrace();
}
return secret;
}
}
AesUtil.java:
@Slf4j
public class AesUtil {
//加密密鑰
private static String SECRET = readSecret();;
/**
* 加密
* @return 加密後內容
*/
public static String encode (String content) {
Key key = getKey();
byte[] result = null;
try{
//創建密碼器
Cipher cipher = Cipher.getInstance("AES");
//初始化爲加密模式
cipher.init(Cipher.ENCRYPT_MODE,key);
//加密
result = cipher.doFinal(content.getBytes("UTF-8"));
//將二進制轉換成16進制
StringBuffer sb = new StringBuffer();
for (int i = 0; i < result.length; i++) {
String hex = Integer.toHexString(result[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
} catch (Exception e) {
//log.info("aes加密出錯");
}
return null;
}
/**
* 解密
* @return 解密後內容
*/
public static String decode (String content) {
//將16進制轉爲二進制
if (content.length() < 1)
return null;
byte[] result = new byte[content.length()/2];
for (int i = 0;i< content.length()/2; i++) {
int high = Integer.parseInt(content.substring(i*2, i*2+1), 16);
int low = Integer.parseInt(content.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
Key key = getKey();
byte[] decrypt = null;
try{
//創建密碼器
Cipher cipher = Cipher.getInstance("AES");
//初始化爲解密模式
cipher.init(Cipher.DECRYPT_MODE,key);
//解密
decrypt = cipher.doFinal(result);
} catch (Exception e) {
//log.info("aes解密出錯");
}
assert decrypt != null;
return new String(decrypt);
}
/**
* 根據私鑰內容獲得私鑰
*/
private static Key getKey () {
SecretKey key = null;
try {
//創建密鑰生成器
KeyGenerator generator = KeyGenerator.getInstance("AES");
//初始化密鑰
generator.init(128,new SecureRandom(SECRET.getBytes()));
//生成密鑰
key = generator.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return key;
}
/**
* 讀取密鑰
* @return 密鑰信息
*/
public static String readSecret () {
String secret = "";
try {
Properties properties = new Properties();
//加載resource目錄下的配置文件
InputStream inputStream = ClassLoader.getSystemResourceAsStream("aes.properties");
properties.load(inputStream);
secret = properties.getProperty("AES_SECRET");
} catch (IOException e) {
//log.info("讀取密鑰文件錯誤");
}
return secret;
}
}
SSO登錄成功後,跳轉至SSO登陸成功處理頁面loginSuccess.jsp(ajax發送命令,讓子系統添加token到Cookie後,重定向頁面回子系統指定的LoginOK頁面)
@RequestMapping(value="/loginSuccess",method=RequestMethod.GET)
public String loginSuccess(Model model,HttpServletRequest req,HttpSession session) {
String domains = "";
String returnURL = (req.getParameter("returnURL")==null)?"":req.getParameter("returnURL");
String token = (req.getParameter("token")==null)?"":req.getParameter("token");
try {
List<z_CF_DomainVO> list = new ArrayList<z_CF_DomainVO>();
List<z_CF_Domain> list0 = domainRepository.findAll();
list0.forEach((domain)->{
z_CF_DomainVO vo = new z_CF_DomainVO();
BeanUtils.copyProperties(domain, vo);
list.add(vo);
});
domains = ScriptDecoder.escape(gson.toJson(list));
}catch(Exception e) {
e.printStackTrace();
}
session.setAttribute("domains",domains );
session.setAttribute("returnURL", returnURL);
session.setAttribute("token", token);
return "public/loginSuccess";
}
@RequestMapping(value="/return",method=RequestMethod.POST)
public String returnPage(Model model,HttpServletRequest req,HttpServletResponse resp,HttpSession session) {
String returnURL = (req.getParameter("returnURL")==null)?"":req.getParameter("returnURL");
String token = (req.getParameter("token")==null)?"":req.getParameter("token");
String type = (req.getParameter("type")==null)?"":req.getParameter("type");
session.setAttribute("type", type);
session.setAttribute("returnURL", returnURL);
session.setAttribute("token", token);
return "public/return";
}
loginSuccess.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>loginSuccess</title>
<%
System.err.println("=============== SSO loginSuccess.jsp ===============");
%>
<!-- jquery -->
<script type="text/javascript" src="${pageContext.request.contextPath }/static/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
$(function(){
var domains = $("#domains").val();
var token = $("#token").val();
var returnURL = $("#returnURL").val();
//jsonpTest();
sendCookie(domains,token);
$("#MyForm").submit();
function sendCookie(domains,token){
if(domains!="" && domains!=undefined){
domains = JSON.parse(unescape(domains));
for(var i in domains){
var domainObj = domains[i];
//var addCookieURL = domainObj.returnURLOfLoginOK;
var addCookieURL = domainObj.urlOfOperateCookie;
if(token!=null && token!=""){
sendAjax(addCookieURL,token);
}
}
}
}
function sendAjax(addCookieURL,token){
if(addCookieURL!="" && addCookieURL!=null && addCookieURL!=undefined){
var url = "";
if(addCookieURL.indexOf("?")>0){
url = addCookieURL+"&jsonpCallback=callbackFunction&type=login&token="+token;
}else{
url = addCookieURL+"?jsonpCallback=callbackFunction&type=login&token="+token;
}
$.ajax({
url: url,
type: "GET",
timeout:10000,//10秒超時,單位:毫秒
async: false,
processData: false,
jsonp: "callback",//傳遞給請求處理程序或頁面的,用以獲得jsonp回調函數名的參數名(默認爲:callback)
jsonpCallback:"callbackFunction",//自定義的jsonp回調函數名稱,默認爲jQuery自動生成的隨機函數名
dataType: "jsonp", //指定服務器返回的數據類型
success:function(data){
},
//ajax Exception
error: function(XMLHttpRequest, textStatus, errorThrown) {
var httpStatus = XMLHttpRequest.status;
if(httpStatus!=200){
addCookieLog(url,httpStatus);
}
/*
alert("XMLHttpRequest.status:"+XMLHttpRequest.status+"\n"+"XMLHttpRequest.readyState:"+XMLHttpRequest.readyState+"\n"
+"textStatus:"+textStatus);
*/
}
});
}
}
function addCookieLog(url,httpStatus){
$.ajax({
type:"POST",
dataType:"json",
timeout:10000,
url:"/log/addCookieFiledLog", //後臺url請求,處理傳遞的參數
async: false,
data:{
url:escape(url),
httpStatus:httpStatus
},
success:function(data){
//ajax請求成功執行該函數
//alert("添加日誌成功:"+data)
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
// alert("添加日誌失敗status:"+XMLHttpRequest.status)
/* alert("XMLHttpRequest.status:"+XMLHttpRequest.status+"\n"+"XMLHttpRequest.readyState:"+XMLHttpRequest.readyState+"\n"
+"textStatus:"+textStatus); */
}
});
}
function jsonpTest(){
var ajax = $.ajax({
type: "GET",
async: false,
timeout:10000,
//url: "http://10.1.0.192:30013/KMS/Test/jsonpData.jsp?jsonpCallback=callbackFunction",
url: "http://10.1.8.12:8082/KMS/Jsp/public/index/cookie.jsp?jsonpCallback=callbackFunction",
dataType: "jsonp",
jsonp: "callback",//傳遞給請求處理程序或頁面的,用以獲得jsonp回調函數名的參數名(一般默認爲:callback)
jsonpCallback:"callbackFunction",//自定義的jsonp回調函數名稱,默認爲jQuery自動生成的隨機函數名,也可以寫"?",jQuery會自動爲你處理數據
success: function(json){
alert(JSON.stringify(json));
},
error: function(){
alert('jsonp fail');
}
});
}
})
</script>
</head>
<body>
<jsp:include page="/public/loading.jsp"/>
<form id="MyForm" action="/return" method="POST">
<input type="hidden" id="type" name="type" value="login" />
<input type="hidden" id="domains" name="domains" value="${domains }" />
<input type="hidden" id="returnURL" name="returnURL" value="${returnURL }" />
<input type="hidden" id="token" name="token" value="${token }" />
</form>
</body>
</html>
return.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
System.err.println("================= return.jsp =================");
String returnURL = (session.getAttribute("returnURL")==null)?"":session.getAttribute("returnURL").toString();
String token = (session.getAttribute("token")==null)?"":session.getAttribute("token").toString();
String type = (session.getAttribute("type")==null)?"":session.getAttribute("type").toString();
if(!returnURL.equals("")){
String returnType = "";
switch(type.toLowerCase()){
case "login":
returnType = "登陸";
if(!token.equals("")){
if(returnURL.indexOf("?")>0){
returnURL = returnURL+"&token="+token;
}else{
returnURL = returnURL+"?token="+token;
}
}
break;
case "logout":
returnType = "註銷";
break;
default:
break;
}
System.err.println("類型:"+returnType);
System.err.println("服務器即將重定向回子系統:"+returnURL);
response.sendRedirect(returnURL);
}
%>
好了,SSO大部分核心代碼已經貼出來了,接下來就看你們的編碼功底了。有什麼相關問題也可以找我討論(q:2225629171)
本次SSO實現跨域單點登陸講解完畢。