springboot 中使用JWT保护资源安全
- 什么是JWT
Json Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
尽管jwt可以被加密以提供双方之间的保密性,但我们将重点关注签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是签名方。
- 什么时候使用JWT
以下是JSON Web Tokens使用的一些场景:
授权:这是使用JWT最常见的场景。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录(Single-Sign-On)是目前广泛使用JWT的一个特性,因为它的开销很小,而且能够轻松地跨不同的域使用。
信息交换:JSON Web Tokens 是一种在各方之间安全传输信息的好方法。因为jwt可以使用公钥/私钥对进行签名,因此可以确保发送者是他们所说的人。另外,由于签名是使用报头和有效负载计算的,所以您还可以验证内容没有被篡改。
- JWT结构
JSON Web Tokens 由三个部分组成,用点(.)隔开,分别是:
1、Header
2、Payload
3、Signature
如下形式:
xxxx.yyyy.zzzz
Header(头)
头通常由两部分组成:Token的类型(JWT)和 签名算法,如HMAC、SHA256或RSA。
示例:
{ "alg": "HS256", "typ": "JWT" }
这个JSON会被Base64Url编码,形成JWT的第一部分。
Payload(负载)
Token的第二部分是有效负载,它包含声明。声明是关于实体(通常是用户)和附加数据。有三种类型的声明:注册声明、公共声明和私有声明。
注册声明:这些是一组预定义的声明,这些声明不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中包括:iss(发行者)、exp(到期时间)、sub(主题)、aud(接受jwt的一方)和其他。
公共声明:这些可以由使用jwt的用户随意定义。一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,这些声明既不是注册的,也不是公开的。
示例:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
然后对有效负载进行Base64Url编码,以形成JSON Web Tokens的第二部分。
Signature(签名)
要创建签名部分,您必须获取编码的头、负载、密钥、算法,并对其进行签名。
示例:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
签名用于验证消息在整个过程中没有被更改,如果是使用私钥签名的Token的情况下,它还可以验证JWT的发送者是它所说的人。
JWT示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6IjEiLCJleHAiOjE2MDY5NzA0ODd9.uMB243IGMnms3KtYPqZR4JJQoXePdzdBg8X8uaOkISI
接下来在springboot中使用jwt
加入依赖:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency>
实体,DAO,Service类
@Entity @Table(name = "T_USERS") public class Users implements Serializable { private static final long serialVersionUID = 1L; @Id private String id ; private String username ; private String password ; } public interface UsersRepository extends JpaRepository<Users, String>, JpaSpecificationExecutor<Users> { Users findByUsernameAndPassword(String username, String password) ; } @Service public class UsersService { @Resource private UsersRepository usersRepository ; public Users login(String username, String password) { return usersRepository.findByUsernameAndPassword(username, password) ; } }
两个API接口,一个登录,一个作为测试资源接口
@RestController @RequestMapping("/users") public class UsersController { @Resource private UsersService usersService ; @GetMapping("/login") public String login(String username, String password) { Users users = usersService.login(username, password) ; if (users != null) { return JwtUtils.genToken(users) ; } return null ; } } @RestController @RequestMapping("/res") public class ResourceController { @GetMapping("/info") public Object infos() { return "resource" ; } }
生成JWT 的工具类:
public class JwtUtils { public static String genToken(Users users) { Builder builder = JWT.create() ; builder.withClaim("id", users.getId()) ; builder.withClaim("name", users.getUsername()) ; LocalDateTime ldt = LocalDateTime.now().plusMinutes(30) ; // 过期时间 builder.withExpiresAt(Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant())) ; // 算法 Algorithm algorithm = Algorithm.HMAC256("abc") ; return builder.sign(algorithm) ; } }
为了简单,密码直接写死“abc”
这里可以看看是怎么生成的token。sign方法:
进入JWTCreator的sign方法
这里接看出来JWT 3部分是怎么生成的了。
拦截器
public class SecurityInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = extractToken(request) ; if (token == null) { throw new RuntimeException("缺失有效token信息") ; } JWTVerifier verify = JWT.require(Algorithm.HMAC256("abc")).build() ; try { verify.verify(token) ; } catch (Exception e) { throw new RuntimeException(e) ; } return HandlerInterceptor.super.preHandle(request, response, handler); } private String extractToken(HttpServletRequest request) { String token = request.getHeader("access-token") ; if (token == null) { token = request.getParameter("access-token") ; } return token ; } }
拦截器配置
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/res/**") ; } }
到此所有的类完成
测试:
直接访问资源接口/res/info
这里没有写全局异常拦截,看到效果即可。
通过登录获取token
将token信息加入到其他接口(资源接口)的请求header中