jwt token 附加用户信息_基于JWT的Token认证机制实现

1、JWT简介

1.1什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用

户和服务器之间传递安全可靠的信息。

1.2 JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以

被表示成一个JSON对象。

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

在头部指明了签名算法是HS256算法。 将上面的json字符串进行BASE64编码

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

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

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:

{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64编码,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流 露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

2、java的JJWT实现JWT测试

(1)导入依赖

io.jsonwebtoken

jjwt

0.6.0

(2)创建测试类,用于生成token

@Test

public void testCreat(){

Date loginTime = new Date();//登录时间Date limitTime = new Date(loginTime.getTime() + 60000);//设置过期时间为1分钟

JwtBuilder jwtBuilder = Jwts.builder()

.setId("111") //用户id.setSubject("小周") //用户名.setIssuedAt(loginTime)//登录时间.signWith(SignatureAlgorithm.HS256,"zhouran") //指定头和盐.setExpiration(limitTime);//设置过期时间 .claim("roles","admin")//添加自定义属性.claim("hobby","code") ;//添加自定义属性

System.out.println(jwtBuilder.compact());//将jwtBuilder变成字符串

//结果://eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTEiLCJzdWIiOiLlsI_lkagiLCJpYXQiOjE1ODQyNTczMzcsImV4cCI6MTU4NDI1NzM5N30.bctrsV-FId6vspTsGu144gJR_swrE_ZiVD8wrGUI9k4

}}

(2)创建测试类,用于解析token

@Test

public void testParse(){

String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTEiLCJzdWIiOiLlsI_lkagiLCJpYXQiOjE1ODQyNTczMzcsImV4cCI6MTU4NDI1NzM5N30.bctrsV-FId6vspTsGu144gJR_swrE_ZiVD8wrGUI9k4";

Claims claims = Jwts.parser()

.setSigningKey("zhouran") //盐.parseClaimsJws(token)//需要解析的token.getBody(); //返回一个Claims对象,里面是键值对

System.out.println("用户id:"+claims.getId());

System.out.println("用户昵称:"+claims.getSubject());

System.out.println("用户登录时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getIssuedAt()));

System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(claims.getExpiration()));

System.out.println("角色:"+claims.get("roles"));

System.out.println("爱好:"+claims.get("hobby"));

}

结果:

用户id:111

用户昵称:小周

用户登录时间:2020-03-15 15:34:07

过期时间:2020-03-15 15:35:07

角色:admin

爱好:code

注意,如果时间超了,执行此方法会报错,所以要try-catch一下

3、springboot继集成JWT

3.1编写JWT工具类

@ConfigurationProperties("jwt.config")

public class JwtUtil {

private String salt ;//盐

private long limitTime ; //过期时间 毫秒

public String getSalt() {

return salt;

}

public void setSalt(String salt) {

this.salt = salt;

}

public long getLimitTime() {

return limitTime;

}

public void setLimitTime(long limitTime) {

this.limitTime = limitTime;

}

/*** 生成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, salt)

.claim("roles", roles);

if (limitTime > 0) {

builder.setExpiration( new Date( nowMillis + limitTime));

}

return builder.compact();

}

/*** 解析JWT* @param jwtStr* @return*/

public Claims parseJWT(String jwtStr){

return Jwts.parser()

.setSigningKey(salt)

.parseClaimsJws(jwtStr)

.getBody();

}

}

3.2、在需要用到jwt的工程下

编写配置文件

jwt:

config:

salt: selfSalt

limitTime: 3600000 #1个小时

将工具类导入环境中

@Bean

public JwtUtil jwtUtil(){

return new JwtUtil();

}

3.3、使用

@Autowired

private JwtUtil jwtUtil;

/*** 登录* @return*/

@PostMapping("/login")

public Result login(@RequestBody Admin admin){

admin = adminService.login(admin);

if(admin==null){

return new Result(false,StatusCode.LOGINERROR,"登录失败");

}

//使前后端可以通话的操作,使用jwt实现//生成令牌String roles = "admin";

String token = jwtUtil.createJWT(admin.getId(), admin.getLoginname(), roles);

//将令牌和角色一起传给前端Map map = new HashMap<>();

map.put("token",token);

map.put("roles",roles);

return new Result(true,StatusCode.OK,"登录成功",map);

}

4、测试权限功能

需求:删除用户,必须拥有管理员权限,否则不能删除。

前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格 +token

(1)修改UserController的findAll方法 ,判断请求中的头信息,提取token并验证权

限。

/*** 删除** 必须有管理员权限才可以删除用户** 前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格 +token** @param id*/

@RequestMapping(value="/{id}",method= RequestMethod.DELETE)

public Result delete(@PathVariable String id, HttpServletRequest request){

String authorization = request.getHeader("Authorization");

//约定为空 if(authorization == null || "".equals(authorization)){

return new Result(false,StatusCode.ACCESSERROR,"权限不足1——约定为空");

}

// 判断Bearer+空格 if(!authorization.startsWith("Bearer ")){

return new Result(false,StatusCode.ACCESSERROR,"权限不足2——约定有误");

}

