Login01
单点登陆
登陆流程图
认证和token的颁发
用接受的用户名密码核对后台数据库
将用户信息加载到写入redis,redis中有该用户视为登录状态。
用userId+当前用户登录ip地址+密钥生成token
重定向用户到之前的来源地址,同时把token作为参数附上。
生成token
JWT工具
JWT(Json Web Token) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
JWT 最重要的作用就是对 token信息的防伪作用。
JWT的原理
一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
1.公共部分
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
2.私有部分
用户自定义的内容,根据实际需要真正要封装的信息。
3.签名部分
根据用户信息+盐值+密钥生成的签名。如果想知道JWT是否是真实的只要把JWT的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存JWT,只要没有密钥就无法伪造。
4.base64编码
并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以吧base64编码解成明文,所以不要在JWT中放入涉及私密的信息,因为实际上JWT并不是加密信息.
JwtUtil工具类
1 public class JwtUtil { 2 3 public static String encode(String key, Map<String,Object> param, String salt){ 4 if(salt!=null){ 5 key+=salt; 6 } 7 JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256,key); 8 9 jwtBuilder = jwtBuilder.setClaims(param); 10 11 String token = jwtBuilder.compact(); 12 return token; 13 14 } 15 16 17 public static Map<String,Object> decode(String token ,String key,String salt){ 18 Claims claims=null; 19 if (salt!=null){ 20 key+=salt; 21 } 22 try { 23 claims= Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody(); 24 } catch ( JwtException e) { 25 return null; 26 } 27 return claims; 28 } 29 }
token = 服务器(key)+UserInfo+ 浏览器(Salt/ip/time)
1 // 用jwt制作token 2 String memberId = umsMemberLogin.getId(); 3 String nickname = umsMemberLogin.getNickname(); 4 Map<String,Object> userMap = new HashMap<>(); 5 userMap.put("memberId",memberId); 6 userMap.put("nickname",nickname); 7 8 9 String ip = request.getHeader("x-forwarded-for");// 通过nginx转发的客户端ip 10 if(StringUtils.isBlank(ip)){ 11 ip = request.getRemoteAddr();// 从request中获取ip 12 if(StringUtils.isBlank(ip)){ 13 ip = "127.0.0.1"; 14 } 15 } 16 17 // 按照设计的算法对参数进行加密后,生成token 18 token = JwtUtil.encode("2019gmall0105", userMap, ip);
验证token
功能:当业务模块某个页面要检查当前用户是否登录时,提交到认证中心,认证中心进行检查校验,返回登录状态、用户Id和用户名称
步骤
1、 利用密钥和IP检验token是否正确,并获得里面的userId
2、 用userId检查Redis中是否有用户信息,如果有延长它的过期时间。
3、 登录成功状态返回。
1 @RequestMapping("verify") 2 @ResponseBody 3 public String verify(String token,String currentIp){ 4 5 // 通过jwt校验token真假 6 HashMap<String, String> map = new HashMap<>(); 7 Map<String, Object> decode = JwtUtil.decode(token, "2019gmall0105", currentIp); 8 9 if (decode!=null){ 10 11 map.put("status","success"); 12 map.put("memberId",(String)decode.get("memberId")); 13 map.put("nickname",(String)decode.get("nickname")); 14 15 }else { 16 map.put("status","fail"); 17 } 18 return JSON.toJSONString(map); 19 }
认证中心的整合
加入拦截器
1 @Component 2 public class AuthInterceptor extends HandlerInterceptorAdapter { 3 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 4 // 拦截代码 5 return true; 6 } 7 }
加入拦截器的整合类
@Configuration public class WebMvcConfiguration extends WebMvcConfigurerAdapter { @Autowired AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(authInterceptor).addPathPatterns("/**"); super.addInterceptors(registry);
通过注解的方式来标识具体的方法是否需要通过拦截器
@LoginRequired
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginRequired { boolean loginSuccess() default true; }
第一类方法:不需要进行拦截的方法(没有拦截器注解),直接放行,不加@LoginRequired
第二类方法:需要拦截但是拦截校验失败(用户没登陆或者登录过期了)也可以继续访问的方法,比如说购物车中的所有方法@LoginRequired(loginSuccess=false)
第三类方法:需要拦截,并且拦截校验一定要通过(用户登录成功了)才能访问的方法@LoginRequired(loginSuccess=true)
主动登陆流程
1 首页访问登录页,携带ReturnUrl会跳地址
2 登录页保存会跳地址
3 登录页通过异步方法验证用户名和密码
4 验证通过后颁发token给异步ajax
5 异步ajax得到token,然后根据会跳地址ReturnUrl访问原始功能
0 请求原始应用
1 被拦截器拦截
2 拦截器需要判断注解
是否需要进行身份验证
是否必须通过身份验证
老token空 老token不空
新token空 从未登录过 之前登录过
新token不空 刚刚登录 过期
3 如果是必须登录但是没有登录,会被重定向认证中心进行登录
4 在认证中心登陆后,进行重新的请求,请求原始应用
输入用户名和密码后,进行第二次请求(请求原始地址)
5 验证
通过httpClient(apach一个通用工具类)
6 验证结果结合注解的情况
AuthInterceptor
1 @Component 2 public class AuthInterceptor extends HandlerInterceptorAdapter { 3 4 5 @Override 6 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 7 8 //拦截代码 9 //判断拦截的请求的访问的方法的注解 10 HandlerMethod hm = (HandlerMethod) handler; 11 12 LoginRequired methodAnnotation = hm.getMethodAnnotation(LoginRequired.class); 13 14 //是否拦截 15 if (methodAnnotation == null) { 16 return true; 17 } 18 19 20 String token = ""; 21 22 String oldToken = CookieUtil.getCookieValue(request, "oldToken", true); 23 if (StringUtils.isNotBlank(oldToken)) { 24 token = oldToken; 25 } 26 27 String newToken = request.getParameter("token"); 28 if (StringUtils.isNotBlank(newToken)) { 29 token = newToken; 30 } 31 32 33 //是否必须登陆 34 boolean loginSuccess = methodAnnotation.loginSuccess(); 35 36 //token有值不为空必须加验证,掉用验证中心进行认证 37 String success="fail"; 38 39 Map<String,String> successMap = new HashMap<>(); 40 41 if (StringUtils.isNotBlank(token)){ 42 43 String ip = request.getHeader("x-forwarded-for");// 通过nginx转发的客户端ip 44 if(StringUtils.isBlank(ip)){ 45 ip = request.getRemoteAddr();// 从request中获取ip 46 if(StringUtils.isBlank(ip)){ 47 ip = "127.0.0.1"; 48 } 49 } 50 51 String successJson = HttpclientUtil.doGet("http://passport.gmall.com:8085/verify?token=" + token+"¤tIp="+ ip); 52 53 successMap = JSON.parseObject(successJson, Map.class); 54 55 success = successMap.get("status"); 56 } 57 58 59 60 if (loginSuccess) { 61 // 必须登录成功才能使用 62 if (!success.equals("success")) { 63 //重定向会passport登录 64 StringBuffer requestURL = request.getRequestURL(); 65 response.sendRedirect("http://passport.gmall.com:8085/index?ReturnUrl="+requestURL); 66 return false; 67 } 68 69 // 需要将token携带的用户信息写入 70 request.setAttribute("memberId", successMap.get("memberId")); 71 request.setAttribute("nickname", successMap.get("nickname")); 72 73 //验证通过,覆盖cookie中的token 74 if(StringUtils.isNotBlank(token)){ 75 CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true); 76 } 77 78 } else { 79 // 没有登录也能用,但是必须验证 80 if (success.equals("success")) { 81 // 需要将token携带的用户信息写入 82 request.setAttribute("memberId", "1"); 83 request.setAttribute("nickname", "nickname"); 84 //验证通过,覆盖cookie中的token 85 if(StringUtils.isNotBlank(token)){ 86 CookieUtil.setCookie(request,response,"oldToken",token,60*60*2,true); 87 } 88 } 89 } 90 91 return true; 92 } 93 }
调用userService 服务进行缓存
User:password:info 存储用户信息
K:username+password
V:用户信息
1 // 将token存入redis一份 2 userService.addUserToken(token,memberId);
userServcieImpl
1 @Override 2 public void addUserToken(String token, String memberId) { 3 Jedis jedis = redisUtil.getJedis(); 4 jedis.setex("user:"+memberId+":token",60*60*2,token); 5 jedis.close(); 6 }
login
1 @RequestMapping("login") 2 @ResponseBody 3 public String login(UmsMember umsMember, HttpServletRequest request){ 4 5 String token = ""; 6 7 // 调用用户服务验证用户名和密码 8 UmsMember umsMemberLogin = userService.login(umsMember); 9 10 if(umsMemberLogin!=null){ 11 // 登录成功 12 13 //1 用jwt制作token 14 15 //2 将token存入redis一份 16 17 }else{ 18 // 登录失败 19 token = "fail"; 20 } 21 22 return token; 23 }
UserServiceImpl login
先查缓存,在查 数据库
1 @Override 2 public UmsMember login(UmsMember umsMember) { 3 Jedis jedis = null; 4 try { 5 jedis = redisUtil.getJedis(); 6 7 if(jedis!=null){ 8 String umsMemberStr = jedis.get("user:" + umsMember.getPassword()+umsMember.getUsername() + ":info"); 9 10 if (StringUtils.isNotBlank(umsMemberStr)) { 11 // 密码正确 12 UmsMember umsMemberFromCache = JSON.parseObject(umsMemberStr, UmsMember.class); 13 return umsMemberFromCache; 14 15 } 16 } 17 // 链接redis失败,开启数据库 18 UmsMember umsMemberFromDb =loginFromDb(umsMember); 19 if(umsMemberFromDb!=null){ 20 jedis.setex("user:" + umsMember.getPassword()+umsMember.getUsername() + ":info",60*60*24, JSON.toJSONString(umsMemberFromDb)); 21 } 22 return umsMemberFromDb; 23 }finally { 24 jedis.close(); 25 } 26 } 27 28 29 30 31 private UmsMember loginFromDb(UmsMember umsMember) { 32 33 List<UmsMember> umsMembers = userMapper.select(umsMember); 34 35 if(umsMembers!=null){ 36 return umsMembers.get(0); 37 } 38 39 return null; 40 }
完