亲宝软件园·资讯

展开

Springboot Jwt认证 利用Springboot实现Jwt认证的代码实例

一个JavaBean 人气:0

JSON Web Token是目前最流行的跨域认证解决方案,,适合前后端分离项目通过Restful API进行数据交互时进行身份认证

image-20200221234116337

关于Shiro整合JWT,可以看这里:Springboot实现Shiro+JWT认证

概述

由于概念性内容网上多的是,所以就不详细介绍了

具体可以看这里:阮一峰大佬的博客

我总结几个重点:

JWT,全称Json Web Token,是一种令牌认证的方式

长相:

image-20200221230910489

安全性:由于载荷里的内容都是用BASE64处理的,所以是没有保密性的(因为BASE64是对称的),但是由于签名认证的原因,其他人很难伪造数据。不过这也意味着,你不能把敏感信息比如密码放入载荷中,毕竟这种可以被别人直接看到的,但是像用户id这种就无所谓了

工作流程

登录阶段

用户首次登录,通过账号密码比对,判定是否登录成功,如果登录成功的话,就生成一个jwt字符串,然后放入一些附带信息,返回给客户端。

image-20200221234215011

这个jwt字符串里包含了有用户的相关信息,比如这个用户是谁,他的id是多少,这个令牌的有效时间是多久等等。下次用户登录的时候,必须把这个令牌也一起带上。

认证阶段

这里需要和前端统一约定好,在发起请求的时候,会把上次的token放在请求头里的某个位置一起发送过来,后端接受到请求之后,会解析jwt,验证jwt是否合法,有没有被伪造,是否过期,到这里,验证过程就完成了。

image-20200221234900958

不过服务器同样可以从验证后的jwt里获取用户的相关信息,从而减少对数据库的查询。

比如我们有这样一个业务:“通过用户电话号码查询用户余额”

如果我们在jwt的载荷里事先就放有电话号码这个属性,那么我们就可以避免先去数据库根据用户id查询用户电话号码,而直接拿到电话号码,然后执行接下里的业务逻辑。

关于有效期

由于jwt是直接给用户的,只要能验证成功的jwt都可以被视作登录成功,所以,如果不给jwt设置一个过期时间的话,用户只要存着这个jwt,就相当于永远登录了,而这是不安全的,因为如果这个令牌泄露了,那么服务器是没有任何办法阻止该令牌的持有者访问的(因为拿到这个令牌就等于随便冒充你身份访问了),所以往往jwt都会有一个有效期,通常存在于载荷部分,下面是一段生成jwt的java代码:

 return JWT.create().withAudience(userId)
  .withIssuedAt(new Date()) <---- 发行时间
  .withExpiresAt(expiresDate) <---- 有效期
  .withClaim("sessionId", sessionId)
  .withClaim("userName", userName)
  .withClaim("realName", realName)
  .sign(Algorithm.HMAC256(userId+"HelloLehr"));

在实际的开发中,令牌的有效期往往是越短越安全,因为令牌会频繁变化,即使有某个令牌被别人盗用,也会很快失效。但是有效期短也会导致用户体验不好(总是需要重新登录),所以这时候就会出现另外一种令牌—refresh token刷新令牌。刷新令牌的有效期会很长,只要刷新令牌没有过期,就可以再申请另外一个jwt而无需登录(且这个过程是在用户访问某个接口时自动完成的,用户不会感觉到令牌替换),对于刷新令牌的具体实现这里就不详细讲啦(其实因为我也没深入研究过XD…)

对比Session

在传统的session会话机制中,服务器识别用户是通过用户首次访问服务器的时候,给用户一个sessionId,然后把用户对应的会话记录放在服务器这里,以后每次通过sessionId来找到对应的会话记录。这样虽然所有的数据都存在服务器上是安全的,但是对于分布式的应用来说,就需要考虑session共享的问题了,不然同一个用户的sessionId的请求被自动分配到另外一个服务器上就等于失效了

