在分布式环境中,如何支持PC、APP(ios、android)等多端的会话共享,这也是所有公司都需要的解决方案,用传统的session方式来解决,我想已经out了,我们是否可以找一个通用的方案,比如用传统cas来实现多系统之间的sso单点登录或使用oauth的第三方登录方案? 今天给大家简单讲解一下使用spring拦截器Interceptor机制、jwt认证方式、redis分布式缓存实现sso单点登录,闲话少说,直接把步骤记录下来分享给大家:
1. 引入jwt的相关jar包,在项目pom.xml中引入:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>2.2.0</version> </dependency>
2. 拦截器配置:
<mvc:interceptor> <mvc:mapping path="${adminPath}/**" /> <mvc:exclude-mapping path="${adminPath}/rest/login"/> <bean class="com.ml.honghu.interceptor.LoginInterceptor" /> </mvc:interceptor>
我这里简单配置了要拦截的url和过滤的url(这个根据自己项目来定)
3. 编写jwt的加密或者解密工具类:
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; } } }
这个加密工具类是我从网上找的,如果各位要修改,可以按照自己业务修改即可。
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对象存储:
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. 定义异常的LoginResponseCode
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": { "QQ":"2147775633", "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": "13888888888", "userType": "", "loginIp": "0:0:0:0:0:0:0:1", "loginDate": "2017-10-30 10:48:06", "loginFlag": "1", "photo": "", "idCard": "420888888888888888", "oldLoginIp": "0:0:0:0:0:0:0:1", "oldLoginDate": "2017-10-30 10:48:06", "roleNames": "", "admin": false } }, "code": 200 }
到此完毕!!
用java实施的电子商务平台太少了,使用spring cloud技术构建的b2b2c电子商务平台更少,大型企业分布式互联网电子商务平台,推出PC+微信+APP+云服务的云商平台系统,其中包括B2B、B2C、C2C、O2O、新零售、直播电商等子平台。
Spring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台(企业架构源码可以加求球:叁五三陆二肆柒二伍玖)