1. 配置session属性
@Component
@ConfigurationProperties(prefix = "gcloud.session")
public class Nonprofessional {
private Integer maxLoginUser;
private Integer maxLoginFail;
private Integer showCaptchaTime;
getter / setter.......
2.key配置
public class Conts {
private static final String KEY_PREFIX = "gcloud_seccenter_mgr";
//项目模块_功能:具体key
private static final String KEY_FORMAT = "%s_%s:%s";
public static final class Lock {
public static final String SUB_PREFIX = "lock";
public static final String MAX_ONLINE_USER_CHECK_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "max_onlie_user_check");
public static final int MAX_ONLINE_USER_CHECK_EXPIRE_TIME = 5;
public static final long MAX_ONLINE_USER_CHECK_APPLY_EXOIRE_TIME = 5000L;
public static final String USER_LOGIN_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "user_login_{0}");
public static final int USER_LOGIN_EXPIRE_TIME = 5;
public static final long USER_LOGIN_APPLY_EXOIRE_TIME = 5000L;
}
//!!!!!!String 类型只能是图片验证码的KEY ,其他常量请放其他地方
public static final class Captcha{
public static final String SUB_PREFIX = "captcha";
public static final int DEFAULT_SHOW_CAPTCHA_TIME = 3;
public static final String USER_LOGIN_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "user_login");
public static final long USER_LOGIN_EXPIRE = 600000L;
}
public static final class RedisCaptcha{
public static final String SUB_PREFIX = "redisCaptcha";
public static final String FORGET_PASSWORD_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "forget_password_{0}");
public static final long FORGET_PASSWORD_EXPIRE = 600000L;
}
public static final class Session{
public static final String SUB_PREFIX = "session";
public static final String SESSION_LOGIN_FAIL_TIME = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "login_fail_time");
}
}
3.登录入参
public class LoginParam {
@NotBlank(message = "mgr_user_login_0001::username can not be null")
private String username;
@NotNull(message = "mgr_user_login_0002::password can not be null")
private String password;
private String captcha;//验证码
getter / setter......
}
4.session管理
public class SessionUtil {
private static final String DEFAULT_KEY_PREFIX = "spring:session:";
//online_user_userid_devicetype_sessionid
private static final String ONLINE_SESSION_PREFIX = "online_user";
@SuppressWarnings("unchecked")
private static RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getApplicationContext().getBean("redisTemplate");
public static String getUserDeviceOnlineKey(String userId, DeviceType deviceType, String sessionId){
return String.format("%s%s", getUserDeviceOnlieKeyPrefix(userId, deviceType), sessionId);
}
public static String getUserDeviceOnlieKeyPrefix(String userId, DeviceType deviceType){
return String.format("%s_%s_%s_", ONLINE_SESSION_PREFIX, userId, deviceType.getValue());
}
public static String getSessionKeysPattern(String sessionId){
return String.format("%s_*_%s", ONLINE_SESSION_PREFIX, sessionId);
}
public static String getOnlineUserPattern(DeviceType deviceType){
String pattern = String.format("%s_*", ONLINE_SESSION_PREFIX);
if(deviceType != null){
pattern = String.format("%s_*_%s_*", ONLINE_SESSION_PREFIX, deviceType.getValue());
}
return pattern;
}
public static String getUserOnlieKeyPrefix(String userId){
return String.format("%s_%s_", ONLINE_SESSION_PREFIX, userId);
}
public static Set<String> getOnlineUserKeys(DeviceType deviceType){
Set<String> keySet = redisTemplate.keys(getOnlineUserPattern(deviceType));
return keySet;
}
public static int getOnlineUserNumber(DeviceType deviceType){
Set<String> keySet = redisTemplate.keys(getOnlineUserPattern(deviceType));
return keySet == null ? 0 : keySet.size();
}
public static String getSessionId(String userDeviceSessionOnlineKey){
String sessionId = null;
if(userDeviceSessionOnlineKey != null){
String[] userSessionInfo = userDeviceSessionOnlineKey.split("_");
if(userSessionInfo.length > 0){
sessionId = userDeviceSessionOnlineKey.split("_")[userSessionInfo.length - 1];
}
}
return sessionId;
}
//查找在线用户
public static Set<String> getUserRelateOnlineKeys(String userId, DeviceType deviceType){
String userSessionKeyPrefix = null;
if(deviceType == null){
userSessionKeyPrefix = SessionUtil.getUserOnlieKeyPrefix(userId);
}else{
userSessionKeyPrefix = SessionUtil.getUserDeviceOnlieKeyPrefix(userId, DeviceType.WEB);
}
Set<String> keySet = redisTemplate.keys(userSessionKeyPrefix + "*");
return keySet;
}
public static Set<String> getSessionRelateOnlineKeys(String sessionId){
Set<String> keySet = redisTemplate.keys(getSessionKeysPattern(sessionId));
return keySet;
}
public static boolean isUserOnline(String userId){
Set<String> keySet = getUserRelateOnlineKeys(userId, null);
boolean isOnline = false;
if(keySet != null && keySet.size() > 0){
isOnline = true;
}
return isOnline;
}
public static void refreshUserDeviceOnlineKey(HttpSession session){
if(session == null){
return;
}
SessionUser sessionUser = (SessionUser) session.getAttribute(HttpRequestConstant.SESSION_USER_INFO);
if(sessionUser != null){
String key = getUserDeviceOnlineKey(sessionUser.getUserId(), sessionUser.getDeviceType(), session.getId());
long expireTime = session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000;
Date expireDate = new Date(expireTime);
redisTemplate.expireAt(key, expireDate);
}
}
public static String getKeyPrefix(String namespace){
String prefix = DEFAULT_KEY_PREFIX;
if(StringUtils.isNotBlank(namespace)){
prefix = prefix + namespace + ":";
}
return prefix;
}
public static String getExpirationKey(String namespace, long expiration){
return getKeyPrefix(namespace) + "expirations:" + expiration;
}
}
5.登录设备
public enum DeviceType implements Serializable {
WEB(0, "web", "web浏览器"),
DESKTOP(1, "desktop", "桌面应用"),
APP(2, "app", "手机APP");
private int value;
private String name;
private String cnName;
DeviceType(int value, String name, String cnName) {
this.value = value;
this.name = name;
this.cnName = cnName;
}
public static DeviceType getByValue(Integer value){
DeviceType result = null;
if(value != null){
for(DeviceType type : DeviceType.values()){
if(type.getValue() == value){
result = type;
break;
}
}
}
return result;
}
public static Map<Integer, String> getValueCnMap(){
Map<Integer, String> result = new HashMap<Integer, String>();
for(DeviceType type : DeviceType.values()){
result.put(type.getValue(), type.getCnName());
}
return result;
}
public int getValue() {
return value;
}
public String getName() {
return name;
}
public String getCnName() {
return cnName;
}
}
6.配置redis session
/*
* @Desccription redis session 配置
*/
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
//默认1个小时
@Value("${server.session.timeout:3600}")
private Integer maxInactiveInterval;
@Value("${spring.session.redis.namespace:}")
private String namespace;
@Autowired
private RedisOperationsSessionRepository sessionRepository;
@PostConstruct
private void afterPropertiesSet() {
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveInterval);
if(StringUtils.isNotBlank(namespace)){
sessionRepository.setRedisKeyNamespace(namespace);
}
}
}
7.session用户信息
/*
* @Desccription session用户信息
*/
public class SessionUser implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String username;
private DeviceType deviceType;
getter / setter......
}
8.redis配置(多台redis环境下)
/*
* @Desccription jedis client 配置 RedisTemplate 的set不能使用 set EX PX [NX|XX] 所以改用jedis
*/
@Configuration
@ConditionalOnExpression("${gcloud.redis.jedisClient.enable:false} == true")
public class JedisConfig {
@Value("${spring.redis.host:}")
private String host;
@Value("${spring.redis.port:}")
private Integer port;
@Value("${spring.redis.timeout:}")
private Integer timeout;
@Value("${spring.redis.pool.max-idle:}")
private Integer maxIdle;
@Value("${spring.redis.pool.max-wait:}")
private Long maxWaitMillis;
@Value("${spring.redis.pool.max-active:}")
private Integer maxActive;
@Value("${spring.redis.password:}")
private String password;
@Bean
public ShardedJedisPool shardedJedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
if(maxActive != null){
jedisPoolConfig.setMaxTotal(maxActive);
}
if(maxIdle != null){
jedisPoolConfig.setMaxIdle(maxIdle);
}
if(maxWaitMillis != null){
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
}
List<JedisShardInfo> jedisShardInfoList = new ArrayList<JedisShardInfo>();
jedisShardInfoList.add(new JedisShardInfo(host, port));
return new ShardedJedisPool(jedisPoolConfig, jedisShardInfoList);
}
@Bean
public JedisClientTemplate jedisClientTemplate(){
return new JedisClientTemplate();
}
@Bean
public RedisLock redisLock(){
return new RedisSpinLock();
}
}
9.jedisclient客户端
/*
* @Desccription jedisclient客户端
*/
public class JedisClientTemplate {
@Autowired
private ShardedJedisPool shardedJedisPool;
/**
* 设置单个值
* @param key
* @param value
* @param nxxx NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key
* if it already exist.
* @param expx EX|PX, expire time units: EX = seconds; PX = milliseconds
* @param time expire time in the units of <code>expx</code>
* @return Status code reply
*/
public String set(String key, String value, String nxxx, String expx, long time) {
String result = null;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
result = shardedJedis.set(key, value, nxxx, expx, time);
}
return result;
}
/**
* 设置单个值
*
* @param key
* @param value
* @return
*/
public String set(String key, String value){
String result = null;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
result = shardedJedis.set(key, value);
}
return result;
}
/**
* 获取单个值
*
* @param key
* @return
*/
public String get(String key){
String result = null;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
result = shardedJedis.get(key);
}
return result;
}
public Boolean exists(String key) throws GCloudException {
Boolean result = false;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
result = shardedJedis.exists(key);
}
return result;
}
/**
*
* @Title: 非空才新增,并且加上超时时间
* @Description: 以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists
* @date 2016-10-18 上午11:37:35
*
* @param key
* @param value
* @param time 超时时间,单位:秒
* @return 如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
* 如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
* @throws GCloudException
*/
public Long setnx(String key, String value, long time) throws GCloudException {
Long result = null;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
String res = shardedJedis.set(key, value, "NX", "EX", time);
if (StringUtils.isNotBlank(res) && res.equalsIgnoreCase("ok")) {
result = 1l;
} else {
result = 0l;
}
}
return result;
}
public Long del(String key) throws GCloudException {
Long result = null;
try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
result = shardedJedis.del(key);
}
return result;
}
}
10.redis锁
public abstract class RedisLock {
public abstract String getLock(String lockName, int lockTimeout, long getLockTimeout) throws GCloudException;
public abstract void releaseLock(String lockName, String value) throws GCloudException;
}
@Slf4j
public class RedisSpinLock extends RedisLock {
@Autowired
private JedisClientTemplate jedisClientTemplate;
@Override
public String getLock(String lockName, int lockTimeout, long getLockTimeout) throws GCloudException {
boolean isSucc = false;
boolean isGetTimerout = false;
String value = UUID.randomUUID().toString(); // 这个值用于删除
long startTime = System.currentTimeMillis();
do {
if (jedisClientTemplate.setnx(lockName, value, lockTimeout) == 1L) {
isSucc = true;
} else {
isSucc = false;
}
if (getLockTimeout >= 0) {
isGetTimerout = System.currentTimeMillis() - startTime > getLockTimeout;
}
} while (!isSucc && !isGetTimerout);
// 不成功直接抛错
if (!isSucc) {
throw new GCloudException("::get lock fail");
}
return value;
}
@Override
public void releaseLock(String lockName, String value) throws GCloudException {
try{
String v = jedisClientTemplate.get(lockName);
if (v != null && v.equals(value)) {
jedisClientTemplate.del(lockName);
}
}catch (Exception ex){
log.error("释放锁失败", ex);
}
}
}
11.登录业务
@Service
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private UserRoleDao userRoleDao;
@Autowired
private SessionRepository<? extends Session> sessionRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private GcloudSessionProp gcloudSessionProp;
@Autowired
private UserRoleService userRoleService;
@Autowired
private UserInfoService userInfoService;
@Autowired
private RedisOperationsSessionRepository redisRepository;
@Autowired
private RedisSessionProp redisSessionProp;
@Autowired
private MenuDao menuDao;
private final int DEFAULT_MAX_FAIL_TIME = 5;
//接入cas时,此方法不使用
@Override
@Transactional(noRollbackFor = GcTransactionException.class)
public LoginResponse login(LoginParam param, HttpServletRequest request) throws GCloudException {
HttpSession session = request.getSession();
Captcha captcha = (Captcha) session.getAttribute(Conts.Captcha.USER_LOGIN_KEY);
Integer sessionLoginFailTime = session.getAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME) == null ? 0 : (Integer) session.getAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME);
Integer showTime = gcloudSessionProp.getShowCaptchaTime() == null ? Conts.Captcha.DEFAULT_SHOW_CAPTCHA_TIME : gcloudSessionProp.getShowCaptchaTime();
//有验证码或者已经达到失败次数,都应该进行验证码验证
if(captcha != null || (showTime > 0 && sessionLoginFailTime >= showTime)){
if(captcha == null){
throw new GCloudException("mgr_user_login_0013::captcha is not correct");
}
if(StringUtils.isBlank(param.getCaptcha())){
throw new GCloudException("mgr_user_login_0012::captcha can not be null");
}
if(System.currentTimeMillis() > captcha.getTimeout()){
throw new GCloudException("mgr_user_login_0010::captcha is invalidate");
}
if(!param.getCaptcha().equals(captcha.getCode())){
throw new GCloudException("mgr_user_login_0011::captcha is not correct");
}
//验证成功,则去掉验证码
removeSessionCaptcha(session, Conts.Captcha.USER_LOGIN_KEY);
}
//同一个用户需要计算失败次数,并锁定用户,所以需要锁定, 后面需要update,这里使用数据库锁
User user = userDao.getValidateUserForUpdate(param.getUsername());
if(user == null){
throw new GCloudException("mgr_user_login_0003::user does not exist");
}
if(user.getIsLock() != null && user.getIsLock()){
throw new GCloudException("mgr_user_login_0009::user was locked");
}
String inputPwdMd5 = null;
try {
inputPwdMd5 = MD5Util.encrypt(param.getPassword());
} catch (Exception ex) {
log.error("mgr_user_login_0004,密码md5加密失败", ex);
throw new GCloudException("mgr_user_login_0004::password encrypt error");
}
if (inputPwdMd5 == null || !inputPwdMd5.equalsIgnoreCase(user.getPassword())) {
//session的失败次数,用于判断是否显示验证码
sessionLoginFailTime++;
session.setAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME, sessionLoginFailTime);
int totalFail = user.getLoginFailCount() == null ? 1 : user.getLoginFailCount() + 1;
int maxFailTime = gcloudSessionProp.getMaxLoginFail() == null ? DEFAULT_MAX_FAIL_TIME : gcloudSessionProp.getMaxLoginFail();
boolean isLock = maxFailTime > 0 && totalFail > maxFailTime;
userDao.loginFail(user.getId(), isLock);
throw new GcTransactionException("mgr_user_login_0005::password is not correct");
}
Integer loginNumLimit = gcloudSessionProp.getMaxLoginUser();
//做最大在线人数限制
String lockId = "";
if(loginNumLimit != null && loginNumLimit >= 0){
//防止并发导致最大在线人数超过限制,需要需要同步锁
lockId = LockUtil.spinLock(Conts.Lock.MAX_ONLINE_USER_CHECK_KEY, Conts.Lock.MAX_ONLINE_USER_CHECK_EXPIRE_TIME, Conts.Lock.MAX_ONLINE_USER_CHECK_APPLY_EXOIRE_TIME, "mgr_user_login_0006::The number of logged users has reached the maximum");
}
try{
//web端
//同一端的不同浏览器,已经登录,则踢出。同时因为已经登录,所以不需要检测最大在线人数
Set<String> userDeviceSessionKeys = SessionUtil.getUserRelateOnlineKeys(user.getId(), DeviceType.WEB);
if(userDeviceSessionKeys != null && userDeviceSessionKeys.size() > 0){
for(String userDeviceSessionKey : userDeviceSessionKeys){
String sessionId = SessionUtil.getSessionId(userDeviceSessionKey);
Session redisSession = sessionRepository.getSession(sessionId);
if(redisSession != null){
redisSession.removeAttribute(HttpRequestConstant.SESSION_USER_INFO);
removeSessionAllCaptcha(redisSession);
redisRepository.delete(sessionId);
deleteExpirations((ExpiringSession) redisSession);
// sessionRepository.save(redisSession);
}
redisTemplate.delete(userDeviceSessionKey);
}
}else if(loginNumLimit != null && loginNumLimit >= 0){
int onlineUserNumber = SessionUtil.getOnlineUserNumber(DeviceType.WEB);
if(loginNumLimit <= onlineUserNumber){
throw new GCloudException("mgr_user_login_0007::The number of logged users has reached the maximum");
}
}
userDao.loginSuccess(user.getId());
if(session != null){
session.invalidate();
}
session = request.getSession();
SessionUser sessionUser = new SessionUser();
sessionUser.setUserId(user.getId());
sessionUser.setUsername(user.getUsername());
sessionUser.setDeviceType(DeviceType.WEB);
session.setAttribute(HttpRequestConstant.SESSION_USER_INFO, sessionUser);
//登录成功,去掉登录失败次数
session.removeAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME);
String userSessionKey = SessionUtil.getUserDeviceOnlineKey(user.getId(), DeviceType.WEB, session.getId());
BoundValueOperations<String, Object> valueOperations = redisTemplate.boundValueOps(userSessionKey);
valueOperations.set(userSessionKey);
long expireTime = session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000;
Date expireDate = new Date(expireTime);
valueOperations.expireAt(expireDate);
}catch(Exception ex){
if(ex instanceof GCloudException){
throw ex;
}else{
log.error("未知错误", ex);
throw new GCloudException("mgr_user_login_0008::login fail");
}
}finally {
if(StringUtils.isNotBlank(lockId)){
LockUtil.releaseSpinLock(Conts.Lock.MAX_ONLINE_USER_CHECK_KEY, lockId);
}
}
LoginResponse response = new LoginResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
return response;
}