单点登录
什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分
例如在访问网易账号中心(http://reg.163.com/ )登录成功后
以下站点都是登录状态
- 网易直播 http://v.163.com
- 网易博客 http://blog.163.com
- 网易花田 http://love.163.com
- 网易考拉 https://www.kaola.com
- 网易Lofter http://www.lofter.com
单点登录解决的问题
- 用户身份信息独立管理,更好的分布式管理
- 可以自行拓展安全策略
- 解决跨域问题
单点登录设计
1.单点登录时序图
2.对上图的一个简要描述
- 用户访问web应用,发现此用户没有登录,重新定向到认证中心,并将自己的url作为参数传递
- 用户输入账号密码提交登录申请,认证中心去数据库核对用户信息
- 用户信息核对完成后存入缓存,并且生成token
- 用户带着token再次访问web应用,跳转到web应用功能页面,并将token存入cookie中
- 用户继续访问其他功能页面,首先检查cookie数据,然后拿着token重新提交认证中心
- 认证成功,跳转到业务功能页面
使用技术
- cookie:缓存token信息
- redis:缓存用户信息
- JWT:JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案(防伪)
- 拦截器:自定义拦截器,重写 preHandle方法
- 自定义注解:判断用户是否登录
- httpClientUtils :HTTP客户端编程工具包
SSO实现
1.所使用到部分的依赖
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--redis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.4</version>
</dependency>
<!--es-->
2.拦截器
package com.atguigu.commerce.config;
import com.alibaba.fastjson.JSON;
import com.atguigu.commerce.util.HttpClientUtil;
import io.jsonwebtoken.impl.Base64UrlCodec;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.atguigu.commerce.config.CookieUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.Map;
/**
* @description
* @Version:V1.0
* @params
* @return
* @auther: zxj
* @date: 2020/4/18 10:39
*/
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
//用户进入控制器之前
//多个拦截器执行顺序,跟拦截器的顺序有关系
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如何获取到token
//用户在登录完成会获得token
String token = request.getParameter("newToken");
// 将token 放入cookie 中!
// Cookie cookie = new Cookie("token",token);
// response.addCookie(cookie);
// 当token 不为null 时候放cookie
if (token!=null){
CookieUtil.setCookie(request,response,"token",token,WebConst.COOKIE_MAXAGE,false);
}
// 当用户访问非登录之后的页面,登录之后,继续访问其他业务模块时,url 并没有newToken,但是后台可能将token 放入了cookie 中!
if (token==null){
token = CookieUtil.getCookieValue(request,"token",false);
}
// 从cookie 中获取token,解密token!
if (token!=null){
// 开始解密token 获取nickName
Map map = getUserMapByToken(token);
// 取出用户昵称
String nickName = (String) map.get("nickName");
// 保存到作用域
request.setAttribute("nickName",nickName);
}
// 在拦截器中获取方法上的注解!
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法上的注解LoginRequire
LoginRequire methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequire.class);
if (methodAnnotation!=null){
// 此时有注解 ,
// 判断用户是否登录了? 调用verify
// 获取服务器上的ip 地址
String salt = request.getHeader("X-forwarded-for");
//"192.168.217.1";
// 调用verify()认证 http://passport.atguigu.com/verify
String result = HttpClientUtil.doGet(WebConst.VERIFY_ADDRESS + "?token=" + token + "&salt=" + salt);
if ("success".equals(result)){
// 登录,认证成功!
// 保存一下userId
// 开始解密token 获取nickName
Map map = getUserMapByToken(token);
// 取出userId
String userId = (String) map.get("userId");
// 保存到作用域
request.setAttribute("userId",userId);
return true;
}else {
// 认证失败!并且 methodAnnotation.autoRedirect()=true; 必须登录
if (methodAnnotation.autoRedirect()){
// 必须登录!跳转到页面
// 先获取到url
String requestURL = request.getRequestURL().toString();
System.out.println("requestURL:"+requestURL); // http://item.gmall.com/36.html
// 将url 进行转换
// http%3A%2F%2Fitem.gmall.com%2F36.html
String encodeURL = URLEncoder.encode(requestURL, "UTF-8");
System.out.println("encodeURL:"+encodeURL); // http%3A%2F%2Fitem.gmall.com%2F36.html
// http://passport.atguigu.com/index?originUrl=http%3A%2F%2Fitem.gmall.com%2F36.html
//LOGIN_ADDRESS="http://passport.atguigu.com/index";
response.sendRedirect(WebConst.LOGIN_ADDRESS+"?originUrl="+encodeURL);
return false;
}
}
}
return true;
}
private Map getUserMapByToken(String token) {
//解吗token获取map数据
//1.获取token中间部分
String tokenUserInfo = StringUtils.substringBetween(token, ".");
//进行base64解吗
Base64UrlCodec base64UrlCodec = new Base64UrlCodec();
//得到byte数组
byte[] decode = base64UrlCodec.decode(tokenUserInfo);
//将获得decode转换为字符串
String mapJson=null;
try {
mapJson = new String(decode, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return JSON.parseObject(mapJson,Map.class);
}
//用户进入控制器之后,视图渲染之前
// 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 {
// }
}
3.PassPortConller
package com.atguigu.commerce.passport.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.atguigu.commerce.bean.UserInfo;
import com.atguigu.commerce.passport.until.JwtUtil;
import com.atguigu.commerce.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Controller
public class PassPortController {
@Value("${token.key}")
private String key;
@Reference
private UserService userService;
@RequestMapping("index")
public String index(HttpServletRequest request){
//获取originUrl
String originUrl = request.getParameter("originUrl");
//保存originUrl
request.setAttribute("originUrl",originUrl);
return "index";
}
@RequestMapping("login")
@ResponseBody
public String login( UserInfo userInfo,HttpServletRequest request){
//获取salt部分,服务器IP地址
String salt=request.getHeader("X-forwarded-for");
//调用登录方法
UserInfo info =userService.login(userInfo);
if (info!=null)
{
//制作token
HashMap<String, Object> map = new HashMap<>();
map.put("userId",info.getId());
map.put("nickName",info.getNickName());
String token= JwtUtil.encode(key,map,salt);
return token;
}else {
return "fail";
}
}
@RequestMapping("verify")
@ResponseBody
public String verify(HttpServletRequest request){
//获取IP
//String salt=request.getHeader("X-forwarded-for");
;
String token = request.getParameter("token");
String salt = request.getParameter("salt");
//String salt="192.168.217.1";
//调用jwt工具类
Map<String, Object> map = JwtUtil.decode(token, key, salt);
if (map!=null && map.size()>0){
//获取jserid
String userId = (String) map.get("userId");
UserInfo userInfo= userService.verify(userId);
if (userInfo!=null){
return "success";
}else {
return "fail";
}
}
return "fail";
}
}
4.ServiceImpl
package com.atguigu.commerce.user.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.fastjson.JSON;
import com.atguigu.commerce.bean.UserAddress;
import com.atguigu.commerce.bean.UserInfo;
import com.atguigu.commerce.config.RedisUtil;
import com.atguigu.commerce.service.UserService;
import com.atguigu.commerce.user.mapper.UserAddressMapper;
import com.atguigu.commerce.user.mapper.UserinfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
public String userKey_prefix="user:";
public String userinfoKey_suffix=":info";
public int userKey_timeOut=60*60*24;
@Autowired
private UserinfoMapper userinfoMapper;
@Autowired
private UserAddressMapper userAddressMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public List<UserInfo> findAll() {
return userinfoMapper.selectAll();
}
@Override
public List<UserAddress> getUserAddressList(String userId) {
UserAddress userAddress = new UserAddress();
userAddress.setUserId(userId);
return userAddressMapper.select(userAddress);
}
@Override
public UserInfo login(UserInfo userInfo) {
//查询数据库
String passwd = userInfo.getPasswd();
//对密码加密
String newPwd = DigestUtils.md5DigestAsHex(passwd.getBytes());
//将加密后的密码赋值给当前对象
userInfo.setPasswd(newPwd);
UserInfo info = userinfoMapper.selectOne(userInfo);
if (info!=null){
//获取jedis
Jedis jedis = redisUtil.getJedis();
//定义key
String userKey=userKey_prefix+info.getId()+userinfoKey_suffix;
//放入key
jedis.setex(userKey,userKey_timeOut, JSON.toJSONString(info));
//关闭jedis、
jedis.close();
return info;
}
return null;
}
@Override
public UserInfo verify(String userId) {
Jedis jedis=null;
try {
//获取jedis
jedis = redisUtil.getJedis();
//定义key
String userKey=userKey_prefix+userId+userinfoKey_suffix;
String userJson = jedis.get(userKey);
if (!StringUtils.isEmpty(userJson)){
//userJson转换为对象
UserInfo userInfo = JSON.parseObject(userJson, UserInfo.class);
return userInfo;
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (jedis!=null){
jedis.close();
}
}
return null;
}
}
登录完成