一、Token 的定义与概念
1.1 Token 是什么?
Token(令牌)是一个用于身份认证的凭证,通常是由服务器生成并返回给客户端。客户端在后续请求中携带该 Token,服务器通过解析 Token 验证用户身份,从而决定是否授权访问资源。Token 的一个显著特点是其 无状态性,这意味着服务器不需要存储关于用户会话的数据,而是通过解析客户端携带的 Token 来获取所有的身份信息。
1.2 无状态性与自包含性
无状态性:在 Token 认证机制中,服务器不需要保存任何关于会话的状态数据。每次请求,客户端都会在请求头中附带 Token,服务器通过解析 Token 来验证身份。这样的设计有助于减少服务器存储压力,尤其适合分布式和微服务架构。
自包含性:以 JWT 为代表的 Token,通常包含了用户的身份信息、权限信息等数据,这些信息直接存储在 Token 本身中。这样,服务器在接收到 Token 后无需查询数据库或其他后端服务,直接通过解析 Token 获取信息。
1.3 Token 的工作流程
1.3.1 基本工作流程
Token 认证的基本流程如下:
用户登录:用户通过登录界面提供用户名和密码,发送给服务器。
服务器验证并生成 Token:服务器验证用户身份(如验证用户名和密码),如果验证通过,生成一个 Token(例如 JWT),并返回给客户端。
客户端存储 Token:客户端收到 Token 后,将其存储在本地,常见的存储方式有 localStorage、sessionStorage 或 Cookie。
客户端在后续请求中携带 Token:在随后的 HTTP 请求中,客户端将 Token 附加到请求头的 Authorization 部分。
服务器验证 Token:服务器解析 Token,验证其有效性,确保 Token 没有过期,并根据 Token 中的信息(如用户身份、权限等)判断是否允许访问相应的资源。
1.3.2 工作流程图示
Syntax error in text
mermaid version 10.9.1
如上图所示,Token 认证的流程非常直观。用户登录后,服务器返回 Token,客户端存储 Token 并在后续请求中携带该 Token,服务器验证并提供资源。
解释:
用户向服务器提交用户名和密码进行身份验证。
服务器返回一个 Token(如 JWT),该 Token 包含用户的身份信息和权限。
客户端将该 Token 存储在本地存储中,以便后续使用。
客户端在每次请求时,将 Token 通过 HTTP 请求头发送给服务器。
服务器根据 Token 验证用户身份和权限,授权访问资源。
二、Token 的常见用途
2.1 用户身份验证
Token 最常见的用途是进行用户身份验证。例如,当用户登录时,服务器通过验证用户名和密码,生成一个 Token,返回给客户端。在之后的请求中,客户端将该 Token 附加到请求中,服务器通过验证 Token 确认用户身份,并授权访问相关资源。
2.2 授权控制
Token 不仅包含身份信息,还可以包含关于用户角色、权限等信息。因此,服务器可以根据 Token 中的数据来判断用户是否有权访问某些资源。例如,后台管理系统可能基于 Token 中的角色信息来决定用户是否有权限访问管理页面或执行管理操作。
2.3 防止跨站请求伪造(CSRF)
传统的基于 Cookie 的身份认证机制容易受到 CSRF(跨站请求伪造)攻击,因为攻击者可以诱导用户访问恶意网站,从而利用用户的身份信息发起请求。而 Token 认证通过将身份信息存储在客户端,并通过 HTTP 请求头传递(而非通过 Cookie),从而有效防止了 CSRF 攻击。
2.4 跨域认证
在现代的前后端分离架构中,前端和后端通常分布在不同的域名下。Token 认证非常适合跨域认证,因为 Token 是通过 HTTP 请求头传递的,不受浏览器的跨域限制。因此,Token 机制可以方便地用于跨域认证。
三、Token 的常见类型
3.1 JWT(JSON Web Token)
JWT 是一种开放标准,用于在网络应用环境中传递声明。JWT 包含三部分:Header(头部)、Payload(载荷)、Signature(签名)。
3.1.1 JWT 的结构
JWT 的结构非常简单,由三部分组成:
Header(头部):通常包含 Token 的类型(即 JWT)和签名算法(如 HMAC SHA256 或 RSA)。
{
"alg": "HS256",
"typ": "JWT"
}
Payload(载荷):包含用户信息和其他元数据。JWT 中的 Payload 是 Base64Url 编码的,因此可以直接解码查看,但不应存储敏感信息。
{
"user_id": "123",
"role": "admin"
}
Signature(签名):签名用于验证 Token 是否被篡改。服务器使用 Header 和 Payload 部分以及密钥生成签名。
HMACSHA256(encode(Header) + "." + encode(Payload), SECRET_KEY)
4.再看一个举例:
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
AI代码解释
{"typ":"JWT","alg":"HS256"}在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
AI代码解释
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2 的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24 个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中 提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的 完成基于 BASE64 的编码和解码
(1)标准中注册的声明(建议但不强制使用)
AI代码解释
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。(2)公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
(3)私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
定义一个payload:
AI代码解释
{"sub":"1234567890","name":"John Doe","admin":true}然后将其进行base64加密,得到Jwt的第二部分。
AI代码解释
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9JWT是一个签证信息,由三部分组成:
header (base64后的) payload (base64后的) secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第 三部分。
代码语言:javascript
AI代码解释
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
AI代码解释
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg
Q注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JJWT是一个JWT创建和验证的Java库。
(1)引入依赖
AI代码解释
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>(2)创建类CreatejwtTest,用于生成token
AI代码解释
public class CreateJwtTest {
public static void main(String[] args) {
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())//用于设置签发时间
.signWith(SignatureAlgorithm.HS256,"wangmh");//用于设置签名秘钥
System.out.println( builder.compact() );
}
}(3)测试
AI代码解释
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0M
TM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk
#再次运行,每次运行结果都会不一样,因为我们载荷中包含了时间AI代码解释
public class ParseJwtTest {
public static void main(String[] args) {
String compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk";
Claims claims =
Jwts.parser().setSigningKey("wangmh").parseClaimsJws(compactJws).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("IssuedAt:"+claims.getIssuedAt());
}
}
//试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个 过期时间。
AI代码解释
public class CreateJwtTest2 {
public static void main(String[] args) {
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"wangmh")
.setExpiration(new Date(exp));//用于设置过期时间
System.out.println( builder.compact() );
}
}AI代码解释
public class ParseJwtTest {
public static void main(String[] args) {
String compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTY1NjksImV4cCI6MTUyMzQxNjYyOX0.Tk91b6mvyjpKcldkic8DgXz0zsPFFnRgTgkgcAsa9cc";
Claims claims =Jwts.parser().setSigningKey("wangmh").parseClaimsJws(compactJws).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
SimpleDateFormat sdf=new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
System.out.println("签发时间:"+sdf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
System.out.println("当前时间:"+sdf.format(new Date()) );
}
}
//测试运行,当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims
AI代码解释
public class CreateJwtTest3 {
public static void main(String[] args) {
//为了方便测试,我们将过期时间设置为1分钟
long now = System.currentTimeMillis();//当前时间
long exp = now + 1000*60;//过期时间为1分钟
JwtBuilder builder= Jwts.builder().setId("888")
.setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"wangmh")
.setExpiration(new Date(exp))
.claim("roles","admin")
.claim("logo","logo.png");
System.out.println( builder.compact() );
String compactJwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTQ5NDcxNTksImV4cCI6MTU1NDk0NzIxOSwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJsb2dvLnBuZyJ9.HD_myvdNjOGGnu4p8Z8QX9dnXHJEZa0nsLKxOFRiYJY";
Claims claims=Jwts.parser().setSigningKey("wangmh").parseClaimsJws(compactJwt).getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("roles:"+claims.get("roles"));
System.out.println("logo:"+claims.get("logo"));
SimpleDateFormat sdf=new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
System.out.println("签发时间:"+sdf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
System.out.println("当前时间:"+sdf.format(new Date()) );
}
}AI代码解释
//JwtUtil.java
@ConfigurationProperties("jwt.config")
public class JwtUtil {
private String key ;
private long ttl ;//一个小时
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* 生成JWT
*
* @param id
* @param subject
* @return
*/
public String createJWT(String id, String subject, String roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
if (ttl > 0) {
builder.setExpiration( new Date( nowMillis + ttl));
}
return builder.compact();
}
/**
* 解析JWT
* @param jwtStr
* @return
*/
public Claims parseJWT(String jwtStr){
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}AI代码解释
#配置文件
jwt:
config:
key: wangmh
ttl: 3600000登录鉴权
代码语言:javascript
AI代码解释
//Controller.java
@Autowired
private JwtUtil jwtUtil;
@RequestMapping(value="/login",method=RequestMethod.POST)
public Result login(@RequestBody Map<String,String> loginMap){
Admin admin =adminService.findByLoginnameAndPassword(loginMap.get("loginname"),loginMap.get("password"));
if(admin!=null){
//生成token
String token = jwtUtil.createJWT(admin.getId(),admin.getLoginname(), "admin");
Map map=new HashMap();
map.put("token",token);
map.put("name",admin.getLoginname());//登陆名
return new Result(true,StatusCode.OK,"登陆成功",map);
}else{
return new Result(false,StatusCode.LOGINERROR,"用户名或密码错误");
}
}删除用户功能鉴权
AI代码解释
@Autowired
private HttpServletRequest request;
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
String authHeader = request.getHeader("Authorization");//获取头信息
if(authHeader==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!authHeader.startsWith("Bearer ")){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
String token=authHeader.substring(7);//提取token
Claims claims = jwtUtil.parseJWT(token);
if(claims==null){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
if(!"admin".equals(claims.get("roles"))){
return new Result(false,StatusCode.ACCESSERROR,"权限不足");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}使用拦截器方式实现token鉴权
如果我们每个方法都去写一段代码,冗余度太高,不利于维护。因此我们可以使用拦截器的方式去实现token鉴权
1.添加拦截器
Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,继承此类,可以非常方便的实现自己的拦截器。他有三个方法:分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲染)、返回处理(已经渲染了页面) 在preHandle中,可以进行编码、安全控制等处理; 在postHandle中,有机会修改ModelAndView; 在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。
1.1创建拦截器类
AI代码解释
//JwtFilter.java
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler)throws Exception {
System.out.println("经过了拦截器");
final String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
final String token = authHeader.substring(7); // The partafter "Bearer "
Claims claims = jwtUtil.parseJWT(token);
if (claims != null) {
if("admin".equals(claims.get("roles"))){//如果是管理员
request.setAttribute("admin_claims", claims);
}
if("user".equals(claims.get("roles"))){//如果是用户
request.setAttribute("user_claims", claims);
}
}
}
return true;
}
}1.2配置拦截器类
AI代码解释
@Configuration
public class ApplicationConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtFilter jwtFilter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter).
addPathPatterns("/**").
excludePathPatterns("/**/login");
}
}1.3删除功能实现
AI代码解释
/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
Claims claims=(Claims) request.getAttribute("admin_claims");
if(claims==null){
return new Result(true,StatusCode.ACCESSRROR,"无权访问");
}
userService.deleteById(id);
return new Result(true,StatusCode.OK,"删除成功");
}原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。