java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot JWT用户登录认证

SpringBoot集成 JWT实现用户登录认证的项目实践

作者:Java后端何哥

当今前后端分离时代,基于Token的会话保持机制比传统的Session/Cookie机制更加方便,本文主要介绍了SpringBoot集成 JWT实现用户登录认证的项目实践,感兴趣的可以了解一下

前言:当今前后端分离时代,基于Token的会话保持机制比传统的Session/Cookie机制更加方便,下面我会介绍SpringBoot快速集成JWT库java-jwt以完成用户登录认证。

一、JWT 简介

1.1、 JWT的概念

JWT 是 JSON Web Token 的缩写,是为了在网络应用环境间传递声明而执行的一种基于  JSON  的开放标准((RFC 7519)。定义了一种简洁的,自包含的方法用于通信双方之间以  JSON  对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT 可以使用  HMAC  算法或者是  RSA  的公私秘钥对进行签名。

1.2、JWT请求流程

JWT 请求流程

1.3、JWT 的主要应用场景

身份认证在这种场景下,一旦用户完成了登录,在接下来的每个请求中包含 JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

1.4、JWT 数据结构

JWT 是由三段信息构成的,将这三段信息文本用  .  连接一起就构成了 JWT 字符串。JWT 的三个部分依次为头部:Header,负载:Payload 和签名:Signature。

①Header:

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中, alg  属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256); typ  属性表示这个令牌(token)的类型(type),JWT 令牌统一写为  JWT

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

②Payload:

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。有效信息包含三个部分:

标准中注册的声明 (建议但不强制使用) :

公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为  base64  是对称解码的,意味着该部分信息可以归类为明文信息。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

③Signature:

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"( . )分隔,就可以返回给用户。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如  api.example.com/?token=xxx )。Base64 有三个字符  + 、  /  和  = ,在 URL 里面有特殊含义,所以要被替换掉: =  被省略、 +  替换成  - /  替换成  _  。这就是 Base64URL 算法。

1.5、JWT 的使用方式

客户端收到服务器返回的 JWT 之后需要在本地做保存。此后,客户端每次与服务器通信,都要带上这个 JWT。一般的的做法是放在 HTTP 请求的头信息  Authorization  字段里面。

Authorization: Bearer <token>

这样每个请求中,服务端就可以在请求头中拿到 JWT  进行解析与认证。

1.6、JWT 的特性

二、SpringBoot整合JWT

新建一个spring boot项目spring-boot-jwt,按照下面步骤操作。

2.1、pom.xml引入jar包

<!-- 引入jwt-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.2</version>
</dependency>

顺便贴一下下面要用到的User类:

package com.hs.demo.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel
public class User
{
    //实体类中,Integer类型的属性加@ApiModelProperty时,必须要给example参数赋值,且值必须为数字类型。
    @ApiModelProperty(value = "用户id",example = "1")
    private Integer id;
    @ApiModelProperty(value = "用户名")
    private String userName;
    @ApiModelProperty(value = "用户密码")
    private String password;
    //getter/setter用@Data注解自动生成
}

2.2、新建Jwt工具类

Jwt工具类进行token的生成和认证,工具类代码如下:

package com.hs.demo.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.hs.demo.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * @description: Jwt工具类,生成JWT和认证
 * @author: heshi
 */
public class JwtUtil {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    /**
     * 密钥
     */
    private static final String SECRET = "my_secret";
    /**
     * 过期时间
     **/
    private static final long EXPIRATION = 1800L;//单位为秒
    /**
     * 生成用户token,设置token超时时间
     */
    public static String createToken(User user) {
        //过期时间
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        String token = JWT.create()
                .withHeader(map)// 添加头部
                //可以将基本信息放到claims中
                .withClaim("id", user.getId())//userId
                .withClaim("userName", user.getUserName())//userName
                .withClaim("password", user.getPassword())//password
                .withExpiresAt(expireDate) //超时设置,设置过期的日期
                .withIssuedAt(new Date()) //签发时间
                .sign(Algorithm.HMAC256(SECRET)); //SECRET加密
        return token;
    }
    /**
     * 校验token并解析token
     */
    public static Map<String, Claim> verifyToken(String token) {
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            jwt = verifier.verify(token);
            //decodedJWT.getClaim("属性").asString()  获取负载中的属性值
        } catch (Exception e) {
            logger.error(e.getMessage());
            logger.error("token解码异常");
            //解码异常则抛出异常
            return null;
        }
        return jwt.getClaims();
    }
}

2.3、添加JWT过滤器

JWT过滤器中进行token的校验和判断,token不合法直接返回,合法则解密数据并把数据放到request中供后续使用。

为了使过滤器生效,需要在启动类添加注解@ServletComponentScan(basePackages = "com.hs.demo.jwt")。

JWT过滤器代码如下:

package com.hs.demo.jwt;
import com.auth0.jwt.interfaces.Claim;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
 * JWT过滤器,拦截 /secure的请求
 */
