漳浦县城乡规划建设局官方网站,松原市建设局网站投诉中心,wordpress 获取当前分类id,做网站网络合同单点登录原理及JWT实现
一、单点登录效果 首先我们看通过一个具体的案例来加深对单点登录的理解。案例地址#xff1a;https://gitee.com/xuxueli0323/xxl-sso?_fromgitee_search 把案例代码直接导入到IDEA中 然后分别修改下server和samples中的配置信息 在host文件中配置
…单点登录原理及JWT实现
一、单点登录效果 首先我们看通过一个具体的案例来加深对单点登录的理解。案例地址https://gitee.com/xuxueli0323/xxl-sso?_fromgitee_search 把案例代码直接导入到IDEA中 然后分别修改下server和samples中的配置信息 在host文件中配置
127.0.0.1 sso.server.com
127.0.0.1 client1.com
127.0.0.1 client2.com然后分别启动server和两个simple服务。 访问测试 其中一个节点登录成功后其他节点就可以访问了 自行测试。
二、单点登录实现 清楚了单点登录的效果后我们就可以自己来创建一个单点登录的实现了。来加深下单点登录的理解了。
1.创建项目 通过Maven创建一个聚合工程然后在工程中创建3个子模块分别为认证服务和客户端模块。 引入相同的依赖 2.client1 我们先在client1中来提供相关的接口。我们提供一个匿名访问的接口和一个需要认证才能访问的接口。
Controller
public class UserController {ResponseBodyGetMapping(/hello)public String hello(){return hello;}GetMapping(/queryUser)public String queryUser(Model model){model.addAttribute(list, Arrays.asList(张三,李四,王五));return user;}
}user.html中的代码为
!DOCTYPE html
html langen xmlns:thhttp://www.thymeleaf.org
headmeta charsetUTF-8title$Title$/title
/head
bodyh1用户管理/h1ulli th:eachuser:${list}[[${user}]]/li/ul
/body
/html访问测试 没有认证就能访问所以得加上验证的逻辑。 GetMapping(/queryUser)public String queryUser(Model model, HttpSession session){Object userLogin session.getAttribute(userLogin);if(userLogin ! null){// 说明登录过了直接放过model.addAttribute(list, Arrays.asList(张三,李四,王五));return user;}// 说明没有登录需要跳转到认证服务器认证 为了能在登录成功后跳回到当前页面传递参数return redirect:http://sso.server:8080/loginPage?redirecthttp://client1.com:8081/queryUser;}可以看到当我们访问queryUser请求的时候因为没有登录所以会重定向到认证服务中的服务做登录处理。这时就需要进入到server服务中处理
3.server服务 在服务端我们需要提供两个接口一个调整到登录界面一个处理认证逻辑以及一个登录页面
Controller
public class LoginController {/*** 跳转到登录界面的逻辑* return*/GetMapping(/loginPage)public String loginPage(RequestParam(value redirect ,required false) String url, Model model){model.addAttribute(url,url);return login;}/*** 处理登录请求* return*/PostMapping(/ssoLogin)public String login(RequestParam(userName) String userName,RequestParam(password) String password,RequestParam(value url,required false) String url){if(zhangsan.equals(userName) 123.equals(password)){// 登录成功return redirect:url;}// 登录失败重新返回登录页面return redirect:loginPage;}}登录页面代码逻辑
!DOCTYPE html
html langen xmlns:thhttp://www.thymeleaf.org
headmeta charsetUTF-8titlesso-server-login/title
/head
bodyh1Server登录页面/h1form action/ssoLogin methodpost 账号:input typetext nameuserName br/密码:input typepassword namepasswordbr/input typehidden nameurl th:value${url}input typesubmit value提交/form
/body
/html然后当我们在client1中访问需要认证的服务的时候就会跳转到登录界面 提交登录操作。当我们提交登录成功的情况应该要重定向会原来的访问地址但实际情况和我们所想的有点出入 原来的queryUser中的逻辑为 4. 认证凭证 上面的问题是我们在认证服务登录成功了但是client1中并不知道登录成功了所以认证成功后需要给client1一个认证成功的凭证。也就是Token信息。 /*** 处理登录请求* return*/PostMapping(/ssoLogin)public String login(RequestParam(userName) String userName,RequestParam(password) String password,RequestParam(value url,required false) String url){if(zhangsan.equals(userName) 123.equals(password)){// 通过UUID生成Token信息String uuid UUID.randomUUID().toString().replace(-,);// 把生成的信息存储在Redis服务中redisTemplate.opsForValue().set(uuid,zhangsan);// 登录成功return redirect:url?tokenuuid;}// 登录失败重新返回登录页面return redirect:loginPage;}生成的Token同步保存在了Redis中然后在重定向的地址中携带了token信息。然后在client1中处理
GetMapping(/queryUser)public String queryUser(Model model,HttpSession session,RequestParam(value token,required false) String token){if(token ! null){// token有值 说明认证了// TODO 基于token 去服务器获取用户信息session.setAttribute(userLogin,张三);}Object userLogin session.getAttribute(userLogin);if(userLogin ! null){// 说明登录过了直接放过model.addAttribute(list, Arrays.asList(张三,李四,王五));return user;}// 说明没有登录需要跳转到认证服务器认证 为了能在登录成功后跳回到当前页面传递参数return redirect:http://sso.server.com:8080/loginPage?redirecthttp://client1.com:8081/queryUser;}然后我们就可以来访问client1中的服务了 5. client2 控制器逻辑
Controller
public class OrderController {GetMapping(/order)public String getOrder(HttpSession session, Model model){Object userLogin session.getAttribute(userLogin);if(userLogin ! null){// 说明认证了model.addAttribute(list, Arrays.asList(order1,order2,order3));return order;}return redirect:http://sso.server.com:8080/loginPage?redirecthttp://client2.com:8082/order;}
}order.html页面内容
!DOCTYPE html
html langen xmlns:thhttp://www.thymeleaf.org
headmeta charsetUTF-8title$Title$/title
/head
bodyh1订单管理/h1ulli th:eachorder:${list}[[${order}]]/li/ul
/body
/html通过前面的介绍我们可以发现clent1认证后可以访问了但是client2提交请求的时候还是会跳转到server服务做认证的处理。 造成这个的原因是client1认证成功后在Session中保存了认证信息但是在client2是获取不到的这时我们可以在Server服务登录成功后在浏览器的Cookie中存储一个token信息然后在其他服务跳转到要进入登录页面之前的接口服务中判断Cookie中是否有值如果有则认为是其他服务登录过的直接放过。 提交请求的时候校验 搞定
三、JWT实现 1.JWT介绍
1.1 什么是JWT 官方JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA . JSON Web 令牌(JWT)是一种开放标准(RFC 7519) 它定义了一种紧凑和自包含的方式用于作为 JSON 对象在各方之间安全地传输信息。可以验证和信任此信息因为它是数字签名的。JWTs 可以使用 secret (使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。 通俗的解释JWT简称 JSON Web Token也就是JSON形式作为Web应用中的令牌信息用于在各方之间安全的将信息作为JSON对象传输在数据传输过程中可以完成数据加密签名等操作。
1.2 基于Session认证 我们最先接触到的认证方式就是基于Session的认证方式每一个会话在服务端都会存储在HttpSession中相当于一个Map然后通过Cookie的形式给客户端返回一个jsessionid然后每次访问的时候都需要从HttpSession中根据jsessionid来获取通过这个逻辑来判断是否是认证的状态。 存在的问题
每个用户都需要做一次记录而Session一般情况下都会存在内存中增大了服务器的开销集群环境下Session需要同步或者分布式Session来处理因为是基于Cookie来传输的如果Cookie被解惑用户容易受到CSRF攻击。前后端分离项目中会更加的麻烦
1.3 基于JWT的认证 具体流程如下 认证的流程
用户通过表单把账号密码提交到后端服务后如果认证成功就会生成一个对应的Token信息之后用户请求资源都会携带这个Token值后端获取到后校验通过放行校验不通过拒绝
jwt的优势
简介可以通过URLPOST参数或者HTTP header发送因为数据量小传输速度快。自包含负载中包含了所有用户所需的信息避免多次查询数据夸语音以JSON形式保存在客户端。不需要服务端保存信息适合分布式环境。
1.4 JWT的结构
令牌的组成
标头(Header)有效载荷(Payload)签名(Signature)
因此JWT的格式为 xxxx.yyyy.zzzz Header.Payload.Signature
Header: header通常由两部分组成令牌的类型【JWT】和所使用的签名算法。例如HMAC、SHA256或者RSA它会使用 Base64 编码组成 JWT结构的第一部分。注意Base64是一种编码是可以被翻译回原来的样子的。
{alg:HS256,typ:JWT
}Payload: 令牌的第二部分是有效负载其中包含声明声明是有关实体通常是用户信息和其他数据的声明它会使用Base64来编码组成JWT结构的第二部分。
{userId:123,userName:波波烤鸭,admin:true
}因为会通过Base64编码所以不要把敏感信息写在Payload中。
Signature 签名部分前面两部分都是使用 Base64 进行编码的即前端可以解开header和payload中的信息Signature需要使用编码后的 header 和 payload 以及我们提供的一个秘钥然后使用 header 中指定的前面算法(HS256) 进行签名签名的作用是保证 JWT 没有被篡改过 2.JWT实现
2.1 JWT基本实现 生成Token令牌 /*** 生成Token信息*/Testvoid generatorToke() {MapString,Object map new HashMap();map.put(alg,HS256);map.put(typ,JWT);Calendar calendar Calendar.getInstance();calendar.add(Calendar.SECOND,60);String token JWT.create().withHeader(map) // 设置header.withClaim(userid, 666) // 设置 payload// 设置过期时间.withExpiresAt(calendar.getTime()).withClaim(username, 波波烤鸭) // 设置 payload.sign(Algorithm.HMAC256(qwaszx)); // 设置签名 保密System.out.println(token);}根据Token来验证是否正确。 /*** 验证Token信息*/Testpublic void verifier(){String token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTMwNTE5ODUsInVzZXJpZCI6NjY2LCJ1c2VybmFtZSI6IuazouazoueDpOm4rSJ9.0LW5MFihMeYNfRfez0a68ncaKQ13j5pSnVZTB7m1CDw;JWTVerifier jwtVerifier JWT.require(Algorithm.HMAC256(qwaszx)).build();DecodedJWT verify jwtVerifier.verify(token);System.out.println(verify.getClaim(userid).asInt());System.out.println(verify.getClaim(username).asString());}验证中场景的异常信息
SignatureVerificationException 签名不一致异常TokenExpiredException Token过期异常AlgorithmMismatchException 算法不匹配异常InvalidClaimException 失效的payload异常
2.2 JWT封装 为了简化操作我们可以对上面的操作进一步封装来简化处理
package com.bobo.jwt.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Calendar;
import java.util.Map;/*** JWT操作的工具类*/
public class JWTUtils {private static final String SING 123qwaszx;/*** 生成Token header.payload.sing 组成* return*/public static String getToken(MapString,String map){Calendar instance Calendar.getInstance();instance.add(Calendar.DATE,7); // 默认过期时间 7天JWTCreator.Builder builder JWT.create();// payload 设置map.forEach((k,v)-{builder.withClaim(k,v);});// 生成Token 并返回return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));}/*** 验证Token* return* DecodedJWT 可以用来获取用户信息*/public static DecodedJWT verify(String token){// 如果不抛出异常说明验证通过否则验证失败return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);}
}
2.3 SpringBoot应用 首先是在登录方法中如果登录成功我们需要生成对应的Token信息然后将Token信息响应给客户端。
PostMapping(/login)public MapString,Object login(User user){MapString,Object res new HashMap();if(zhang.equals(user.getUserName()) 123.equals(user.getPassword())){// 登录成功MapString,String map new HashMap();map.put(userid,1);map.put(username,zhang);String token JWTUtils.getToken(map);res.put(flag,true);res.put(msg,登录成功);res.put(token,token);return res;}res.put(flag,false);res.put(msg,登录失败);return res;}然后就是用户提交请求的时候需要携带Token信息然后我们在controller中处理请求之前需要对token做出校验。如果验证通过就继续处理请求否则就拦截该请求。 PostMapping(/queryUser)public MapString,Object queryUser(RequestParam(token) String token){// 获取用信息之前校验MapString,Object map new HashMap();try{DecodedJWT verify JWTUtils.verify(token);map.put(state,true);map.put(msg,请求成功);return map;}catch (SignatureVerificationException e){e.printStackTrace();map.put(msg,无效签名);}catch (TokenExpiredException e){e.printStackTrace();map.put(msg,Token过期);}catch (AlgorithmMismatchException e){e.printStackTrace();map.put(msg,算法不一致);}catch (Exception e){e.printStackTrace();map.put(msg,Token无效);}map.put(state,false);return map;}但是上面的情况我们看到在controller中添加了大幅度的Token校验的代码增大的冗余代码这时我们可以考虑把Token校验的代码放在拦截器中处理。我们创建一个自定义的拦截器.
/*** 自定义的拦截器* 对特定的情况校验是否携带的有Token信息如果不携带直接拒绝* 然后对Token校验合法性*/
public class JWTInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token request.getParameter(token);// 获取用信息之前校验MapString,Object map new HashMap();try{DecodedJWT verify JWTUtils.verify(token);return true;}catch (SignatureVerificationException e){e.printStackTrace();map.put(msg,无效签名);}catch (TokenExpiredException e){e.printStackTrace();map.put(msg,Token过期);}catch (AlgorithmMismatchException e){e.printStackTrace();map.put(msg,算法不一致);}catch (Exception e){e.printStackTrace();map.put(msg,Token无效);}map.put(state,false);// 把Map转换为JSON响应String json new ObjectMapper().writeValueAsString(map);response.setContentType(application/json;charsetUTF-8);response.getWriter().println(json);return false;}
}要让拦截器生效我们还需要添加对应的配置类。
Configuration
public class InterceptorConfig implements WebMvcConfigurer {Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTInterceptor()).addPathPatterns(/queryUser) // 需要拦截的请求.addPathPatterns(/saveUser) // 需要拦截的请求.excludePathPatterns(/login); // 需要排除的请求}
}然后添加一个测试的方法 /saveUser PostMapping(/saveUser)public String saveUser(){System.out.println(------------);return success;}测试访问把过期时间缩短到1分钟 正常的访问 Token过期后再访问 搞定