//得到token 从第七位往后都是token String token = authorization.substring(7);

try {

Claims claims = jwtUtil.parseJWT(token);

if(claims==null){

return new Result(false,StatusCode.ACCESSERROR,"权限不足5——令牌为空");

}

String roles = (String) claims.get("roles");

if (!roles.contains("admin")) {

return new Result(false,StatusCode.ACCESSERROR,"权限不足3——没有管理员权限");

}

} catch (Exception e) {

return new Result(false,StatusCode.ACCESSERROR,"权限不足4——时间过期");

}

userService.deleteById(id);

return new Result(true,StatusCode.OK,"删除成功");

}

问题:每次验证用户权限时,都要写这么长的代码,很多都是重复的

解决:将该功能放到拦截器中

Spring为我们提供了

org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,

继承此类,可以非常方便的实现自己的拦截器。他有三个方法:

分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲

染)、返回处理(已经渲染了页面)

在preHandle中,可以进行编码、安全控制等处理;

在postHandle中,有机会修改ModelAndView;

在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。

问题:

springboot没有配置文件,该如何将拦截器放入容器中?

注册拦截器:

/*** @author: zhouR* @date: Create in 2020/3/15 - 17:18* @function: 注册拦截器*/

@Configuration

public class InterceptorConfig extends WebMvcConfigurationSupport {

@Autowired

private JwtInterceptor jwtInterceptor;

/*** 注册拦截器* 声明拦截器对象 + 要拦截的请求** @param registry*/

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(jwtInterceptor)

.addPathPatterns("/**") //拦截所有请求.excludePathPatterns("/**/login");//登录请求不拦截}

}

完善过滤器:这里不是拦截请求,而是根据请求头来解析角色,如果是admin角色,就在request请求中放入claims_admin为key的token值,如果是user,就放入claims_user,这样在controller方法中,如果某个方法需要admin角色,只要获取requeset中的claims_admin进行判断即可

@Component

public class JwtInterceptor implements HandlerInterceptor {

@Autowired

private JwtUtil jwtUtil;

public boolean preHandle(HttpServletRequest request,

HttpServletResponse response, Object handler) throws Exception {

System.out.println("经过了拦截器");

String authorization = request.getHeader("Authorization");

//如果有authorization头信息,就解析,如果没有就放行if(authorization!=null && !"".equals(authorization)){

//约定正确if(authorization.startsWith("Bearer ")){

//获取令牌String token = authorization.substring(7);

//对令牌进行验证try {

Claims claims = jwtUtil.parseJWT(token);

if(claims==null){

String roles = (String) claims.get("roles");

//如果是admin角色,往request域中放入令牌if (roles!=null && roles.contains("admin")) {

request.setAttribute("claims_admin",token);

}

//如果是user角色,往request域中放入令牌if (roles!=null && roles.contains("user")) {

request.setAttribute("claims_user",token);

}

}

} catch (Exception e) {

throw new RuntimeException("令牌有误");

}

}

}

return true;

}

}

改造controller:该方法需要admin权限,只要获取requeset中的claims_admin进行判断即可,如果有值就删除,如果没有值就提示权限不足

/*** 删除** 必须有管理员权限才可以删除用户** 前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格 +token** @param id*/

@RequestMapping(value="/{id}",method= RequestMethod.DELETE)

public Result delete(@PathVariable String id, HttpServletRequest request){

String token = (String) request.getAttribute("claims_admin");

if(token==null||"".equals(token)){

throw new RuntimeException("权限不足");

}

userService.deleteById(id);

return new Result(true,StatusCode.OK,"删除成功");

}

我自己的改造:将以上权限判断代码以注解的形式完成

(1)自定义注解

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface AuthName {

/*** 权限:* admin - 管理员* user - 用户** 支持多个角色,以逗号分隔*/

String value() default "";

}

(2)改造代码

/*** @author: zhouR* @date: Create in 2020/3/15 - 17:05* @function: 用户拦截器*/

@Component

public class JwtInterceptor implements HandlerInterceptor {

@Autowired

private JwtUtil jwtUtil;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

System.out.println("经过了拦截器");

//静态资源放行if (!(handler instanceof HandlerMethod)) return true;

//将请求的方法强转成子类,这个子类可以通过反射得到方法上面的注解HandlerMethod hm = (HandlerMethod) handler;

// 判断被拦截的请求的访问的方法的注解(是否时需要拦截的)AuthName authNameAnnotation = hm.getMethodAnnotation(AuthName.class);

//如果没有标注注解,就不用拦截,直接放行if (authNameAnnotation == null) return true;

//获取注解上的值String authName = authNameAnnotation.value();

//支持多个角色,以逗号分隔String[] authNames = authName.split(",");

String authorization = request.getHeader("Authorization");

//如果有authorization头信息,就解析,如果没有就放行if(authorization!=null && !"".equals(authorization)){

//约定正确if(authorization.startsWith("Bearer ")){

//获取令牌String token = authorization.substring(7);

//对令牌进行验证try {

Claims claims = jwtUtil.parseJWT(token);

if(claims!=null){

String roles = (String) claims.get("roles");

if(roles!=null && !"".equals(roles)){

//遍历注解上的角色,挨个比对当前用户的角色for (String name : authNames) {

//如果用户的角色不包含注解上的角色,抛出异常if(!roles.contains(name)){

throw new RuntimeException("权限不足");

}

}

request.setAttribute("token",token);

}

}

} catch (Exception e) {

throw new RuntimeException("权限不足");

}

}

}