@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/secure/*")
public class JwtFilter implements Filter
 {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        response.setCharacterEncoding("UTF-8");
        //获取 header里的token
        final String token = request.getHeader("authorization");
        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            chain.doFilter(request, response);
        }
        // Except OPTIONS, other request should be checked by JWT
        else {
            if (token == null) {
                response.getWriter().write("没有token!");
                return;
            }
            Map<String, Claim> userData = JwtUtil.verifyToken(token);
            if (userData == null) {
                response.getWriter().write("token不合法!");
                return;
            }
            Integer id = userData.get("id").asInt();
            String userName = userData.get("userName").asString();
            String password= userData.get("password").asString();
            //拦截器 拿到用户信息,放到request中
            request.setAttribute("id", id);
            request.setAttribute("userName", userName);
            request.setAttribute("password", password);
            chain.doFilter(req, res);
        }
    }
    @Override
    public void destroy() {
    }
}

2.4、添加LoginController

LoginController进行登录操作,登录成功后生产token并返回。

LoginController代码如下:

package com.hs.demo.jwt;
import com.hs.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
 * 登录Controller
 */
@Slf4j
@RestController
public class LoginController
 {
    static Map<Integer, User> userMap = new HashMap<>();
    static {
        //模拟数据库
        User user1 = new User(1,"张三","123456");
        userMap.put(1, user1);
        User user2 = new User(2,"李四","123123");
        userMap.put(2, user2);
    }
    /**
     * 模拟用户 登录
     */
    @RequestMapping("/login")
    public String login(User user)
    {
        for (User dbUser : userMap.values()) {
            if (dbUser.getUserName().equals(user.getUserName()) && dbUser.getPassword().equals(user.getPassword())) {
                log.info("登录成功!生成token!");
                String token = JwtUtil.createToken(dbUser);
                return token;
            }
        }
        return "";
    }
}

2.5、添加SecureController

SecureController中的请求会被JWT过滤器拦截,合法后才能访问。

SecureController代码如下:

package com.hs.demo.jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
 * 需要登录后携带JWT才能访问
 */
@Slf4j
@RestController
public class SecureController 
{
    /**
     * 查询 用户信息,登录后携带JWT才能访问
     */
    @RequestMapping("/secure/getUserInfo")
    public String login(HttpServletRequest request) {
        Integer id = (Integer) request.getAttribute("id");
        String userName = request.getAttribute("userName").toString();
        String password= request.getAttribute("password").toString();
        return "当前用户信息id=" + id + ",userName=" + userName+ ",password=" + password;
    }
}

三、接口测试

测试分两步,首先访问登录接口,登录成功后获取token,然后拿着token在访问查询用户信息接口。

3.1、访问登录接口

打开PostMan,访问http://localhost:8080/login?userName=zhangsan&password=123456,登录成功后接口返回token,请求成功截图如下:

3.2、访问用户信息接口

打开PostMan,访问http://localhost:8080/secure/getUserInfo,header里需要携带token,请求成功截图如下:

四、Token 认证的优势

相比于 Session 认证的方式来说,使用 token 进行身份认证主要有下面三个优势:

4.1、无状态

JWT实现的Token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。但是,也正是由于 token 的无状态,也导致了它最大的缺点:当后端在token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。

4.2、有效避免了CSRF 攻击

CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造,属于网络攻击领域范围。相比于 SQL 脚本注入、XSS等等安全攻击方式,CSRF 的知名度并没有它们高。但是,它的确是每个系统都要考虑的安全隐患,就连技术帝国 Google 的 Gmail 在早些年也被曝出过存在  CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。

那么究竟什么是  跨站请求伪造 呢?说简单用你的身份去发送一些对你不友好的请求。举个简单的例子:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

<a src=http://www.mybank.com/Transfer?bankId=11&money=10000>科学理财,年盈利率过万</>

导致这个问题很大的原因就是:Session 认证中 Cookie 中的 session_id 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

那为什么 token 不会存在这种问题呢?

我是这样理解的:一般情况下我们使用 JWT 的话,在我们登录成功获得 token 之后,一般会选择存放在  local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。

但是这样会存在  XSS 攻击中被盗的风险,为了避免 XSS 攻击,你可以选择将 token 存储在标记为 httpOnly   的cookie 中。但是,这样又导致了你必须自己提供CSRF保护。

具体采用上面哪两种方式存储 token 呢,大部分情况下存放在  local storage 下都是最好的选择,某些情况下可能需要存放在标记为 httpOnly  的cookie 中会更好。

4.3、适合移动端应用

使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。

但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。

4.4、单点登录友好

使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。

五、Token 认证常见问题以及解决办法

5.1、注销登录等场景下 token 还有效

这个问题不存在于 Session  认证方式中,因为在  Session  认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 token 认证的方式就不好解决了。我们也说过了,token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。那么,我们如何解决这个问题呢?查阅了很多资料,总结了下面几种方案:

对于修改密码后 token 还有效问题的解决还是比较容易的,说一种我觉得比较好的方式:使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

5.2、token 的续签问题

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。

总结

JWT 最适合的场景是不需要服务端保存用户状态的场景,如果考虑到 token 注销和 token 续签的场景话,没有特别好的解决方案,大部分解决方案都给 token 加上了状态,这就有点类似 Session 认证了。

参考链接:

基于Token的WEB后台认证机制

SpringBoot拦截器Interceptor

Spring Boot实战:拦截器与过滤器

chain.doFilter(request,response)含义

到此这篇关于SpringBoot集成 JWT实现用户登录认证的项目实践的文章就介绍到这了,更多相关SpringBoot JWT用户登录认证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文