SpringBoot 使用 JWT

SpringBoot 使用 JWT

登录方式对比

  1. 客户端向服务器发送用户名和密码
  2. 服务器验证通过,并把相关数据保存在 Session 中,例如登录时间之类的
  3. 服务器返回给用户一个 SessionId ,客户端把这个 SessionId 写入 Cookie
  4. 用户每次请求都会通过 Cookie 提交 SessionId 到服务器
  5. 服务器收到 SessionId 后查找数据,就可以知道用户身份

特点:

  • 数据存储在服务器,安全性较强,但是占用服务器资源
  • 因为使用到了 Cookie ,所以会被伪造
  • 如果服务器较多,或者跨域访问之类的操作,就要求共享 Session 资源,否则就需要用户和服务器重复登录验证操作,或者记录用户登录的服务器,对服务器和用户体验都不好。

JWT 方式登录

  1. 客户端向服务器发送用户名和密码
  2. 服务器验证通过,对用户数据进行加密,生成 Token 返回给客户端
  3. 浏览器(客户端)接收到 Token 后,将 Token 存储在 Local Storage,需要使用 JavaScript 代码获取,而 Cookie 是自动携带
  4. 用户每次请求都把 Token 提交到服务器
  5. 服务器对传来的 Token 进行解密,再去查询用户数据,一次知道用户身份

特点:

  • 存储在客户端,不占用服务器资源,但是同样会被伪造
  • 前后端分离,带上 Token 进行请求,不需要考虑用户是在哪个服务器上登录的,多服务器和跨域请求都没有问题

建议:对数据库的增删改,必须加上 Token 验证,查询不加 Token ,这样效率会比较高,同时查询操作也无法获取 Token ,更安全

如何强制token失效?
在数据库里保存一份 Token ,验证时再拿出来校验,重新登录就刷新覆盖这个值

JWT

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。

先看概念

官网:https://jwt.io/
这张图来自官网

JWT 结构

JWT 分为三个部分

  • Header,算法和令牌类型
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload:数据,实际需要传递的 JSON 对象
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • Verify Signature:签名,用于防伪验证
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    your-256-bit-secret
)

加密之后的结果:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

终于可以上代码了

代码

新建一个 SpringBoot 项目

application.properties ,我只配置了最基本的

#配置程序端口,默认为8080
server.port= 8080
# 配置默认访问路径,默认为/
server.servlet.context-path=/jwt_demo

# 配置 Tomcat
# 配置Tomcat编码,默认为UTF-8
server.tomcat.uri-encoding=UTF-8
# 配置最大线程数
server.tomcat.max-threads=1000

JWT 依赖库

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

模型类

因为我要用 RESTful 所以可以用一个模型类

public class UserModel
{
    private String _username;
    private String _password;

    public String get_username()
    {
        return _username;
    }

    public void set_username(String _username)
    {
        this._username = _username;
    }

    public String get_password()
    {
        return _password;
    }

    public void set_password(String _password)
    {
        this._password = _password;
    }
}

控制器

DemoController ,用于测试访问数据,Foo_01()不需要Token,Foo_02()需要 Token

@RestController
@RequestMapping("/v1/Demo")
public class DemoController
{
    @RequestMapping(value = "/Get1",method = RequestMethod.GET,produces = "application/json")
    public List<String> Foo_01()
    {
        List<String> list=new ArrayList<>();

        list.add("Foo_01");
        list.add("Test");

        return list;
    }

    @RequestMapping("/Get2")
    public List<String> Foo_02()
    {
        List<String> list=new ArrayList<>();

        list.add("Foo_02");
        list.add("Test");

        return list;
    }
}

LoginController ,用于登录并获取 Token

@RestController
@RequestMapping("/v1/Login")
public class LoginController
{
    @RequestMapping(value = "/Login", method = RequestMethod.POST, produces = "application/json")
    public String Login(@RequestBody UserModel user)
    {
        if (user.get_username().equals("abc") && user.get_password().equals("123456"))
        {
            Map<String, String> claimMap = new HashMap<>();

            claimMap.put("username", "abc");

            return TokenUtli.GenerateToken(claimMap);
        }

        return "登录失败";
    }
}

封装 JWT 工具类

其实我封装的很随便啦,仅用于本案例

public class TokenUtli
{
    //Issuer
    public static final String ISSUER = "Test.com";
    //Audience
    public static final String AUDIENCE = "Client";
    //密钥
    public static final String KEY = "ThisIsMySecretKey";
    //算法
    public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
    //Header
    public static final Map<String, Object> HEADER_MAP = new HashMap<>()
    {
        {
            put("alg", "HS256");
            put("typ", "JWT");
        }
    };

