在分佈式環境中,如何支持PC、APP(ios、android)等多端的會話共享,這也是全部公司都須要的解決方案,用傳統的session方式來解決,我想已經out了,咱們是否能夠找一個通用的方案,好比用傳統cas來實現多系統之間的sso單點登陸或使用oauth的第三方登陸方案? 今天給你們簡單講解一下使用spring攔截器Interceptor機制、jwt認證方式、redis分佈式緩存實現sso單點登陸,閒話少說,直接把步驟記錄下來分享給你們:html
1. 引入jwt的相關jar包,在項目pom.xml中引入:java
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>2.2.0</version> </dependency>
2. 攔截器配置:android
<mvc:interceptor> <mvc:mapping path="${adminPath}/**" /> <mvc:exclude-mapping path="${adminPath}/rest/login"/> <bean class="com.ml.honghu.interceptor.LoginInterceptor" /> </mvc:interceptor>
我這裏簡單配置了要攔截的url和過濾的url(這個根據本身項目來定)ios
3. 編寫jwt的加密或者解密工具類:redis
public class JWT { private static final String SECRET = "HONGHUJWT1234567890QWERTYUIOPASDFGHJKLZXCVBNM"; private static final String EXP = "exp"; private static final String PAYLOAD = "payload"; //加密 public static <T> String sign(T object, long maxAge) { try { final JWTSigner signer = new JWTSigner(SECRET); final Map<String, Object> claims = new HashMap<String, Object>(); ObjectMapper mapper = new ObjectMapper(); String jsonString = mapper.writeValueAsString(object); claims.put(PAYLOAD, jsonString); claims.put(EXP, System.currentTimeMillis() + maxAge); return signer.sign(claims); } catch(Exception e) { return null; } } //解密 public static<T> T unsign(String jwt, Class<T> classT) { final JWTVerifier verifier = new JWTVerifier(SECRET); try { final Map<String,Object> claims= verifier.verify(jwt); if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) { String json = (String)claims.get(PAYLOAD); ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.readValue(json, classT); } return null; } catch (Exception e) { return null; } } }
這個加密工具類是我從網上找的,若是各位要修改,能夠按照本身業務修改便可。spring
4. 建立Login.java對象,用來進行jwt的加密或者解密:數據庫
public class Login implements Serializable{ /** * */ private static final long serialVersionUID = 1899232511233819216L; /** * 用戶id */ private String uid; /** * 登陸用戶名 */ private String loginName; /** * 登陸密碼 */ private String password; public Login(){ super(); } public Login(String uid, String loginName, String password){ this.uid = uid; this.loginName = loginName; this.password = password; } public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
5. 定義RedisLogin對象,用來經過uid往redis進行user對象存儲:json
public class RedisLogin implements Serializable{ /** * */ private static final long serialVersionUID = 8116817810829835862L; /** * 用戶id */ private String uid; /** * jwt生成的token信息 */ private String token; /** * 登陸或刷新應用的時間 */ private long refTime; public RedisLogin(){ } public RedisLogin(String uid, String token, long refTime){ this.uid = uid; this.token = token; this.refTime = refTime; } public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public long getRefTime() { return refTime; } public void setRefTime(long refTime) { this.refTime = refTime; } }
6. 編寫LoginInterceptor.java攔截器緩存
public class LoginInterceptor implements HandlerInterceptor{ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { PrintWriter writer = null; HandlerMethod method = null; try { method = (HandlerMethod) handler; } catch (Exception e) { writer = response.getWriter(); ResponseVO responseVO = ResponseCode.buildEnumResponseVO(ResponseCode.REQUEST_URL_NOT_SERVICE, false); responseMessage(response, writer, responseVO); return false; } IsLogin isLogin = method.getMethodAnnotation(IsLogin.class); if(null == isLogin){ return true; } response.setCharacterEncoding("utf-8"); String token = request.getHeader("token"); String uid = request.getHeader("uid"); //token不存在 if(StringUtils.isEmpty(token)) { writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.LOGIN_TOKEN_NOT_NULL, false); responseMessage(response, writer, responseVO); return false; } if(StringUtils.isEmpty(uid)){ writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.USERID_NOT_NULL, false); responseMessage(response, writer, responseVO); return false; } Login login = JWT.unsign(token, Login.class); //解密token後的loginId與用戶傳來的loginId判斷是否一致 if(null == login || !StringUtils.equals(login.getUid(), uid)){ writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.USERID_NOT_UNAUTHORIZED, false); responseMessage(response, writer, responseVO); return false; } //驗證登陸時間 RedisLogin redisLogin = (RedisLogin)JedisUtils.getObject(uid); if(null == redisLogin){ writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.RESPONSE_CODE_UNLOGIN_ERROR, false); responseMessage(response, writer, responseVO); return false; } if(!StringUtils.equals(token, redisLogin.getToken())){ writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.USERID_NOT_UNAUTHORIZED, false); responseMessage(response, writer, responseVO); return false; } //系統時間>有效期(說明已經超過有效期) if (System.currentTimeMillis() > redisLogin.getRefTime()) { writer = response.getWriter(); ResponseVO responseVO = LoginResponseCode.buildEnumResponseVO(LoginResponseCode.LOGIN_TIME_EXP, false); responseMessage(response, writer, responseVO); return false; } //從新刷新有效期 redisLogin = new RedisLogin(uid, token, System.currentTimeMillis() + 60L* 1000L* 30L); JedisUtils.setObject(uid , redisLogin, 360000000); return true; } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } private void responseMessage(HttpServletResponse response, PrintWriter out, ResponseVO responseVO) { response.setContentType("application/json; charset=utf-8"); JSONObject result = new JSONObject(); result.put("result", responseVO); out.print(result); out.flush(); out.close(); } }
7. 定義異常的LoginResponseCodesession
public enum LoginResponseCode { USERID_NOT_NULL(3001,"用戶id不能爲空."), LOGIN_TOKEN_NOT_NULL(3002,"登陸token不能爲空."), USERID_NOT_UNAUTHORIZED(3003, "用戶token或ID驗證不經過"), RESPONSE_CODE_UNLOGIN_ERROR(421, "未登陸異常"), LOGIN_TIME_EXP(3004, "登陸時間超長,請從新登陸"); // 成員變量 private int code; //狀態碼 private String message; //返回消息 // 構造方法 private LoginResponseCode(int code,String message) { this.code = code; this.message = message; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public static ResponseVO buildEnumResponseVO(LoginResponseCode responseCode, Object data) { return new ResponseVO(responseCode.getCode(),responseCode.getMessage(),data); } public static Map<String, Object> buildReturnMap(LoginResponseCode responseCode, Object data) { Map<String, Object> map = new HashMap<String, Object>(); map.put("code", responseCode.getCode()); map.put("message", responseCode.getMessage()); map.put("data", data); return map; } }
8. 編寫統一sso單點登陸接口:
@RequestMapping(value = "/login", method = RequestMethod.POST) public Map<String, Object> login(@RequestBody JSONObject json){ String loginName = json.optString("loginName"); String password = json.optString("password"); //校驗用戶名不能爲空 if(StringUtils.isEmpty(loginName)){ return MemberResponseCode.buildReturnMap(MemberResponseCode.RESPONSE_CODE_USER_NAME_IS_NOT_EMPTY, null); } //校驗用戶密碼不能爲空 if(StringUtils.isEmpty(password)){ return MemberResponseCode.buildReturnMap(MemberResponseCode.RESPONSE_CODE_PWD_CAN_NOT_BE_EMPTY, null); } //根據用戶名查詢數據庫用戶信息 User user = systemService.getBaseUserByLoginName(loginName); //用戶名或密碼不正確 if(null == user){ return MemberResponseCode.buildReturnMap(MemberResponseCode.RESPONSE_CODE_USER_VALIDATE_NO_SUCCESS, false); } boolean isValidate = systemService.validatePassword(password, user.getPassword()); if(!isValidate){ return MemberResponseCode.buildReturnMap(MemberResponseCode.RESPONSE_CODE_USER_VALIDATE_NO_SUCCESS, false); } if(isValidate){ //HttpSession session =request.getSession(false); Login login = new Login(user.getId(), user.getLoginName(), user.getPassword()); //給用戶jwt加密生成token String token = JWT.sign(login, 60L* 1000L* 30L); Map<String,Object> result =new HashMap<String,Object>(); result.put("loginToken", token); result.put("userId", user.getId()); result.put("user", user); //保存用戶信息到session //session.setAttribute(user.getId() + "@@" + token, user); //重建用戶信息 this.rebuildLoginUser(user.getId(), token); return ResponseCode.buildReturnMap(ResponseCode.RESPONSE_CODE_LOGIN_SUCCESS, result); } return ResponseCode.buildReturnMap(ResponseCode.USER_LOGIN_PWD_ERROR, false); }
9. 測試sso單點登陸:
返回結果集:
{ "message": "用戶登陸成功", "data": { "loginToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MDkzODA1OTU0NTksInBheWxvYWQiOiJ7XCJ1aWRcIjpcIjExXCIsXCJsb2dpbk5hbWVcIjpcImFkbWluXCIsXCJwYXNzd29yZFwiOlwiZjU0NGQxM2QyY2EwNDU5ZGQ0ZTU1NzVjNmZkYWIzMzM0MzE1MWFlZjgwYmE5ZTNiN2U1ZjM2MzJcIn0ifQ.56L60WtxHXSu9vNs6XsWy5zbmc3kP_IWG1YpReK50DM", "userId": "11", "user": { "id": "11", "isNewRecord": false, "remarks": "", "createDate": "2017-08-08 08:08:08", "updateDate": "2017-10-29 11:23:50", "loginName": "admin", "no": "00012", "name": "admin", "email": "2147775633@qq.com", "phone": "400000000", "mobile": "15888888888", "userType": "", "loginIp": "0:0:0:0:0:0:0:1", "loginDate": "2017-10-30 10:48:06", "loginFlag": "1", "photo": "", "idCard": "370888888888888888", "oldLoginIp": "0:0:0:0:0:0:0:1", "oldLoginDate": "2017-10-30 10:48:06", "roleNames": "", "QQ":"2147775633", "admin": false } }, "code": 200 }