文章目录
前言
前面已经完成了用户注册,这里我们基于之前的用户表,完成用户的登录接口。
一、JWT
1. 什么是JWT
JWT全称JSON Web Token,JSON Web令牌(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于作为JSON对象在各方之间安全地传输信息。此信息是经过数字签名的,因此可以验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
虽然JWT可以加密,也可以在各方之间提供保密性,但我们将专注于签名代币。签名令牌可以验证其中包含的声明的完整性,而加密令牌则向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是签名方。
2. 使用场景
以下是JSON Web令牌有用的一些场景:
授权:这是使用JWT最常见的场景。一旦用户登录,每个后续请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是目前广泛使用JWT的一个功能,因为它的开销很小,并且能够在不同的域中轻松使用。
信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以签名——例如,使用公钥/私钥对——所以你可以确保发送者就是他们所说的那个人。此外,由于签名是使用标头和有效载荷计算的,因此还可以验证内容是否未被篡改。
3. 结构
以
.
作为各部分分隔符
xxxxx.yyyyy.zzzzz
- Header
- Payload
- Signature
3.1 Header
算法可以使用 HMAC SHA256 or RSA
{
"alg": "HS256",
"typ": "JWT"
}
java-jwt 支持以下签名和验证算法:
JWS | Algorithm | Description |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
3.2 Payload
有效载荷用来存放一些用户信息,但不建议存放用户私密信息,以免造成不必要的安全隐患。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3.3 Signature
签名用于验证消息在发送过程中没有更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发送者是它所说的那个人。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
4. 使用
在身份验证中,当用户使用其凭据成功登录时,将返回一个JSON Web令牌。由于令牌是凭据,因此必须非常小心地防止出现安全问题。一般来说,您不应将令牌保存的时间超过所需的时间。
每当用户想要访问受保护的路由或资源时,用户代理都应该发送JWT,通常在Authorization头中使用Bearer模式。标头的内容应如下所示:
Authorization: Bearer <token>
请注意,如果您通过HTTP头发送JWT令牌,则应尽量防止它们变大。有些服务器不接受超过8 KB的标头。如果您试图在JWT令牌中嵌入太多信息,例如通过包括所有用户的权限,您可能需要一个替代解决方案,如Auth0细粒度授权。
如果令牌是在Authorization标头中发送的,则跨来源资源共享(CORS)不会成为问题,因为它不使用cookie。
下图显示了如何获取JWT并将其用于访问API或资源:
二、案例
1.引入库
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.JwtUtils
package org.example.springboot3.bigevent.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static final String SECRET = "167589447321";
/**
* 生成token
* @param claims 用户信息
* @return String
*/
public static String create(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withIssuer("auth0")
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证token
* @param token token
* @return boolean
*/
public static boolean verify(String token) {
DecodedJWT decodedJWT;
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// specify any specific claim validations
.withIssuer("auth0")
// reusable verifier instance
.build();
decodedJWT = verifier.verify(token);
} catch (JWTVerificationException exception){
// Invalid signature/claims
return false;
}
return true;
}
/**
* 解析token数据
* @param token token
* @return Map
*/
public static Map<String, Object> getClaims(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.withIssuer("auth0")
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
3. UserController1
package org.example.springboot3.bigevent.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.example.springboot3.bigevent.entity.Result;
import org.example.springboot3.bigevent.entity.User;
import org.example.springboot3.bigevent.service.UserSerivce;
import org.example.springboot3.bigevent.utils.JwtUtils;
import org.example.springboot3.bigevent.utils.Md5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Create by zjg on 2024/5/22
*/
@RequestMapping("/user/")
@RestController
@Validated
public class UserController1 {
@Autowired
UserSerivce userSerivce;
@RequestMapping("register")
public Result register(@Pattern(regexp = "^\\S{6,20}$",message = "用户名长度为6-20位") String username,@Pattern(regexp = "^\\S{8,20}$",message = "密码为8-20位") String password){
User user=userSerivce.findUserByName(username);
if(user==null){//用户不存在,可以注册
int i=userSerivce.addUser(username,password);
if(i!=1){
return Result.error("失败注册,请稍后重新注册!");
}
}else{
return Result.error("该用户已存在,请重新注册!");
}
return Result.success();
}
@RequestMapping("register1")
public Result register1(@Valid User user){
if(userSerivce.findUserByName(user.getUsername())==null){//用户不存在,可以注册
int i=userSerivce.addUser(user.getUsername(),user.getPassword());
if(i!=1){
return Result.error("失败注册,请稍后重新注册!");
}
}else{
return Result.error("该用户已存在,请重新注册!");
}
return Result.success();
}
@RequestMapping("login")
public Result login(@Valid User loginUser){
String message="用户名/密码不正确";
User user = userSerivce.findUserByName(loginUser.getUsername());
if(user!=null){//用户存在
if(user.getPassword().equals(Md5Util.getMD5String(loginUser.getPassword()))){//密码正确
Map<String,Object> claims=new HashMap();
claims.put("userId",user.getId());
claims.put("username",user.getUsername());
String token = JwtUtils.create(claims);
return Result.success("登录成功",token);
}
}
return Result.error(message);
}
//todo 登出暂未使用
@RequestMapping("logout")
public Result logout(@Valid User loginUser){
String message="用户名/密码不正确";
User user = userSerivce.findUserByName(loginUser.getUsername());
if(user!=null){//用户存在
if(user.getPassword().equals(Md5Util.getMD5String(loginUser.getPassword()))){//密码正确
Map<String,Object> claims=new HashMap();
claims.put("userId",user.getId());
claims.put("userUsername",user.getUsername());
String token = JwtUtils.create(claims);
return Result.success(token);
}
}
return Result.error(message);
}
}
4. ArticleController
主要为测试使用
package org.example.springboot3.bigevent.controller;
import org.example.springboot3.bigevent.entity.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Create by zjg on 2024/5/26
*/
@RestController
@RequestMapping("/article/")
public class ArticleController {
@RequestMapping("list")
public Result<String> list(){
return Result.success("article.list");
}
}
三、拦截器
1. 定义拦截器
我们使用拦截器来对接口进行横切面的token验证
package org.example.springboot3.bigevent.inceptors;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.springboot3.bigevent.entity.Result;
import org.example.springboot3.bigevent.utils.JwtUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* Create by zjg on 2024/5/26
*/
@Component
public class LoginInceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if(token!=null&&token.contains("Bearer")){
boolean verify = JwtUtils.verify(token.substring(token.indexOf("Bearer")+7));
if(verify){
return true;
}
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writerFor(Result.class);
String message = objectMapper.writeValueAsString(Result.error("token验证失败,请重新获取token后重试!"));
response.getWriter().println(message);
return false;
}
}
2. 注册拦截器
拦截器拦截初注册和登录之外的所有请求
package org.example.springboot3.config;
import org.example.springboot3.bigevent.inceptors.LoginInceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* Create by zjg on 2024/5/26
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
LoginInceptor loginInceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns=new ArrayList<>();
patterns.add("/user/register");
patterns.add("/user/login");
registry.addInterceptor(loginInceptor).excludePathPatterns(patterns);
}
}
四、测试
1. 登录
2. 无token
3. 有token
4. 全局配置
在Postman中可以对目录配置token认证信息,对目录下的全部接口生效。