【SpringBoot】集成JWT实现用户认证

初识JWT

1.什么是JWT

JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,可以在客户端与服务器之间作为JSON对象安全地传输信息。

2.JWT使用场景

  • 身份验证: 用户在登录以后,后续的每个请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源等。Session同样也可以实现这个功能,但是在使用Session的同时也会相应的增加服务器的压力;而JWT的开销则相对较小,因为其将存储的压力分布到各个客户端中,从而减轻了服务器的压力,并且能够在不同域的系统当中轻松的使用。单点登录(SSO)就广泛使用了JWT的功能。
  • 信息交换: JWT能够在客户端与服务器之间安全地传输信息,因为其可以签名,通过签名可以验证传输信息是否被修改。

3.JWT组成

JWT就是一个字符串,经过加密处理与校验处理的字符串,由 . 分割的三个部分组成,分别是头(Header)、有效荷载(Playload)、签名(Signature),因此JWT的格式通常也是这样: header.playload.signature(header由JWT的表头信息经过加密后得到;playload由JWT用到的身份验证信息JSON数据加密得到;signature是由header和playload加密得到,这一部分作为校验部分)。

  • Header
    通常是由两部分组成的:一是令牌的类型,即JWT;二是哈希算法,比如SHA256

例如:

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

然后这个JSON通过Base64加密形成JWT的第一个部分即header

  • Playload
    JWT的第二个部分是有效荷载,其中包含了声明(Claim)。JWT提供了一组预定义的声明,这些声明都是可选的,并不是强制性的。当然你也可以自定义声明传输所需信息,比如系统用户ID。出于安全考虑,一般不会将用户的敏感信息存放在声明当中。
声明属性 说明
iss 发行人,JWT由谁签发
iat JWT创建时间,unix时间戳格式
exp JWT过期时间,unix时间戳格式
sub JWT所面向的用户
aud 接收方,接收JWT的一方
nbf 当前时间在nbf之前,JWT不能被接收处理
jti JWT唯一ID

例如:

{
	"iss": "Hilox",
	"sub": "HiloxApiUser",
	"iat": "1542337107",
	"exp": "1542340707",
	"userId": "5"
}

将上述声明(Claim)通过Base64加密后得到payload

  • Signature
    将表头经过Base64加密得到的headerClaim经过Base64加密得到的playload进行组合,形成一个新字符串header.playload,对新形成的字符串使用标头当中指定的算法(例如:上述Header例子中使用HS256算法)和自定义的密钥(例如:Hilox)进行加密得到signature

最后,将字符串组合 header.playload.signature就是生成的token了。


【SpringBoot】集成JWT实现用户认证_第1张图片
图1 JWT生成流程图

JWT应用

1.JWT如何使用

博主为移动端app搭建服务器,所采用的方式是将token放到http请求的请求头部当中,通常使用的是Authorization属性字段。
移动端app使用cookie不太方便,所以暂不做考虑。

2.应用流程


【SpringBoot】集成JWT实现用户认证_第2张图片
图2 初次登录生成JWT流程图


【SpringBoot】集成JWT实现用户认证_第3张图片
图3 用户访问资源流程图

JWT应用代码实现

下面通过代码来实现用户认证的功能,博主这里主要采用Spring Boot与JWT整合的方式实现。
关于Spring Boot项目如何搭建与使用本章不做详细介绍。
代码当中针对异常自行做处理,我这里偷点懒直接用日志在控制台打印。

1.添加JWT依赖


	io.jsonwebtoken
	jjwt
	0.9.1

2.添加JWT相关配置

Base64在线加密

jwt:
 # 发行者
 name: Hilox
 # 密钥, 经过Base64加密, 可自行替换
 base64Secret: SGlsb3g=
 #jwt中过期时间设置(分)
 jwtExpires: 120

3.JWT配置实体类

/**
 * jwt 相关参数
 * Created by Hilox on 2018/11/16 0016.
 */
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtParam {

    /**
     * 发行者名
     */
    private String name;

    /**
     * base64加密密钥
     */
    private String base64Secret;

    /**
     * jwt中过期时间设置(分)
     */
    private int jwtExpires;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBase64Secret() {
        return base64Secret;
    }

    public void setBase64Secret(String base64Secret) {
        this.base64Secret = base64Secret;
    }

    public int getJwtExpires() {
        return jwtExpires;
    }

    public void setJwtExpires(int jwtExpires) {
        this.jwtExpires = jwtExpires;
    }
}

4.配置JWT拦截器

/**
 * jwt 拦截器
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtParam jwtParam;

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

        // 忽略带JwtIgnore注解的请求, 不做后续token认证校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        final String authHeader = request.getHeader(JwtConstant.AUTH_HEADER_KEY);

        if (StringUtils.isEmpty(authHeader)) {
            // TODO 这里自行抛出异常
            log.info("===== 用户未登录, 请先登录 =====");
            return false;
        }

        // 校验头格式校验
        if (!JwtUtils.validate(authHeader)) {
            // TODO 这里自行抛出异常
            log.info("===== token格式异常 =====");
            return false;
        }

        // token解析
        final String authToken = JwtUtils.getRawToken(authHeader);
        Claims claims = JwtUtils.parseToken(authToken, jwtParam.getBase64Secret());
        if (claims == null) {
            log.info("===== token解析异常 =====");
            return false;
        }

        // 传递所需信息
         request.setAttribute("CLAIMS", claims);
        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 {

    }
}

5.配置MVC拦截器

/**
 * mvc 配置
 * Created by Hilox on 2018/11/15 0015.
 */
