2022-05-27 16:57

基于JWT实现的登录功能

wanmatea

JavaEE

(951)

(0)

收藏

一、什么是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条评论

点击登录参与评论