而Jwt不但可以用于登录认证,也把相应的数据返回给了用户(就是载荷里的内容),通过签名来保证数据的真实性,该应用的各个服务器上都有统一的验证方法,只要能通过验证,就说明你的令牌是可信的,我就可以从你的令牌上获取你的信息,知道你是谁了,从而减轻了服务器的压力,而且也对分布式应用更为友好。(毕竟就不用担心服务器session的分布式存储问题了)

整合Springboot

导入java-jwt包

导入java-jwt包:

这个包里实现了一系列jwt操作的api(包括上面讲到的怎么校验,怎么生成jwt等等)

如果你是Maven玩家:

pom.xml里写入

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.8.3</version>
</dependency>

如果你是Gradle玩家:

build.gradle里写入

compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'

如果你是其他玩家:

maven中央仓库地址点这里

工具类的编写

代码如下:

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;

/**
 * @author Lehr
 * @create: 2020-02-04
 */
public class JwtUtils {

 /**
 签发对象:这个用户的id
 签发时间:现在
 有效时间:30分钟
 载荷内容:暂时设计为:这个人的名字,这个人的昵称
 加密密钥:这个人的id加上一串字符串
 */
 public static String createToken(String userId,String realName, String userName) {

 Calendar nowTime = Calendar.getInstance();
 nowTime.add(Calendar.MINUTE,30);
 Date expiresDate = nowTime.getTime();

 return JWT.create().withAudience(userId) //签发对象
  .withIssuedAt(new Date()) //发行时间
  .withExpiresAt(expiresDate) //有效时间
  .withClaim("userName", userName) //载荷,随便写几个都可以
  .withClaim("realName", realName)
  .sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密
 }

 /**
 * 检验合法性,其中secret参数就应该传入的是用户的id
 * @param token
 * @throws TokenUnavailable
 */
 public static void verifyToken(String token, String secret) throws TokenUnavailable {
 DecodedJWT jwt = null;
 try {
  JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"HelloLehr")).build();
  jwt = verifier.verify(token);
 } catch (Exception e) {
  //效验失败
  //这里抛出的异常是我自定义的一个异常,你也可以写成别的
  throw new TokenUnavailable();
 }
 }

 /**
 * 获取签发对象
 */
 public static String getAudience(String token) throws TokenUnavailable {
 String audience = null;
 try {
  audience = JWT.decode(token).getAudience().get(0);
 } catch (JWTDecodeException j) {
  //这里是token解析失败
  throw new TokenUnavailable();
 }
 return audience;
 }


 /**
 * 通过载荷名字获取载荷的值
 */
 public static Claim getClaimByName(String token, String name){
 return JWT.decode(token).getClaim(name);
 }
}

一点小说明:

关于jwt生成时的加密和验证方法:

jwt的验证其实就是验证jwt最后那一部分(签名部分)。这里在指定签名的加密方式的时候,还传入了一个字符串来加密,所以验证的时候不但需要知道加密算法,还需要获得这个字符串才能成功解密,提高了安全性。我这里用的是id来,比较简单,如果你想更安全一点,可以把用户密码作为这个加密字符串,这样就算是这段业务代码泄露了,也不会引发太大的安全问题(毕竟我的id是谁都知道的,这样令牌就可以被伪造,但是如果换成密码,只要数据库没事那就没人知道)

关于获得载荷的方法:

可能有人会觉得奇怪,为什么不需要解密不需要verify就能够获取到载荷里的内容呢?原因是,本来载荷就只是用Base64处理了,就没有加密性,所以能直接获取到它的值,但是至于可不可以相信这个值的真实性,就是要看能不能通过验证了,因为最后的签名部分是和前面头部和载荷的内容有关联的,所以一旦签名验证过了,那就说明前面的载荷是没有被改过的。

注解类的编写

在controller层上的每个方法上,可以使用这些注解,来决定访问这个方法是否需要携带token,由于默认是全部检查,所以对于某些特殊接口需要有免验证注解

免验证注解

@PassToken:跳过验证,通常是入口方法上用这个,比如登录接口

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Lehr
 * @create: 2020-02-03
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
 boolean required() default true;
}

拦截器的编写

 配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author lehr
