大家好,我是烤鸭:
今天给大家分享简单的单点登录的实现方式。
环境及jar包:
springboot 1.5.10
redis 2.9.0 (可以用tomcat的session,但是无法解决多个tomcat共享session的问题)
shiro 1.4.0
lombok 1.16.10 (可选,编译的时候自动生成get,set,toString()方法)
登录方法:
主要是用户完成登录,登录信息写在cookie,
服务器端存缓存(redis)中。
LoginService:
package xxx.xxx.system.service.impl; import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.fastjson.JSONObject; import xxx.xxx.common.constant.IMsgEnum; import xxx.xxx.common.resp.BaseMsgResp; import xxx.xxx.common.utils.MD5Utils; import xxx.xxx.entity.sys.UserDO; import xxx.xxx.redis.cache.RedisManager; import xxx.xxx.system.service.LoginService; import xxx.xxx.system.service.UserService; @Service public class LoginServiceImpl implements LoginService { @Autowired private UserService userService; @Override public BaseMsgResp login(String username, String password) throws AuthenticationException{ BaseMsgResp resp = new BaseMsgResp(); password = MD5Utils.encrypt(username, password); UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); //获取用户 Map<String, Object> params = new HashMap<>(); params.put("username", username); List<UserDO> list = userService.list(params); if(list.size() > 1) { resp = new BaseMsgResp(IMsgEnum.SYSTEM_ANOMALY.getMsgCode()+"",IMsgEnum.SYSTEM_ANOMALY.getMsgText()); return resp; } //token Serializable id = subject.getSession().getId(); //将token放入redis RedisManager manager = RedisManager.getRedisSingleton(); manager.set("sys:login:user_token_"+id.toString(),list.get(0).getUserId()+"",60*30); //防止同一个账号同时登录 manager.set("sys:user:id_"+list.get(0).getUserId(), id.toString(),60*30); //用户信息 manager.set("sys:login:user_info_"+list.get(0).getUserId(), JSONObject.toJSONString(list.get(0)),60*30); return resp; } }
RedisManager.java
用于redis初始化,和一些基本方法:
packagexxx.xxx.redis.cache; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * @author * @version V1.0 */ import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import com.alibaba.fastjson.JSONObject; import xxx.xxx.redis.config.RedisConstant; import lombok.Data; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * */ @Configuration @ConfigurationProperties(value="jedis.pool") @Data public class RedisManager { private final Logger logger = LoggerFactory.getLogger(this.getClass()); volatile static RedisManager redisSingleton; private String host; private int port; private int expire; private int timeout; private String password = ""; private static JedisPool jedisPool = null; //第几个仓库 private String database; public String getDatabase() { if(null == database || "".equals(database)) return "0"; return database; } public void setDatabase(String database) { this.database = database; } public RedisManager() { } public static RedisManager getRedisSingleton() { if(redisSingleton == null) { synchronized (RedisManager.class) { if(redisSingleton == null) return new RedisManager(); } } return redisSingleton; } /** * 初始化方法 */ public void init() { if (jedisPool == null) { if (password != null && !"".equals(password)) { jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout, password); } else if (timeout != 0) { jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout); } else { jedisPool = new JedisPool(new JedisPoolConfig(), host, port); } } } /** * 给Redis中Set集合中某个key值设值 * * @param key * @param value */ public void set(String key, String value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.select(Integer.parseInt(getDatabase())); jedis.set(key, value); } catch (Exception e) { logger.error("Jedis set 异常" + e.getMessage()); } finally { if (jedis != null) { jedis.close(); } } } /** * 从Redis中Set集合中获取key对应value值 * * @param key */ public String get(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.select(Integer.parseInt(getDatabase())); return jedis.get(key); } catch (Exception e) { logger.error("Jedis get 异常" + e.getMessage()); return null; } finally { if (jedis != null) { jedis.close(); } } } /** * 给Redis中Set集合中某个key值设值 * * @param key * @param value */ public void set(String key, String value, long time) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.select(Integer.parseInt(getDatabase())); jedis.set(key, value); jedis.set(key, value, "XX", "EX", time); } catch (Exception e) { logger.error("Jedis set 异常" + e.getMessage()); } finally { if (jedis != null) { jedis.close(); } } } /** * * @Title: expire * @Description: 设置指定key的生存时间 * @return Long 成功 返回 1 * @throws */ public Long expire(String key,int seconds) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.select(Integer.parseInt(getDatabase())); return jedis.expire(key, seconds); } catch (Exception e) { logger.error("Jedis expire 异常" + e.getMessage()); return null; } finally { if (jedis != null) { jedis.close(); } } } /** * * @Title: exist * @Description: 判断key是否存在 * @return Boolean 返回类型 * @throws */ public Boolean exist(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.select(Integer.parseInt(getDatabase())); return jedis.exists(key); } catch (Exception e) { logger.error("Jedis exist 异常" + e.getMessage()); return null; } finally { if (jedis != null) { jedis.close(); } } } }
LoginInterceptor:
每次请求都会经过拦截器,校验用户是否登录或者多个浏览器(客户端)登录。
如果用户已登录,用这次请求cookie中的token和缓存中的比较,如果不一致,就把之前缓存中的用户信息清空。
这样之前的用户如果再次操作,就会跳转到登陆页面。
package xxx.xxx.system.interceptor; import java.io.IOException; import java.io.Serializable; import java.lang.ProcessBuilder.Redirect; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSONObject; import xxx.xxx.common.utils.LogUtils; import xxx.xxx.redis.cache.RedisManager; import xxx.xxx.system.utils.ShiroUtils; /** * ClassName: PlatformInterceptor date: 2015年12月30日 下午2:13:24 Description: 拦截器 * * @author * @version * @since JDK 1.8 */ @Component @EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class) public class LoginInterceptor implements HandlerInterceptor { private static final Log logger = LogFactory.getLog(LoginInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { try { logger.info(LogUtils.getRequestLog(request)); //校验用户是否已经登录,如果登录过,将之前用户踢掉,同时更新缓存中用户信息 Subject subject = SecurityUtils.getSubject(); Serializable token = subject.getSession().getId(); RedisManager redisManager = RedisManager.getRedisSingleton(); //获取用户id String userId = redisManager.get("sys:login:user_token_"+token.toString()); if(StringUtils.isNotBlank(userId)) { String tokenPre = redisManager.get("sys:user:id_"+userId); if(!token.equals(tokenPre)) { //重定向到login.html redirect(request, response); return false; }else { Long expire = redisManager.ttl("sys:login:user_token_"+token.toString()); //过期时间小于1分钟的,更新token if(expire < 1 * 60 * 1000) { redisManager.expire("sys:login:user_token_"+token.toString(), 60*30); } } }else { redirect(request, response); return false; } } catch (Exception e) { logger.info("preHandle="+e.getMessage()); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } //对于请求是ajax请求重定向问题的处理方法 public void redirect(HttpServletRequest request, HttpServletResponse response) throws IOException{ //获取当前请求的路径 String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()+request.getContextPath(); // response.getOutputStream().write("账号在别处登录。".getBytes("UTF-8")); //如果request.getHeader("X-Requested-With") 返回的是"XMLHttpRequest"说明就是ajax请求,需要特殊处理 否则直接重定向就可以了 if("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))){ //告诉ajax我是重定向 response.setHeader("REDIRECT", "REDIRECT"); //告诉ajax我重定向的路径 response.setHeader("CONTENTPATH", basePath+"/login"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); }else{ response.sendRedirect(basePath + "/login"); } } }
ShiroUtils.java
shiro 工具类,获取subject对象。
package xxx.xxx.system.utils; import java.security.Principal; import java.util.Collection; import java.util.List; import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import xxx.xxx.entity.sys.UserDO; public class ShiroUtils { @Autowired private static SessionDAO sessionDAO; public static Subject getSubjct() { return SecurityUtils.getSubject(); } public static UserDO getUser() { Object object = getSubjct().getPrincipal(); return (UserDO)object; } public static Integer getUserId() { return getUser().getUserId(); } public static void logout() { getSubjct().logout(); } public static List<Principal> getPrinciples() { List<Principal> principals = null; Collection<Session> sessions = sessionDAO.getActiveSessions(); return principals; } }
UserDO.java
用户对象。
package xxx.xxx.entity.sys; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.util.Date; import java.util.List; public class UserDO implements Serializable { private static final long serialVersionUID = 1L; //用户id private Integer userId; // 用户名 private String username; // 用户真实姓名 private String name; // 密码 private String password; }
application.yml
jedis : pool : host : 127.0.0.1 port : 9001 password: abcd123 maxTotal: 100 maxIdle: 10 maxWaitMillis : 100000