一、什么是JWT
JWT(全称:Json Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT的最常见方案是用于用户登录鉴权。用户登录后,每个后续请求都将包含JWT,从而允许用户访问该令牌允许的路由,服务和资源。
下面我们看看如何基于JWT来实现登录功能。
二、JWT结构介绍
在其紧凑的形式中,JSON Web令牌由点(.)分隔的三个部分组成,它们是:
头
有效载荷
签名
因此,JWT通常如下所示。
xxxxx.yyyyy.zzzzz
让我们分解一下不同的部分。
1、头
报头通常由两部分组成:令牌的类型,即JWT,以及所使用的签名算法,如HMAC SHA256或RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,该JSON是Base64Url编码的,以形成JWT的第一部分。
2、有效载荷
令牌的第二部分是有效负载,它包含声明。声明是关于实体(通常是用户)和附加数据的声明。声明有三种类型:注册声明、公开声明和私有声明。
注册声明:这些是一组预定义声明,它们不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中包括:iss(发行人)、exp(到期时间)、sub(主题)、aud(受众)等。
注意,声明名只有三个字符长,因为JWT是为了紧凑。
公开声明:这些声明可以由使用JWT的人随意定义。但是为了避免冲突,应该在IANA JSON Web Token注册表中定义它们,或者将它们定义为包含抗冲突名称空间的URI。
私有声明:这些定制声明是为了在同意使用它们的各方之间共享信息而创建的,它们既不是注册的也不是公开的声明。
有效载荷的一个例子可以是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对有效负载进行Base64Url编码,以形成JSON Web Token的第二部分。
请注意,对于已签名的令牌,该信息虽然受到保护,不会被篡改,但任何人都可以读懂。不要将机密信息放在JWT的有效负载或头元素中,除非它被加密了。
3、签名
要创建签名部分,你必须获取已编码的头、已编码的有效负载、一个密钥和头中指定的算法,并对其进行签名。
例如,使用HMAC SHA256算法时,签名的生成方式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在整个过程中没有被更改,而且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发送方是它所声称的那个人。
4、把所有合起来
输出是三个用点分隔的Base64-URL字符串,它们可以很容易地在HTML和HTTP环境中传递,同时与基于xml的标准(如SAML)相比更紧凑。
下面展示了一个JWT,它对前面的头和有效负载进行了编码,并使用secret对其进行了签名。
三、JWT进行鉴权的思路
1、用户发起登录请求。
2、服务端创建一个加密后的JWT信息,作为Token返回。
3、在后续请求中JWT信息作为请求头,发给服务端。
4、服务端拿到JWT之后进行解密,正确解密表示此次请求合法,验证通过;解密失败说明Token无效或者已过期。
四、JWT实现的鉴权实例
1、新建数据库和表
这里我们使用Mysql数据库,首先在MySQL数据库中新建一个数据库jwt和数据库表User,
表结构如下:
插入一条测试数据:
2、新建springboot项目
在idea新建一个springboot项目,然后在pom文件中分别添加jwt、mysql、mybatis、lombok的依赖。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.19.2</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
3、搭建项目
项目结构如下:
(1)、添加实体类User
首先,添加一个实体类User,用于装载从数据库中查询的数据
@Data public class User { private int id; private String username; private String password; private String name; }
(2)、添加Mapper接口UserMapper,并添加一个方法findByUserNameAndPassword()根据用户名和密码从数据库中查询数据
@Mapper public interface UserMapper { @Select(" select * from user where username=#{username} and password=#{password} ") User findByUserNameAndPassword(@Param("username") String username,@Param("password") String password); }
(3)、添加jwt帮助类 JwtUtils
这个类需要Spring来管理,所以要添加@Component注解。
在这个类中添加三个方法:
getToken()方法,根据有效载荷生成并返回token
verify()方法用来验证token
getValue()方法用来根据有效载荷的key来获取值
注意:载荷中的值不需要解密也可以获得,因此要存放密码等机密的数据需要先加密
@Component public class JwtUtils { private static String secretKey="!@#$%^&123abcQWE"; /** * 生成token * @param map 传入的载荷 * @return */ public static String getToken(Map<String, String> map){ JWTCreator.Builder builder = JWT.create(); map.forEach((k,v)->{ builder.withClaim(k, v); }); Calendar instance = Calendar.getInstance(); //定义过期时间 instance.add(Calendar.DATE, 1); builder.withExpiresAt(instance.getTime()); return builder.sign(Algorithm.HMAC256(secretKey)).toString(); } /** * 验证获取token中的载荷,验证失败返回null * @param token * @return */ public static DecodedJWT verify(String token){ return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token); } /** * 获得token中的信息无需secret解密也能获得 * @return 指定key对应的值 */ public static String getValue(String token,String key) { try { if (token == null){ return null; } DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(key).asString(); } catch (JWTDecodeException e) { return null; } } }
(4)、添加Service接口和实现类
首先添加接口UserService 并添加一个login()登录方法
public interface UserService { String login(String username, String password); }
然后添加接口的实现类UserServiceImpl
然后实现接口的login方法,这个方法首先去数据库中查询用户输入的账号和密码是否正确,如果正确的话,调用JWTUtils里面的方法生成token,并返回。
@Service public class UserServiceImpl implements UserService { @Resource private JwtUtils jwtUtil; @Resource private UserMapper userMapper; @Override public String login(String username, String password) { //登录验证 User user = userMapper.findByUserNameAndPassword(username, password); if (user == null) { return null; } //如果能查出,则表示账号密码正确,生成jwt返回 String uuid = UUID.randomUUID().toString().replace("-", ""); HashMap<String, String> map = new HashMap<>(); map.put("username",user.getUsername()); map.put("name", user.getName()); map.put("id",String.valueOf(user.getId())); return JwtUtils.getToken(map); } }
(5)、添加控制器类UserController
接收用户输入的用户名和密码,如果正确,生成token并存放到session里面。
@Controller @RequestMapping("/user") public class UserController { @Resource private UserService userService; @PostMapping("/login") public String login(@RequestParam(name = "username") String username, @RequestParam(name = "password") String password, HttpSession session, HttpServletResponse response){ String token = userService.login(username, password); if(token==null){ return "redirect:/user/login.html"; } session.setAttribute("token",token); return "redirect:/user/index.html"; } }
(6)、添加拦截器类 UserInterceptor
如果用户登录成功,就放行,否则重定向回登录页面
@Component public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Map<String, Object> map = new HashMap<>(); //从请求头中获取tokenString token=request.getHeader("token");//从session中获取token 兼容不是前后端分离的项目if(token==null||token.equals("")){ HttpSession session=request.getSession(); token =(String)session.getAttribute("token");}//从请求参数中获取tokenif(token==null||token.equals("")){ token=request.getParameter("token");} try { 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","token算法不一致!"); }catch (Exception e){ e.printStackTrace(); map.put("msg","token无效!!"); } map.put("state",false);//设置状态 //将map 专为json jackson String json = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.sendRedirect("/user/login.html"); return false; } }
(7)、添加配置类 WebConfig
对user目录下面的页面添加拦截器,排除登录验证链接/user/login和登录页面/user/login.jsp
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private UserInterceptor userInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(userInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/login","/user/login.html"); } }
(8)、在resources/templates/user目录下添加两个页面 会员登录页面login.html和会员中心首页index.html。
login.html代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="/user/login" method="post"> <fieldset> <span>用户名:</span> <input type="text" name="username" id="username"> </fieldset> <fieldset> <span>密 码:</span> <input type="text" name="password" id="password"> </fieldset> <fieldset> <input type="submit"> </fieldset> </form> </body> </html>
Index.html代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>会员中心首页</title> </head> <body> 会员中心首页 </body> </html>
(9)、修改配置文件 application.properties
在配置文件中添加数据库的连接信息以及配置可以访问templates目录下的文件。
spring.datasource.url=jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=wanmait spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.resources.static-locations=classpath:/templates,classpath:/static
至此,项目搭建完成。
4、运行项目
运行项目,并访问登陆页面/user/login.html
如果填写正确的用户名和密码,就会打开会员中心首页
因为我们的token是放到session里面存放的,所以再访问会员中心的其他页面,也不需要再登录了。如果需要长时间的保持登录状态,可以把token放到cookie中保存。但是这两种方式无法跨域,如果要跨域,可以把token放到请求头中请求。
这种方式也可以用于前后端分离的登录,前端访问的时候把获取的token放在请求头中就可以了
0条评论
点击登录参与评论