JWT小结(认证和授权)

JWT

用于分布式系统的单点登录SSO场景,主要用来做用户身份鉴别或者资源安全性的技术。

传统的session认证:

流程:用户登录成功会保存一个会话,在cookie中维护一个jessionid,之后每次请求会携带到服务器上,如果没有过期并有状态,可以得到想要数据。一个session过期时间是30分钟,可在配置文件更改(server.servlet.session.timeout=30m)

@GetMapping
public R login(String username,String password,HttpSession session) {
   //...登录成功
   session.setAttribute("session",user);    
}

如果用户大概有10W,日活用户3000左右,可以上面session认证可以满足。如果日活用户过多,一个用户会创建一个Session会话对象,一个用户字节是10kb,3000个用户就是20MB,JVM内存是2G,会出现页面卡顿现象。

暴露的问题:

  1. session会保存在服务器上,会给服务器造成压力,对于分布式服务,需要使用Redis,做存储。
  2. CSRF攻击,cookie在客户端,容易被截取,受到其他网站的伪请求攻击。

基于token机制:

流程:每个服务可以生成token,认证后把token传给前端,token储存给客户端进行储存,服务端没有压力。前端每次请求携带token,token通过可以得到想要的数据。

JWT构成

  1. 头部 header

    声明加密的算法,是一个JSON,然后将JSON进行base64加密,得到一个字符串。

  2. 载荷 payload

    存放有效信息(有3部分)

    • 标准中注册的声明

      iss: jwt签发者
      sub: jwt所面向的用户
      aud: 接收jwt的一方
      exp: jwt的过期时间,这个过期时间必须要大于签发时间
      nbf: 定义在什么时间之前,该jwt都是不可用的.
      iat: jwt的签发时间
      jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
      
    • 公共的声明

      公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息
      
    • 私有的声明

      有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息
      
  3. 签证

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

官方网站:https://jwt.io/

如何使用

引入相关包:

<!-- jwt 实现接口安全性 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.7.0</version>
</dependency>

编写JWT实现类

/**
 * jwt - 实现类
 * @author shengren.yan
 * @create 2022-10-30
 */
@Service
public class JwtService {

    private static final Logger log = LoggerFactory.getLogger(JwtService.class);

    // 1 定义加密的盐信息
    private static final String KEY = "goodyan";
    // 2 发行者
    private static final String ISSUSER = "yan";
    // 3 定义token的过期时间 (30天)
    public static final Long TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 30L;

    // 生产token
    public String token(UserVo vo) {
        Date now = new Date();
        // 1 确定加密算法 (有很多种加密算法 - 可在官网中查看 https://jwt.io/libraries )
        Algorithm algorithm = Algorithm.HMAC256(KEY);
        // 2 创建token
        String token = JWT.create()
                .withIssuer(ISSUSER) // 签发者
                .withIssuedAt(now)   // token签发的时间
                .withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME))
                .withClaim("userid", userVo.getUserid())  // 加入要加密的值(可以多个)
                .sign(algorithm);
        return token;
    }

    // 验证token 参数 token 、userid (不要用自增主键 - 使用雪花算法)
    public boolean verifyUserId(String token, String userid) {
        try {
            // 定义算法
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            // 进行校验
            JWTVerifier jwtVerifier = JWT.require(algorithm).withIssuer(ISSUSER)
                .withClaim("userid", userid).build();
            jwtVerifier.verify(token);
          	return true;
        } catch (Exception ex) {
            return false;
        }
    }

}

登录

/**
 * token 授权 登录方式
 * @author shengren.yan
 * @create 2022-10-30
 */
@RestController
public class LoginAuthController {

    @Autowired
    private JwtService jwtService;
    @Autowired(required = false)
    private RedisTemplate redisTemplate;
    @Autowired
    private UserService userService;

