1.在大型互联网公司下,一个公司有多个平台系统项目,而且有可能每个项目开发语言都不同,同时每个平台也会分别有APP、PC(前后端分离)、小程序端等,而且服务部署在分布式环境下,要实现支持PC、APP(ios、android)、小程序等多端的会话共享,本文章采取的方案是spring+springmvc+Interceptor+jwt+redis实现sso单点登录。假如用传统方式session、cookie实现不太好,第一如果用session实现,会面临集群环境下session共享问题(可以用springsession来解决),但是session存过多数据会给服务器带来压力。第二如果用cookie实现,会面临域名跨域问题,而且不适合用在native app里面:native app不好管理cookie,毕竟它不是浏览器。
2.通俗来讲,JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据,同时JSON解析成对象。
优点:在非跨域环境下使用JWT机制是一个非常不错的选择,实现方式简单,操作方便,能够快速实现。由于服务端不存储用户状态信息,因此大用户量,对后台服务也不会造成压力;
缺点:跨域实现相对比较麻烦,安全性也有待探讨。因为JWT令牌返回到页面中,可以使用js获取到,如果遇到XSS攻击令牌可能会被盗取,在JWT还没超时的情况下,就会被获取到敏感数据信息(敏感信息最好别放置JWT中)。
但是我们可以针对令牌盗取,做一些防范:比如前后端传输参数RSA验签防止篡改,采取https加密参数传输。没有一个互联网平台做到绝对安全的,只能说提高安全性,把风险降到最低。
com.auth0
java-jwt
2.2.0
public class JWT {
private static final String SECRET = "填写你的秘钥";
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
//加密,传入一个对象和有效期
public static String sign(T object, long maxAge) {
try {
final JWTSigner signer = new JWTSigner(SECRET);
final Map claims = new HashMap();
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;
}
}
//解密,传入一个加密后的token字符串和解密后的类型
public static T unsign(String jwt, Class classT) {
final JWTVerifier verifier = new JWTVerifier(SECRET);
try {
final Map claims= verifier.verify(jwt);
if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
//long exp = (Long)claims.get(EXP);
//long currentTimeMillis = System.currentTimeMillis();
//if (exp > currentTimeMillis) {
String json = (String)claims.get(PAYLOAD);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, classT);
//}
}
return null;
} catch (Exception e) {
return null;
}
}
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
//加密
String ss = sign("123444444444444444444444444444444444444444444444444444444444444444444;4a", 0);
System.out.println(ss);
//解密
System.out.println(unsign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NTg5NDY4NDUzODksInBheWxvYWQiOiJcIjExMTFcIiJ9.jJwpdHgpNLjU1vL0DZhxtAsNEBG0ysXrJEvMf_X5OXk", String.class));
}
}
public class LoginInterceptor implements HandlerInterceptor{
public static final String REDIS_TOKEN_FLAG = "redis_token";
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//过滤不匹配的请求
PrintWriter writer = null;
if(handler instanceof DefaultServletHttpRequestHandler){
writer = response.getWriter();
Map resultMap = ResponseCode.buildReturnMap(ResponseCode.REQUEST_URL_NOT_SERVICE, false);
responseMessage(response, writer, resultMap);
return false;
}
response.setCharacterEncoding("utf-8");
String token = request.getHeader("token");
//拼接redis的key=平台标识+登录方式+(会员表id或用户授权表id)
//token不存在
if(StringUtils.isEmpty(token)) {
writer = response.getWriter();
Map resultMap = LoginResponseCode.buildReturnMap(LoginResponseCode.LOGIN_TOKEN_NOT_NULL, false);
responseMessage(response, writer, resultMap);
return false;
}
//tokenRedisKey是redis存token的key
String tokenRedisKey = LoginInterceptor.REDIS_TOKEN_FLAG + JWT.unsign(token, String.class);
if(StringUtils.isEmpty(tokenRedisKey)){
writer = response.getWriter();
Map resultMap = LoginResponseCode.buildReturnMap(LoginResponseCode.USERID_NOT_UNAUTHORIZED, false);
responseMessage(response, writer, resultMap);
return false;
}
//根据token截取用户id
String redisToken = (String)RedisClusterUtils.get(tokenRedisKey);
LoginResponseCode loginCode = null;
if(StringUtils.isEmpty(redisToken)){
//未登录
loginCode = LoginResponseCode.LOGIN_TIME_EXP;
}
else if(!StringUtils.equals(redisToken, token)){
//用户在同设备(APP、PC、小程序、公众号,比如APP设备不能登录两个一样的账号)上登录,你已经被踢下线
loginCode= LoginResponseCode.RESPONSE_CODE_LOGIN_ERROR;
}
if(loginCode != null){
writer = response.getWriter();
Map resultMap = LoginResponseCode.buildReturnMap(loginCode, false);
responseMessage(response, writer, resultMap);
return false;
}
//重新设置会话时间,半小时
RedisClusterUtils.expire(LoginInterceptor.REDIS_TOKEN_FLAG + token, 1800);
request.setAttribute("tokenRedisKey", tokenRedisKey);
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, Object responseVO) {
response.setContentType("application/json; charset=utf-8");
out.print(JSONObject.fromObject(responseVO).toString());
out.flush();
out.close();
}
}
public enum LoginResponseCode {
USERID_NOT_NULL(3001,"用户id不能为空."),
LOGIN_TOKEN_NOT_NULL(3002,"登录token不能为空."),
USERID_NOT_UNAUTHORIZED(3003, "用户token无效"),
RESPONSE_CODE_UNLOGIN_ERROR(421, "未登录异常"),
RESPONSE_CODE_LOGIN_ERROR(422, "用户在另一台机器上登录,你已经被踢下线"),
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 buildReturnMap(LoginResponseCode responseCode, Object data) {
Map map = new HashMap();
map.put("code", responseCode.getCode());
map.put("message", responseCode.getMessage());
map.put("data", data);
return map;
}
}
public enum ResponseCode {
RESPONSE_CODE_FAILURE(10000,"请求失败、结果处理失败"),
RESPONSE_CODE_SUCCESS(200,"请求成功、结果处理成功"),
RESPONSE_CODE_PARAM_FORMAT_ERROR(10002,"请求失败、参数格式错误"),
RESPONSE_CODE_PARAM_ERROR(10003,"参数错误"),
RESPONSE_CODE_REQ_CANNOT_EMPTY(10004,"必要的请求参数不能为空"),
RESPONSE_CODE_USER_DOES_NOT_EXIST(10005,"用户不存在"),
RESPONSE_CODE_QUERY_SUCCESS(10006,"数据查询成功"),
RESPONSE_CODE_QUERY_NO_DATA(10007,"无数据或者结果"),
USER_LOGIN_PWD_ERROR(10008,"用户名密码错误"),
REQUEST_URL_NOT_SERVICE(10009,"访问了非服务路径"),
RESPONSE_CODE_QUERY_REMARKS_50(10010,"退货原因不得超过50字"),
RESPONSE_CODE_UNLOGIN_ERROR(421,"未登录异常"),
RESPONSE_CODE_LOGIN_SUCCESS(200,"用户登录成功"),
RESPONSE_CODE_NO_PERMISSION(403,"无权访问该系统"),
RESPONSE_CODE_SYSTEM_ERROR(500,"系统内部异常"),
RESPONSE_CODE_TIME_OUT(504,"上游服务端网关超时"),
REGISTER_MOBILE_EXIST(2001,"手机号码已经注册"),
REGISTER_VERIFICA_CODE_INVALID(2002,"验证码错误或者已经失效,请重新获取验证码"),
VERIFICA_CODE_ERROR(2003,"验证码不正确或失效"),
REFERRER_NOT_EXIST(2004, "推荐人不存在"),
PROXY_ACTIVATE_CODE_ERROR(2005, "激活码错误"),
USER_TRADE_PASSWORD_ERROR(2006,"用户交易密码错误"),
APP_VERSION_MISMATCHING(2007,"app版本号不是最新版本"),
USER_IDNO_NOT_MATCHING(2008, "用户身份证号码不匹配"),
BANK_CARD_MUST_HAVE_TO_ONE(2010, "银行卡必须保留一张"),
OLD_PASSWORD_ERROR(2011, "原密码不正确"),
NEW_PASSWORD_INCONSISTENT(2012, "新密码不一致"),
V_PASSWORD_REG(2013, "密码为数字或字母"),
TWO_PASSWORD_INCONSISTENCIES(2014, "密码和确认密码不一致"),
ACCEPT_PROTOCOL(2015, "请阅读和接受协议"),
LOGOUT_SUCESS(2016, "退出成功"),
TRANSCODE_FAILED(2017,"转码失败"),
APPLY_SALE_AFTER_STATUS_ERROR(2018,"申请售后失败,该订单未付款"),
NO_PAY_VALIDATE(2019,"支付未认证"),
PAY_VALIDATE_PROCESSING(2020,"支付人工审核中"),
PAY_VALIDATE_FAIL(2021,"支付人工审核失败,请重新填写"),
PAY_VALIDATE_SUCESS(2022,"支付人工审核成功"),
ACCOUNT_EXIST(2023,"账号已存在"),
ROLE_NAME_EXIST(2024,"角色已存在"),
PASSWORD_IS_BLENK(2025,"密码不能为空"),
DECRYPT_FAIL(2026,"参数解密失败"),
NOT_BINDING_MEMBER(2027,"未绑定会员"),
GET_ACCESSTOKEN_FAIL(2028,"获取accessToken失败"),
TWO_PASSWORD_CONSISTENCIES(2029, "新旧密码一致"),
NOT_PLATFORM_AUTH(2030, "该平台账号未授权或者已停用"),
AUTH_CODE_NOT_NULL(2031, "平台授权码不能为空");
private int code; //状态码
private String message; //返回消息
private static String version = "v1.0"; //版本号
// 构造方法
private ResponseCode(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(ResponseCode responseCode, Object data) {
return new ResponseVO(responseCode.getCode(),responseCode.getMessage(),data);
}
public static Map buildReturnMap(ResponseCode responseCode, Object data) {
Map map = new HashMap();
map.put("code", responseCode.getCode());
map.put("version", version);
map.put("message", responseCode.getMessage());
map.put("data", data);
return map;
}
/**
* 自定义状态码返回结果Map
* @param code 状态码
* @param message 消息提示
* @param data 数据
* @return
*/
public static Map customBuildReturnMap(String code, String message, Object data) {
if(StringUtils.isEmpty(code)){
code = "9999"; // 自定义错误消息状态码
}
Map map = new HashMap();
map.put("code", code);
map.put("version", version);
map.put("message", message);
map.put("data", data);
return map;
}
}
public class RedisClusterUtils {
@SuppressWarnings("rawtypes")
private static RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisClusterUtils.redisTemplate = redisTemplate;
}
static {
if (null == redisTemplate) {
RedisClusterUtils.redisTemplate = SpringContextHolder
.getBean(RedisTemplate.class);
}
}
/**
* 普通缓存获取
*
* @param key
* 键
* @return 值
*/
public static Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key
* 键
* @param value
* 值
* @return true成功 false失败
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public static boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time,
TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key
* 键
* @param by
* 要增加几(大于0)
* @return
*/
public static long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key
* 键
* @param by
* 要减少几(小于0)
* @return
*/
public static long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
*
* @param key
* 键 不能为null
* @param item
* 项 不能为null
* @return 值
*/
public static Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
public static Object hget(byte[] key, byte[] item) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setHashValueSerializer(stringSerializer);
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key
* 键
* @return 对应的多个键值
*/
public static Map
Jedis Configuration
#--------------redis settings--------------
redis.keyPrefix=
redis.host1=${redis.host1}
redis.port1=${redis.port1}
redis.host2=${redis.host2}
redis.port2=${redis.port2}
redis.host3=${redis.host3}
redis.port3=${redis.port3}
redis.host4=${redis.host4}
redis.port4=${redis.port4}
redis.host5=${redis.host5}
redis.port5=${redis.port5}
redis.host6=${redis.host6}
redis.port6=${redis.port6}
redis.maxTotal=1000
redis.maxIdle=100
redis.maxWaitMillis=1000
redis.timeout=30000
redis.testOnBorrow=true
redis.password=
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ResponseVO login(@RequestBody Member member, HttpServletRequest request, HttpServletResponse response) {
//获取平台标识
String strPlatformFlag = (String)request.getAttribute("platformFlag");
System.out.println("==============strPlatformFlag:=============" + strPlatformFlag);
member.setPlatformFlag(strPlatformFlag);
//判断必要参数是否为空
if (!StringUtils.isNotBlank(member.getLoginName()) || !StringUtils.isNotBlank(member.getPassword()) ||
!StringUtils.isNotBlank(member.getPlatformFlag()) ||!StringUtils.isNotBlank(member.getLoginType()) ||
!StringUtils.isNotBlank(member.getSource())) {
return ResponseCode.buildEnumResponseVO(ResponseCode.RESPONSE_CODE_REQ_CANNOT_EMPTY, null);
}
//请求参数,登录日志记录
String strParam = member.toString();
//判断用户是否存在
Member user = memberService.getByLoginName(member.getLoginName());
if(user == null ){
return ResponseCode.buildEnumResponseVO(ResponseCode.RESPONSE_CODE_USER_DOES_NOT_EXIST, null);
}
String strPassword = "";
String password = "";
try {
//RSA解密密码
strPassword = RSAUtils.decrypt(member.getPassword(), RSAUtils.getPrivateKey());
member.setPassword(strPassword);
password = MD5EncryptUtil.encryptMD5Code(member.getPassword());
}catch (Exception e){
return ResponseCode.buildEnumResponseVO(ResponseCode.DECRYPT_FAIL, null);
}
try {
//判断密码是否正确并添加token
if (StringUtils.equals(password, user.getPassword())) {
String token = JWT.sign(member.getSource() + user.getId(), 0);
//token在redis中的key等于redis_token+ 来源(0.公众号,1.PC,2.APP3.小程序) + 用户id
//token在redis中的value等于key加密
RedisClusterUtils.set(LoginInterceptor.REDIS_TOKEN_FLAG + member.getSource() + user.getId(), token, 1800);
Map dataMap = new HashMap();
dataMap.put("token", token);
dataMap.put("mid", user.getId());
return ResponseCode.buildEnumResponseVO(ResponseCode.RESPONSE_CODE_LOGIN_SUCCESS, JSONObject.fromObject(dataMap));
}
}catch (Exception e){
return ResponseCode.buildEnumResponseVO(ResponseCode.RESPONSE_CODE_FAILURE, null);
}
return ResponseCode.buildEnumResponseVO(ResponseCode.USER_LOGIN_PWD_ERROR, false);
}