单点登录
单点登录全称:Single Sign On(SSO),是指在多系统应用群中登录其中一个系统,便可在其他所有系统中得到授权而无需再次登录。
业务介绍
- 早期单一服务器的用户认证,单点性能压力,无法扩展
- Web应用集群:session共享模式,解决了单点性能瓶颈,问题是,跨顶级域名无法访问,cookie中使用JSESSIONID容易被篡改,盗取
- 分布式SSO模式:解决跨域,用户身份信息独立管理,更好的分布式管理,可以自己扩展安全策略,缺点就是,认证服务器压力较大
SSO简单流程图
SSO登录流程:
- 用户访问具体受保护的业务(就是会被拦截的业务)之前,会被拦截器所拦截,这里采用注解式拦截方法(后面说明)。
- 拦截到认证中心,认证中心只负责两件事(负责token的颁发和对token真伪的验证),如果发现用户没有登录,直接返回到登录页面引导用户进行认证
- 用户进行登录验证,认证中心调用后台数据库进行验证,验证成功,生成认证token(下面说明生成token),将token存入redis缓存中,实现用户信息的共享。带着token跳转最初的具体业务的地址,如果是带token的跳转,则将token写入Cookie中,并继续打开具体业务功能
- 用户在获取到认证后的token继续访问其他受保护的业务时,先检查Cookie中是否存在token,如果存在,则继续提交到认证中心验证,认证成功,跳转到具体业务页面
认证中心采取这种架构的原因(认证中心独立出来的好处):实现单点登录
用户的登录认证、用户服务 都独立出来,这么做的好处是,在将来扩展其他业务时,只需要在认证中心完成认证,即可完成具体业务功能,还有一个好处是,在分布式系统中会有多个模块,会出现跨顶级域名
(一级域名)认证,为了突破Cookie中JSESSIONID跨域不能共享的这些限制(redis 也可以实现session共享,太过繁琐),需要将认证中心完全独立出来,降低耦合,放弃session共享
的形式,而是直接采用token的形式,用户第一次认证完成后,在浏览器中定义一个token(包含用户信息),访问具体业务模块时,携带这个token,业务模块将浏览器中的token 和Redis中的token进行对比,数据一致代表认证成功,然后就可以访问具体业务,用token 代替JSESSIONID,把session舍弃,token+redis 实现
生成token
- 生成token 前先进行用户名密码的核对(数据库校验)
- 将用户信息加载到redis中 自定义格式存储 如“user:”+(账号,手机,唯一)+“info”
- 自定义token的生成,采用JWT工具生成
- 将token 存入redis中一份,实现用户信息共享
- 由前端重定向用户之前的业务地址,同时把token作为参数附上
JWT工具的介绍及使用:
什么是 JWT -- JSON WEB TOKENwww.jianshu.com/***
* jwt 制作token
* @param umsMember 数据库校验后的用户信息
* @param request
* @return
*/
public String makingToken(UmsMember umsMember,HttpServletRequest request){
//jwt 制作token
String id = umsMember.getId();
String nickname = umsMember.getNickname();
HashMap<String,Object> userMap = new HashMap<>();
userMap.put("memberId",id);
userMap.put("nickname",nickname);
//用户基本信息可以扩展
String ipAddr = request.getHeader("x-forwarded-for");//通过nginx转发的客户端ip
if (StringUtils.isBlank(ipAddr)) {
ipAddr = request.getRemoteAddr();//从request中获取ip
if (StringUtils.isBlank(ipAddr)) {
ipAddr = UmsConst.LOCALHOST;
}
}
//按照设计的算法对参数进行加密后生成 token //ip 作为盐值
return JwtUtil.encode(UmsConst.TOKEN_KEY, userMap, ipAddr);
}
/**
* @author panxs
* @version 1.0
* @date 2020/2/4 0:16
*/
/**
* 用jwt实现用户登录的校验(Token 的加密、解密算法)
*
* key: 服务器上的公共key
* param: 用户的基本信息
* salt: 盐值:可以是 ip 或者 time
*
* 三个参数 生成的 token
*/
public class JwtUtil {
//加密算法
public static String encode(String key, Map<String,Object> param,String salt){
if (StringUtils.isNotBlank(salt)) {
key+=salt;
}
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256, key);
jwtBuilder.setClaims(param);
String token = jwtBuilder.compact();
return token;
}
//解密算法
public static Map<String,Object> decode(String token ,String key,String salt){
Claims claims=null;
if (salt!=null){
key+=salt;
}
try {
claims= Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
} catch ( JwtException e) {
return null;
}
return claims;
}
}
redis中添加token实现用户信息的共享,并设置过期时间
/**
* 生成token 之后往redis中保留一份,实现用户信息的共享
* redis 这里不做说明
*/
public void addUserToken(String token, String id) {
Jedis jedis = null;
try {
jedis = redisUtil.getJedis();
jedis.setex("user:"+id+":token",60*60*24,token);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Cookie中添加token
/**
* @author panxs
* @date 2020/2/2 16:08
* @version 1.0
*/
public class CookieUtil {
/***
* 获得cookie中的值,
* @param request
* @param cookieName
* @param isDecoder
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals(cookieName)) {
if (isDecoder) {//如果涉及中文
retValue = URLDecoder.decode(cookies[i].getValue(), "UTF-8");
} else {
retValue = cookies[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/***
* 设置cookie的值
* @param request
* @param response
* @param cookieName
* @param cookieValue
* @param cookieMaxage 失效时间
* @param isEncode
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage >= 0)
cookie.setMaxAge(cookieMaxage);
if (null != request)// 设置域名的cookie
cookie.setDomain(getDomainName(request));
// 在域名的根路径下保存
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/***
* 获得cookie的主域名,本系统为gmall.com,保存时使用
* @param request
* @return
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();//获取浏览器地址栏的url
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split(".");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split(":");
domainName = ary[0];
}
System.out.println("domainName = " + domainName);
return domainName;
}
/***
* 将cookie中的内容按照key删除
* @param request
* @param response
* @param cookieName
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
setCookie(request, response, cookieName, null, 0, false);
}
}
基于注解式的拦截器(自定义拦截)
/**
* @author panxs
* @version 1.0
* @date 2020/2/3 20:23
*/
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
/*
*拦截器的配置
*
* 第一类方法:不需要进行拦截(没有拦截器注解@LoginRequired),直接放行
*
*
* 第二类方法:需要拦截但是拦截校验失败(用户没有登陆或者登陆过期了)也可以继续访问的方法,比如说购物车中的所有方法
*
*
* 第三类方法:需要拦截,并且拦截校验一定要通过(用户登录成功了) 才能访问的方法
*
*
* 访问没有认证的方法 会被拦截到认证中心 如果没有登录 会被转到认证中心 , 然后再认证中心 登录 之后会重新访问原来访问的方法(做了一次重定向)
*
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//拦截代码
//判断被拦截的请求的访问的方法的注解(是否是需要拦截的)
//利用反射获取该方法的所有信息
HandlerMethod handlerMethod = (HandlerMethod) handler;
LoginRequired methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequired.class);
//是否拦截拦截
if (methodAnnotation==null) {
return true; //不需要拦截
}
/**
* token 的 四种情况
* oldToken NULL oldToken NOTNUll
*
* newToken NULL 从未登录过 之前登录过
*
* newToken NOTNULL 刚刚登录 过期
*
*
*/
String token ="";
String oldToken = CookieUtil.getCookieValue(request, "oldToken", true);
if (StringUtils.isNotBlank(oldToken)) {
token = oldToken;
}
String newToken = request.getParameter("token");
if (StringUtils.isNotBlank(newToken)) {
token = newToken;
}
//获取该请求是否必须要登录
boolean b = methodAnnotation.loginSuccess();
//先验证
String status=UmsConst.STATUS_FAIL;
Map<String, String> statusMap = new HashMap<>();
if (StringUtils.isNotBlank(token)) {
//调用验证中心进行验证
String addr = request.getHeader("x-forwarded-for");//通过nginx转发的客户端ip
if (StringUtils.isBlank(addr)) {
addr = request.getRemoteAddr();//从request中获取ip
if (StringUtils.isBlank(addr)) {
addr = UmsConst.LOCALHOST;
}
}
/**
* 注意:
*
* 这里会产生一个新的request
*/
String statusJson = HttpClientUtil.doGet("http://auth.qmall.com:8882/auth/verification?token=" + token+"¤tIp="+addr);
statusMap = JSON.parseObject(statusJson, Map.class);
status = statusMap.get("status");
}
if (b) {
//必须登录才能使用
//String oldToken = CookieUtil.getCookieValue(request, "oldToken", true);
//request.getParameter("token");
if (!UmsConst.STATUS_SUCCESS.equals(status)) {
//重定向到认证中心登录
response.sendRedirect("http://auth.qmall.com:8882/auth/index?ReturnUrl="+request.getRequestURL());
return false;
}
//需要将 token携带的用户信息 写入
request.setAttribute("memberId",statusMap.get("memberId"));
request.setAttribute("nickname",statusMap.get("nickname"));
//验证成功 覆盖/设置cookie中的token
if (StringUtils.isNotBlank(token)){
CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
}
}else{
//不需要登录也能使用的模块 但是必须要验证
if (UmsConst.STATUS_SUCCESS.equals(status)) {
//需要将 token携带的用户信息 写入
request.setAttribute("memberId",statusMap.get("memberId"));
request.setAttribute("nickname",statusMap.get("nickname"));
}
//验证成功 覆盖cookie中的token
if (StringUtils.isNotBlank(token)){
CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true);
}
}
return true; //要拦截
}
}
自定义注解
/**
* @author panxs
* @version 1.0
* @date 2020/2/3 20:46
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 拦截器的标识
*/
@Target(ElementType.METHOD) //只在方法上生效
@Retention(RetentionPolicy.RUNTIME) //生效范围 , 在虚拟机运行时也生效
public @interface LoginRequired {
boolean loginSuccess() default true;
}
2020年3月4日14:22:37 panxs