    // 登录
    @PostMapping("/login")
    public AuthResponse login(String username, String password) {
        // 1 效验是否为空
        // 2 根据用户查询用户是否存在
        User user = userService.getByUserName(username);
        if (user == null) 
            throw new VException(403, "用户名或密码有误!!!");
        // 3 对密码进行加密加盐进行处理
        password = MD5Util.md5slat(password);
        // 如果用户输入的密码和数据库查询到密码不一致
        if (!password.equalsIgnoreCase(user.getPassword())) 
            throw new ValidationException(403, "用户名或密码有误!!!");

        UserVo userVo = new UserVo();
        userVo.setUserid(user.getId());
        userVo.setUsername(user.getUsername());
        // 生成token信息
        String token = jwtService.token(userVo);
        userVo.setToken(token);
        // 刷新token (把值存入Redis中)
        userVo.setRefreshToken(UUID.randomUUID().toString());
        redisTemplate.opsForValue().set(userVo.getRefreshToken(), userVo, JwtService.TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);

        return AuthResponse.success(userVo, ResponseCode.SUCCESS);
    }


    /**
     * 续签的方法 -- 重新生成token
     * @param refreshToken
     * @return
     */
    @PostMapping("/refresh/{refreshToken}")
    public AuthResponse refresh(@PathVariable("refreshToken")  String refreshToken) {
        // 查看当前用户是不是还在有效期内  (在redis查看)
        UserVo userVo = (UserVo) redisTemplate.opsForValue().get(refreshToken);
        if (userVo == null) 
             throw new VException(403, "用户不存在!!!");
        // 重新生成token
        String jwt = jwtService.token(userVo);
        userVo.setToken(jwt);
        // 删除 redis中的key ,重新生成
        redisTemplate.delete(refreshToken);
        userVo.setRefreshToken(UUID.randomUUID().toString());
        redisTemplate.opsForValue().set(userVo.getRefreshToken(), userVo, JwtService.TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);

        return AuthResponse.success(userVo, ResponseCode.SUCCESS);
    }

    @GetMapping("/logout")
    public AuthResponse logout(String refreshToken) {
//        redisTemplate.delete(refreshToken);
        return AuthResponse.code(409L);
    }

}

流程:当前端输入账号和密码进行登录,后端把token、该用户返回前端,之后每次请求,前端需要把token值携带传给后端,来验证是否有权限访问。

每次携带token,进行获取验证可以放在拦截器进行校验:

/**
 * JWT 自定义过滤器
 * 作用:每条请求都携带 token,通过自定义拦截器来获取 token值
 * @author shengren.yan
 * @create 2022-11-02
 */
public class AuthorizationInterceptor implements HandlerInterceptor {
    
    @Autowired
    private JwtService jwtService;

    // 获取配置文件
    @Value("${spring.profiles.active}")
    private String profiles;

    // token名,存token值
    private static final String AUTH = "token";
    private static final String USER = "usercode";

    /**
     * 校验token
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        // 1. 获取配置文件,如果是开发环境直接(放行)
        if (!StringUtils.isEmpty(profiles) && profiles.equals("dev")) {
            return true;
        }
        // 2. 从 http 请求头获取请求接口
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        String username = getParam(request, AUTH_USERNAME);
        String token = getParam(request, AUTH);
        if (StringUtils.isEmpty(token)) 
            throw new ValidationException(300, "校验失败,请重新登录!!!");
        if (StringUtils.isEmpty(username)) 
            throw new ValidationException(300, "校验失败,请重新登录!!!");

        // 开始对你token和你用户名进行token校验,如果正常直接返回,如果不正常抛出异常
        AuthResponse authResponse = jwtService.verify(token, username);
        // 如果不等于1,说明token和用户名校验失败
        if (authResponse.getCode() != 1L) {
            throw new ValidationException(300, "token 校验失败!");
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }

    // 从表头获取信息
    public static String getParam(HttpServletRequest request, String filedName) {
        return request.getHeader(filedName);
    }

}

案例代码可参考我的gitee上的JWT。

跳转地址:https://gitee.com/yan418

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值