@Configuration
public class MyWebConfigurer extends WebMvcConfigurerAdapter {

    // 这里这么做是为了提前加载, 防止过滤器中@AutoWired注入为空
    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }

    // 自定义过滤规则
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**");
    }
}

6.自定义忽略token验证注解

/**
 * JWT请求忽略注解
 * Created by Hilox on 2018/11/20 0020.
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtIgnore {
}

7.JWT工具类

/**
 * JWT工具类
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
public class JwtUtils {

    private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";

    // 构造私有
    private JwtUtils() {}

    /**
     * 获取原始token信息
     * @param authorizationHeader 授权头部信息
     * @return
     */
    public static String getRawToken(String authorizationHeader) {
        return authorizationHeader.substring(AUTHORIZATION_HEADER_PREFIX.length());
    }

    /**
     * 获取授权头部信息
     * @param rawToken token信息
     * @return
     */
    public static String getAuthorizationHeader(String rawToken) {
        return AUTHORIZATION_HEADER_PREFIX + rawToken;
    }

    /**
     * 校验授权头部信息格式合法性
     * @param authorizationHeader 授权头部信息
     * @return
     */
    public static boolean validate(String authorizationHeader) {
        return StringUtils.hasText(authorizationHeader) 
        	&& authorizationHeader.startsWith(AUTHORIZATION_HEADER_PREFIX);
    }

    /**
     * 生成token, 只在用户登录成功以后调用
     * @param userId 用户id
     * @param jwtParam JWT加密所需信息
     * @return
     */
    public static String createToken(String userId, JwtParam jwtParam) {
        return createToken(userId, null, jwtParam);
    }

    /**
     * 生成token, 只在用户登录成功以后调用
     * @param userId 用户id
     * @param claim 声明
     * @param jwtParam JWT加密所需信息
     * @return
     */
    public static String createToken(String userId, Map<String, Object> claim, JwtParam jwtParam) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            // 生成签名密钥
            byte[] apiKeySecretBytes = 
            			DatatypeConverter.parseBase64Binary(jwtParam.getBase64Secret());
            SecretKeySpec signingKey = 
            			new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            // 添加构成JWT的参数
            JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT")
                    .claim(JwtConstant.USER_ID_KEY, userId)
                    .addClaims(claim)
                    .setIssuer(jwtParam.getName())
                    .setIssuedAt(now)
                    .signWith(signatureAlgorithm, signingKey);

            // 添加token过期时间
            long TTLMillis = jwtParam.getJwtExpires() * 60 * 1000;
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                jwtBuilder.setExpiration(exp).setNotBefore(now);
            }

            return jwtBuilder.compact();
        } catch (Exception e) {
            // TODO 这里自行抛出异常
            log.error("签名失败", e);
            return null;
        }
    }

    /**
     * 解析token
     * @param authToken 授权头部信息
     * @param base64Secret base64加密密钥
     * @return
     */
    public static Claims parseToken(String authToken, String base64Secret) {
        try{
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Secret))
                    .parseClaimsJws(authToken).getBody();
            return claims;
        } catch (SignatureException se) {
            // TODO 这里自行抛出异常
            log.error("===== 密钥不匹配 =====", se);
        } catch (ExpiredJwtException ejw) {
            // TODO 这里自行抛出异常
            log.error("===== token过期 =====", ejw);
        } catch (Exception e){
            // TODO 这里自行抛出异常
            log.error("===== token解析异常 =====", e);
        }
        return null;
    }
}

8.编写登录验证Controller

/**
 * 登录验证Controller
 * Created by Hilox on 2018/11/16 0016.
 */
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private JwtParam jwtParam;

    // 登录
    @PostMapping("/login")
    @JwtIgnore // 加此注解, 请求不做token验证
    public String login() {
        // 1.用户密码验证我这里忽略, 假设用户验证成功, 取得用户id为5
        Integer userId = 5;
        // 2.验证通过生成token
        String token = JwtUtils.createToken(userId + "", jwtParam);
        if (token == null) {
            log.error("===== 用户签名失败 =====");
            return null;
        }
        log.info("===== 用户{}生成签名{} =====", userId, token);
        return JwtUtils.getAuthorizationHeader(token);
    }

    // 验证
    @PostMapping("/hilox")
    public String hilox() {
        return "Hello World!";
    }
}

源码传送门

【源码地址】:springboot-jwt

JWT代码测试效果

启动以上项目,博主这里使用工具Postman来模拟http请求。

1.未登录情况请求测试接口/hilox


【SpringBoot】集成JWT实现用户认证_第4张图片

图4 未登录情况请求测试接口效果图

2.请求登录接口/login


【SpringBoot】集成JWT实现用户认证_第5张图片

图5 请求登录接口效果图

3.登录情况请求测试接口/hilox

这里我们需要将请求登录接口时返回的token放入请求头的Authorization当中。


【SpringBoot】集成JWT实现用户认证_第6张图片
图6 登录情况请求测试接口效果图

你可能感兴趣的:(SpringBoot)