一、业务介绍来源
早期单一服务器,用户认证
缺点:单点性能压力,无法扩展
早期我们开发web应用都是所有的包放在一起打成一个war包放入tomcat容器来运行的,所有的功能,所有的业务,后台管理,门户界面,都是由这一个war来支持的。
用户的登录以及权限就显得十分简单,用户登录成功后,把相关信息放入会话中,HTTP维护这个会话,再每次用户请求服务器的时候来验证这个会话即可,大致可以用下图来表示:

服务器和客户端的状态是由session来维护的,当集群出现后,各台机器上的Session就会出现不一致的问题,下面来梳理下Session。
Token和Session
Session:
客户端首次请求,服务器生成sessionId,存在服务器上,同时发送一份到客户端,客户端保存在浏览器中,下次访问的时候带上sessionId去访问(放在header里),服务器验证比对是否存有改Id就能够判断曾经登录与否
Session存在什么问题:
- cookie中使用jsessionId 容易被篡改、盗取,不法分子直接可以从浏览器中截取出保存的JsessionId,直接可用于登录
- 用户量大时空间浪费高
- 跨顶级域名无法访问
- 严重的限制了服务器扩展能力, 比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统, 那session id会保存在机器A上, 假设小F的下一次请求被转发到机器B怎么办? 机器B可没有小F的 session id啊。
Token流程:
1.用户通过用户名和密码发送请求。
2.程序验证。
3.程序返回一个签名的token 给客户端。
4.客户端储存token,并且每次用于每次发送请求。
5.服务端验证token并返回数据。
发现Token和Session的区别了吗?
Token并不会在服务器上进行保存,而是在服务器上有一套验证逻辑,下一次登录服务器只需要对客户端发来的token进行合法性验证就好了,而session是需要在服务器中保存一个值,服务器对比和客户端请求的值的是否一致。
Token:时间换空间
Session:空间换时间(社区合法性验证过程)
注意这里Session和web中的Session有区别
SSO单点登录模式(single sign on)
解决了单点性能瓶颈
系统架构:


二、实现单点登录
Step1: 生成token
JWT工具
JWT(Json Web Token) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
JWT 最重要的作用就是对 token信息的防伪作用。
JWT的组成
一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。

1、公共部分(Header)
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
2、私有部分(Payload)
用户自定义的内容,根据实际需要真正要封装的信息。
3、签名部分(Signature)
根据用户信息+盐值+密钥生成的签名。如果想知道JWT是否是真实的只要把JWT的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存JWT,只要没有密钥就无法伪造。
4、base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以吧base64编码解成明文,所以不要在JWT中放入涉及私密的信息,因为实际上JWT并不是加密信息。
5、采用HMAC-SHA256进行Hsh加密
Step2: 制作 JWT的工具类(JwtUtil)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class JwtUtil {
public static String encode(String key,Map<String,Object> param,String salt){ if(salt!=null){ key+=salt; } JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256,key);
jwtBuilder = 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; } }
|
Step3: 建立注册中心
实际项目中,可建立一个模块用作注册中心,如我这里的passport作为注册中心。
注册中心主要功能:
1.验证用户是否合法
2.为登录用户颁发token