*/
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer {
 @Override
 public void addInterceptors(InterceptorRegistry registry) {

 //默认拦截所有路径
 registry.addInterceptor(authenticationInterceptor())
  .addPathPatterns("/**");
 }
 @Bean
 public JwtAuthenticationInterceptor authenticationInterceptor() {
 return new JwtAuthenticationInterceptor();
 }
} 

拦截器

import com.auth0.jwt.interfaces.Claim;
import com.imlehr.internship.annotation.PassToken;
import com.imlehr.internship.dto.AccountDTO;
import com.imlehr.internship.exception.NeedToLogin;
import com.imlehr.internship.exception.UserNotExist;
import com.imlehr.internship.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @author Lehr
 * @create: 2020-02-03
 */
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
 @Autowired
 AccountService accountService;

 @Override
 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
 // 从请求头中取出 token 这里需要和前端约定好把jwt放到请求头一个叫token的地方
 String token = httpServletRequest.getHeader("token");
 // 如果不是映射到方法直接通过
 if (!(object instanceof HandlerMethod)) {
  return true;
 }
 HandlerMethod handlerMethod = (HandlerMethod) object;
 Method method = handlerMethod.getMethod();
 //检查是否有passtoken注释,有则跳过认证
 if (method.isAnnotationPresent(PassToken.class)) {
  PassToken passToken = method.getAnnotation(PassToken.class);
  if (passToken.required()) {
  return true;
  }
 }
 //默认全部检查
 else {
  System.out.println("被jwt拦截需要验证");
  // 执行认证
  if (token == null) {
  //这里其实是登录失效,没token了 这个错误也是我自定义的,读者需要自己修改
  throw new NeedToLogin();
  }
  
  // 获取 token 中的 user Name
  String userId = JwtUtils.getAudience(token);

  //找找看是否有这个user 因为我们需要检查用户是否存在,读者可以自行修改逻辑
  AccountDTO user = accountService.getByUserName(userId);

  if (user == null) {
  //这个错误也是我自定义的
  throw new UserNotExist();
  }

  // 验证 token 
  JwtUtils.verifyToken(token, userId)
  
  //获取载荷内容
 	String userName = JwtUtils.getClaimByName(token, "userName").asString();
 	String realName = JwtUtils.getClaimByName(token, "realName").asString();
 	
  //放入attribute以便后面调用
  request.setAttribute("userName", userName);
 	request.setAttribute("realName", realName);
  

  return true;

 }
 return true;
 }

 @Override
 public void postHandle(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse,
    Object o, ModelAndView modelAndView) throws Exception {

 }

 @Override
 public void afterCompletion(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse,
    Object o, Exception e) throws Exception {
 }
}

这段代码的执行逻辑大概是这样的:

接口的编写

这里设计了两个接口:登录和查询名字,来模拟一个迷你业务,其中后者需要登录之后才能使用,大致流程如下:

image-20200222005538848

登录代码

/**
 * 用户登录:获取账号密码并登录,如果不对就报错,对了就返回用户的登录信息
 * 同时生成jwt返回给用户
 *
 * @return
 * @throws LoginFailed 这个LoginFailed也是我自定义的
 */
 @PassToken
 @GetMapping(value = "/login")
 public AccountVO login(String userName, String password) throws LoginFailed{
 
 try{
  service.login(userName,password);
 }
 catch (AuthenticationException e)
 {
  throw new LoginFailed();
 }

 //如果成功了,聚合需要返回的信息
 AccountVO account = accountService.getAccountByUserName(userName);
 
 //给分配一个token 然后返回
 String jwtToken = JwtUtils.createToken(account);

 //我的处理方式是把token放到accountVO里去了
 account.setToken(jwtToken);

 return account;

 }

业务代码

这里列举一个需要登录,用来测试用户名字的接口(其中用户的名字来源于jwt的载荷部分)

 @GetMapping(value = "/username")
 public String checkName(HttpServletRequest req) {
 //之前在拦截器里设置好的名字现在可以取出来直接用了
 String name = (String) req.getAttribute("userName");
 return name;
 }

加载全部内容

相关教程
猜你喜欢
用户评论