1单点登录简单描述一下需求,
有另外两个项目A、B,需要在A或者B登陆后,打开另一个项目的网页也是登陆中的状态,但是如果在B登出,A的登录状态也应该失效,不过A,B是相同的顶级域名实际上cookie可以互用
实际上实现是登录请求在服务端,服务端登陆后会分发一个token,在cookie中存入加密后的token,在客户端登陆时拦截器中取到token,拿token去服务端进行校验.
如果登录会让token过期,使得之前页面的登录状态失效.
2服务端代码
//登录接口
@PostMapping(value = "/dologin")
public String dologin(HttpSession session, UserVo user, Model model, HttpServletResponse response) {
try {
if(user == null) {
return "login";
}
if(StrUtil.isNotBlank(user.getService())) {
model.addAttribute("serviceUrl", user.getService());
}
//尝试查找用户库是否存在
if(StrUtil.isNotBlank(user.getPassword()) && StrUtil.isNotBlank(user.getLoginName())) {
SysUser sysUser = sysUserService.selectByLoginName(user.getLoginName());
if (sysUser == null) {
model.addAttribute("message", ResponseCode.USER_NOT_EXISTS.getMessage());
}else {
if (sysUser.getStatus() == 3) {
model.addAttribute("message", ResponseCode.IMAGE_CODE_ERROR.getMessage());
}
//校验密码是否正确
String password = user.getPassword();
String passwordMD5Encode = MD5.createPassword(password, sysUser.getPasswordSalt(), 2);
if(!StrUtil.equals(passwordMD5Encode, sysUser.getPassword())) {
model.addAttribute("message", ResponseCode.LOGIN_ACCOUNT_PWD_ERROR.getMessage());
}else {
Long userId = sysUser.getId();
//1.创建令牌信息
String token = TokenUtil.creatToken(userId);
//2.创建全局的会话,把令牌信息放入会话中.
session.setAttribute("token",token);
session.setMaxInactiveInterval(DateUtil.DAY_SECONDS);
//3.将token存入ridis当中
ssoCache.hset(userId.toString(), "token", token);
//将token放入顶级域名的cookie中
Cookie cookie = new Cookie(Constant.SSO_TOKEN, token);
cookie.setMaxAge(DateUtil.MONTH_SECONDS);
cookie.setPath("/");
if(!StringUtils.equals(domain_top, "localhost")){
cookie.setDomain(domain_top);
}
response.addCookie(cookie);
//重定向地址
if(StrUtil.isNotBlank(user.getService())) {
response.sendRedirect(user.getService());
}else {
response.sendRedirect(adminDomain + "/help/sunce/cmaudit-pending-list");
}
}
}
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
LOGGER.info("[{}] is logining", user.getLoginName());
return "login";
}
//登录页面
@RequestMapping("login")
public String login(HttpSession session, Model model, String service, HttpServletResponse response) throws IOException {
//1.判断是否有全局的会话
//从会话中获取令牌信息,如果取不到说明没有全局会话,如果能取到说明有全局会话
String token = (String) session.getAttribute("token");
if(StrUtil.isNotBlank(token)){
//有全局会话,校验token是否是有效的
Long userId = TokenUtil.getTokenDataUserId(token);
String tokenCache = ssoCache.hget(userId.toString(), "token");
//校验token是否与缓存中的相同
if(StrUtil.isNotBlank(tokenCache) && StrUtil.equals(token, tokenCache)) {
response.sendRedirect(service);
}
}
model.addAttribute("serviceUrl",service);
return "/login";
}
//用于验证token
@PostMapping(value = "/verify")
@ResponseBody
public ResponseObject verify(String tokenStr) {
if(StrUtil.isBlank(tokenStr)) {
return ResponseObject.error("token为空");
}
try {
Long userId = TokenUtil.getTokenDataUserId(tokenStr);
if(userId == null) {
return ResponseObject.error("userId为空");
}
//校验token是否与缓存中的相同
String tokenCache = ssoCache.hget(userId.toString(), "token");
if(StrUtil.isBlank(tokenCache) || !StrUtil.equals(tokenStr, tokenCache)) {
return ResponseObject.error("登陆失效");
}
} catch (Exception e) {
return ResponseObject.error("token异常");
}
return ResponseObject.success("验证成功");
}
//登出接口
@RequestMapping(value = "/logout")
public String loginOut(HttpSession session, HttpServletResponse response) {
//从会话中获取令牌信息,如果取不到说明没有全局会话,如果能取到说明有全局会话
String token = (String) session.getAttribute("token");
if(StrUtil.isNotBlank(token)) {
//有全局会话,删除缓存中的token
Long userId = TokenUtil.getTokenDataUserId(token);
ssoCache.hdel(userId.toString(), "token");
}
return "/login";
}
3客户端代码
在拦截器中实现
由于是前后端分离业务所以和前端约定好返回code码为301时,视作登录失败
3.1拦截器中处理代码放在请求发生前preHandle中
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler, HttpSession session)
throws Exception {
if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
try {
userService.setSessionId(response, request); // 每个页面添加cookie
ResponseBody responseBody = method.getAnnotation(ResponseBody.class);
if (responseBody == null && !"void".equals(method.getReturnType().getTypeName())) {
setCurrentUrlInCookie(response, request);// 把当前路径的url放入cookie中
}
// 权限检查
checkAndSetAuth(method, request, response, session);
} catch (Throwable ex) {
ex.printStackTrace();
log.error(ex.getMessage());
ResponseBody responseBody = method.getAnnotation(ResponseBody.class);
if (responseBody != null && ex instanceof BizException) {
BizException bizEx = (BizException) ex;
if (bizEx.getResultCode() != null && bizEx.getResultCode() == ResultCode.NOT_LOGIN) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
ResponseObject responseObject = new ResponseObject();
responseObject.setResultCode(ResultCode.NOT_LOGIN);
response.getWriter().append(new Gson().toJson(responseObject));
response.getWriter().flush();
response.getWriter().close();
response.setStatus(301);
}
}
UserHolder.clear();
return false;
}
}
return true;
}
3.2实际校验代码
HttpSession session = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest().getSession();
String token = (String) session.getAttribute("token");
if(StrUtil.isEmpty(token)){
//表示没有局部会话
token = CookieUtil.getToken();
}
//尝试去掉session只从cookie中获取
String token = CookieUtil.getToken();
SysUser user = null;
Object userObj = null;
//有局部会话,校验token是否是有效的
if (StrUtil.isNotBlank(token) && verify(token)) {
userObj = helpCache.getObj(token);
if (userObj != null && userObj instanceof SysUser) {
user = (SysUser) userObj;
}
if (user == null) {
Long userId = TokenUtil.getTokenDataUserId(token);
user = baseMapper.selectById(userId);
}
return user;
}
//通过Http请求去服务端校验token是否有效
public Boolean verify(String tokenStr) {
if(StrUtil.isBlank(tokenStr)) {
return false;
}
Map<String, String> quarm = new HashMap<String, String>();
quarm.put("tokenStr", tokenStr);
try {
Map<String, Object> result = OKHttpUtil.POST(loginDomain + "/verify", quarm);
JSONObject jsonObject = JSONObject.parseObject((String)result.get("strResponse")) ;
if(jsonObject.containsKey("success") && jsonObject.getBoolean("success")) {
return true;
}else {
return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
4 加密
使用JWT(JSON Web Token)加密
public class TokenUtil {
//过期时间设置(30分钟)
private static final long EXPIRE_TIME = 30*60*1000;
private static final String TOKEN_SECRET = "5xcJVrXNyQDIxK1l2RS9nw";
public static String getToken(Token token){
//过期时间和加密算法设置
Date date=new Date(System.currentTimeMillis()+EXPIRE_TIME);
Algorithm algorithm =Algorithm.HMAC256(TOKEN_SECRET);
//头部信息
Map<String,Object> header=new HashMap<>(2);
header.put("typ","JWT");
header.put("alg","HS256");
return JWT.create()
.withHeader(header)
.withClaim("userId", token.getUserId())
.withExpiresAt(date)
.sign(algorithm);
}
public static Token getTokenData(String token){
DecodedJWT jwt = JWT.decode(token);
Token tk = new Token();
tk.setUserId(jwt.getClaim("userId").asLong());
return tk;
}
public static String creatToken(Long userId){
//这里是传入的是token对象,决定token的内容
Token tk=new Token();
//获取时间用
Date date=new Date();
tk.setUserId(userId);
//交给上面的实现类得到token
return getToken(tk);
}
public static Long getTokenDataUserId(String token){
return JWT.decode(token).getClaim("userId").asLong();
}
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUzMjE3MjcsInVzZXJJZCI6NDc4fQ.DwrZO4zofq_QNZZv0xfRDNJHrvlQonroopdqCn1Akf0";
System.out.println(JWT.decode(token).getClaim("userId").asLong());
}
}