#####登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @RequestMapping("login") @ResponseBody public String login(UmsMember umsMember, HttpServletRequest request){
String token = "";
// 调用用户服务验证用户名和密码 UmsMember umsMemberLogin = userService.login(umsMember);
if(umsMemberLogin!=null){ // 登录成功
// 用jwt制作token String memberId = umsMemberLogin.getId(); String nickname = umsMemberLogin.getNickname(); Map<String,Object> userMap = new HashMap<>(); userMap.put("memberId",memberId); userMap.put("nickname",nickname);
String ip = request.getHeader("x-forwarded-for");// 通过nginx转发的客户端ip if(StringUtils.isBlank(ip)){ ip = request.getRemoteAddr();// 从request中获取ip if(StringUtils.isBlank(ip)){ ip = "127.0.0.1"; } }
// 按照设计的算法对参数进行加密后,生成token token = JwtUtil.encode("2019gmall0105", userMap, ip);
// 将token存入redis一份 userService.addUserToken(token,memberId);
}else{ // 登录失败 token = "fail"; }
return token; }
|
#####验证token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RequestMapping("verify") @ResponseBody public String verify(String token,String currentIp,HttpServletRequest request){
// 通过jwt校验token真假 Map<String,String> map = new HashMap<>();
Map<String, Object> decode = JwtUtil.decode(token, "2019gmall0105", currentIp);
if(decode!=null){ map.put("status","success"); map.put("memberId",(String)decode.get("memberId")); map.put("nickname",(String)decode.get("nickname")); }else{ map.put("status","fail"); }
return JSON.toJSONString(map); }
|
#####跳转到index登录界面,登录失败时使用
1 2 3 4 5 6
| @RequestMapping("index") public String index(String ReturnUrl, ModelMap map){
map.put("ReturnUrl",ReturnUrl); return "index"; }
|
Step3: 业务模块的登录检查(@注解与拦截器)
1 、由认证中心签发的token如何保存?保存到浏览器的cookie中
2 、难道每一个模块都要做一个token的保存功能? 拦截器
3 、如何区分请求是否一定要登录?自定义注解
拦截器(Interceptor)
拦截器原理基于反射,网上很多教程,要注意之前servlet中用过的Fliter和Interceptor(拦截器)的区别—–>一个是反射,一个是和Servlet有关
HandlerInterceptor->Spring中提供的拦截器接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public interface HandlerInterceptor {
/** * 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应; */ boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
/** * 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。 */ void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
/** * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中 */ void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}
|
1、定义登录检查注解
LoginRequired.class:
1 2 3 4 5 6 7 8
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginRequired {
boolean loginSuccess() default true;
}
|
2、自定义拦截器,继承成HandlerInterceptorAdapter,通过重写它的preHandle方法,业务代码前的校验工作
AuthInterceptor.class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| @Component public class AuthInterceptor extends HandlerInterceptorAdapter
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 拦截代码 // 判断被拦截的请求的访问的方法的注解(是否时需要拦截的) HandlerMethod hm = (HandlerMethod) handler; LoginRequired methodAnnotation = hm.getMethodAnnotation(LoginRequired.class);
StringBuffer url = request.getRequestURL();
// 没有LoginRequired则直接放行 if (methodAnnotation == null) { return true; } //先从cookie中查找有没有保存的token信息 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 loginSuccess = methodAnnotation.loginSuccess();// 获得该请求是否必登录成功
// 进入认证中心进行验证 String success = "fail"; Map<String,String> successMap = new HashMap<>(); if(StringUtils.isNotBlank(token)){ String ip = request.getHeader("x-forwarded-for");// 通过nginx转发的客户端ip if(StringUtils.isBlank(ip)){ ip = request.getRemoteAddr();// 从request中获取ip if(StringUtils.isBlank(ip)){ ip = "127.0.0.1"; } } String successJson = HttpclientUtil.doGet("http://passport.gmall.com:8085/verify?token=" + token+"¤tIp="+ip);
successMap = JSON.parseObject(successJson,Map.class);
success = successMap.get("status");
}
if (loginSuccess) { // 必须登录成功才能使用 if (!success.equals("success")) { //重定向会passport登录 StringBuffer requestURL = request.getRequestURL(); response.sendRedirect("http://passport.gmall.com:8085/index?ReturnUrl="+requestURL); return false; }
// 需要将token携带的用户信息写入 request.setAttribute("memberId", successMap.get("memberId")); request.setAttribute("nickname", successMap.get("nickname")); //验证通过,覆盖cookie中的token if(StringUtils.isNotBlank(token)){ CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true); }
} else { // 没有登录也能用,但是必须验证 if (success.equals("success")) { // 需要将token携带的用户信息写入 request.setAttribute("memberId", successMap.get("memberId")); request.setAttribute("nickname", successMap.get("nickname"));
//验证通过,覆盖cookie中的token if(StringUtils.isNotBlank(token)){ CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true); }
} }
return true; } }
|
CookieUtil等工具包地址
https://github.com/YellowRifle/Web-Utils