    /**
     * 生成 Token 字符串
     *
     * @param claimMap claim 数据
     * @return Token 字符串
     */
    public static String GenerateToken(Map<String, String> claimMap)
    {
        Date nowDate = new Date();
        //120 分钟过期
        Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);

        //Token 建造器
        JWTCreator.Builder tokenBuilder = JWT.create();

        for (Map.Entry<String, String> entry : claimMap.entrySet())
        {
            //Payload 部分,根据需求添加
            tokenBuilder.withClaim(entry.getKey(), entry.getValue());
        }

        //token 字符串
        String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
                .withIssuer(TokenUtli.ISSUER)//issuer
                .withAudience(TokenUtli.AUDIENCE)//audience
                .withIssuedAt(nowDate)//生效时间
                .withExpiresAt(expireDate)//过期时间
                .sign(TokenUtli.ALGORITHM);//签名,算法加密

        return token;
    }

    /**
     * 时间加法
     *
     * @param date   当前时间
     * @param minute 持续时间(分钟)
     * @return 时间加法结果
     */
    private static Date AddDate(Date date, Integer minute)
    {
        if (null == date)
        {
            date = new Date();
        }
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.add(Calendar.MINUTE, minute);

        return calendar.getTime();
    }
}

在此示例中,我们指定了必须考虑哪些参数才能将 JWT 视为有效。根据我们的代码,以下项目认为令牌有效:

  • 验证生成令牌的服务器 Issuer
  • 验证令牌的接收者被授权接收 Audience
  • 检查令牌是否未过期以及颁发者的签名密钥是否有效 Lifetime
  • 验证令牌的签名 IssuerSigningKey
  • 此外,我们指定Issuer、Audience、SigningKey的值。在本例中,我将这些值存储在常量中。

验证 Token ,Token 错误或者过期就抛出异常,这里我就只判断了一个空字符串,其它验证规则可以自己写啦

/**
  * 验证 Token
  *
  * @param webToken 前端传递的 Token 字符串
  * @return Token 字符串是否正确
  * @throws Exception 异常信息
  */
public static boolean VerifyJWTToken(String webToken) throws Exception
{
    String[] token = webToken.split(" ");

    if (token[1].equals(""))
    {
        throw new Exception("token错误");
    }


    //JWT验证器
    JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();

    //解码
    DecodedJWT jwt = verifier.verify(token[1]);//如果 token 过期,此处就会抛出异常

    //Audience
    List<String> audienceList = jwt.getAudience();
    String audience = audienceList.get(0);

    //Payload
    Map<String, Claim> claimMap = jwt.getClaims();
    for (Map.Entry<String, Claim> entry : claimMap.entrySet())
    {

    }

    //生效时间
    Date issueTime = jwt.getIssuedAt();
    //过期时间
    Date expiresTime = jwt.getExpiresAt();

    return true;
}

注册拦截器

JWTInterceptor

public class JWTInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        //从请求头内获取token
        String token = request.getHeader("authorization");

        //验证令牌,如果令牌不正确会出现异常会被全局异常处理
        return TokenUtli.VerifyJWTToken(token);
    }
}

注册拦截器

@Configuration
public class InterceptorConfig implements WebMvcConfigurer
{
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/**")//全部路径
                .excludePathPatterns("/v1/Demo/Get1")//排除不需要Token的路径
                .excludePathPatterns("/v1/Login/Login");//开放登录路径
    }
}

测试

因为懒得写前端页面,所以使用 postman 调试

首先,不登录去访问 DemoController 里的两个函数

报了 500错误

登录,以及生成的 Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJDbGllbnQiLCJpc3MiOiJUZXN0LmNvbSIsImV4cCI6MTYyNjY4OTczMiwiaWF0IjoxNjI2NjgyNTMyLCJ1c2VybmFtZSI6ImFiYyJ9.ZtSyoqmUVKgGY3_wFQD24_mOot7qnMUKh8xMPKOW2JQ

使用 Token 再去测试需要 Token 的函数

这次就访问到数据了

SpringBoot 使用 JWT 结束

项目结构

写完才发现,我Util又拼错了,拉倒吧,之后整 AOP 重新写过吧

posted @ 2021-07-19 16:19  .NET好耶  阅读(2625)  评论(0编辑  收藏  举报