return true;

}

}

(3)使用时,只需要在需要权限的方法上,加上注解,写上角色名称即可

@AuthName("admin"),如果需要多个注解,以逗号分隔@AuthName("admin,user")

@AuthName("admin")

@RequestMapping(value="/{id}",method= RequestMethod.DELETE)

public Result delete(@PathVariable String id){

/*String token = (String) request.getAttribute("claims_admin");if(token==null||"".equals(token)){throw new RuntimeException("权限不足");}*/

userService.deleteById(id);

return new Result(true,StatusCode.OK,"删除成功");

}

连每个方法里面的四行判断代码都不需要了,上面代码已经注掉

注意:这个过滤器写在common工程中,给其他所有工程共用,但是在springboot中,启动类只能扫描自己所在的包下的注解,所以需要在启动类上添加指定扫描的包

比如当前项目启动类在com.tensquare.user包下,而过滤器在common包中的com.tensquare包下,有两种解决办法

1、将com.tensquare.user包下的启动类,拖拽到上一级目录com.tensquare.,就可以使用了

2、启动类上添加组件扫描器@ComponentScan("com.tensquare")

@SpringBootApplication

@ComponentScan("com.tensquare")

public class UserApplication {

public static void main(String[] args) {

SpringApplication.run(UserApplication.class, args);

}

@Bean

public IdWorker idWorkker(){

return new IdWorker(1, 1);

}

//spring-security 密码加密@Bean

public BCryptPasswordEncoder encoder(){

return new BCryptPasswordEncoder();

}

//jwt生成token@Bean

public JwtUtil jwtUtil(){

return new JwtUtil();

}

}

------------------------------------------------------------------

上面的自定义注解方法有点问题,无法将所有问题都捕获到,并且无法返回指定响应,修改如下:

package com.tensquare;

import entity.Result;

import entity.StatusCode;

import io.jsonwebtoken.Claims;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import com.alibaba.fastjson.JSON;

import util.JwtUtil;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

/*** @author: zhouR* @date: Create in 2020/3/15 - 17:05* @function: 用户拦截器*/

@Component

public class JwtInterceptor implements HandlerInterceptor {

@Autowired

private JwtUtil jwtUtil;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

System.out.println("经过了拦截器");

//静态资源放行if (!(handler instanceof HandlerMethod)) return true;

//将请求的方法强转成子类,这个子类可以通过反射得到方法上面的注解HandlerMethod hm = (HandlerMethod) handler;

// 判断被拦截的请求的访问的方法的注解(是否时需要拦截的)AuthName authNameAnnotation = hm.getMethodAnnotation(AuthName.class);

//如果没有标注注解,就不用拦截,直接放行if (authNameAnnotation == null) return true;

//获取注解上的值String authName = authNameAnnotation.value();

//支持多个角色,以逗号分隔String[] authNames = authName.split(",");

//获取请求头中的令牌String authorization = request.getHeader("Authorization");

//需要认证的方法里,却没有Authorization请求头if (authorization == null || "".equals(authorization)) {

write(response, StatusCode.ACCESSERROR, "请求头有误,请重新登录");

return false;

}

//authorization头信息中的约定不正确if (!authorization.startsWith("Bearer ")) {

write(response, StatusCode.ACCESSERROR, "约定有误,请重新登录");

return false;

}

//获取令牌String token = authorization.substring(7);

//对令牌进行验证Claims claims;

try {

claims = jwtUtil.parseJWT(token);

} catch (Exception e) {

write(response, StatusCode.ACCESSERROR, "令牌失效1,登录时间过期,请重新登录");

return false;

}

if (claims == null) {

write(response, StatusCode.ACCESSERROR, "令牌失效2,请重新登录");

return false;

}

String roles = (String) claims.get("roles");

if (roles == null || "".equals(roles)) {

write(response, StatusCode.ACCESSERROR, "角色获取失败,请重新登录");

return false;

}

//遍历注解上的角色,挨个比对当前用户的角色for (String name : authNames) {

//如果用户的角色不包含注解上的角色,拦截返回错误信息if (!roles.contains(name)) {

if (name.equals("admin")) {

write(response, StatusCode.ACCESSERROR, "权限不足");

return false;

} else if (name.equals("user")) {

write(response, StatusCode.ERROR, "请先登录");

return false;

}

}

}

request.setAttribute("token", token);

return true;

}

private void write(HttpServletResponse response, int errCode, String message) throws IOException {

//重置responseresponse.reset();

//设置编码格式response.setCharacterEncoding("UTF-8");

response.setContentType("application/json;charset=UTF-8");

PrintWriter pw = response.getWriter();

Result result = new Result(false, errCode, message);

String jsonString = JSON.toJSONString(result);

pw.write(jsonString);

pw.flush();

pw.close();

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值