Springboot +JWT实现登录认证,密码加密及Token校验全过程(附源码)

JWT实现登录认证

简介

  • 通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探

  • 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串

  • 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可

  • 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)

  • 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等

  • 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

在这里插入图片描述
本博客项目源码地址:


环境

本教程使用jdk11,其他环境自行测试
在这里插入图片描述

api测试工具postman{{localhost}}请自行更改为自己的地址,如 localhost:9999
在这里插入图片描述

1. 依赖

  • 1.1 pom导入依赖
       <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
       <!--swagger3 生成接口注释-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!-- 解决jdk11缺失jar包引起的报错-->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <!-- jwt token核心依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- jwt token核心依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.3</version>
        </dependency>

2. token生成及校验

  • 2.1 封装用户

存储用户的基本信息

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author l
 */
@Data
@Accessors(chain = true)
public class JwtUser {

    private boolean valid;
    private String userId;
    private String role;

    public JwtUser() {
        this.valid = false;
    }
}
  • 2.2 编写JWT提供者

主要关注 createToken 和 checkToken 两个方法

  • createToken 生成token
  • checkToken 校验token

createToken

import io.jsonwebtoken.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;


/**
 * date: 2021-01-05 08:48
 * description token管理
 *
 * @author qiDing
 */
@Slf4j
@ApiModel("token提供者")
public class TokenProvider {

    @ApiModelProperty("盐")
    private static final String SALT_KEY = "links";

    @ApiModelProperty("令牌有效期毫秒")
    private static final long TOKEN_VALIDITY = 86400000;

    @ApiModelProperty("权限密钥")
    private static final String AUTHORITIES_KEY = "auth";

    @ApiModelProperty("Base64 密钥")
    private final static String SECRET_KEY =  Base64.getEncoder().encodeToString(SALT_KEY.getBytes(StandardCharsets.UTF_8));


    /**
     * 生成token
     * @param userId 用户id
     * @param clientId 用于区别客户端,如移动端,网页端,此处可根据自己业务自定义
     * @param role 角色权限
     */
    public static String createToken(String userId, String clientId, String role) {
        Date validity = new Date((new Date()).getTime() + TOKEN_VALIDITY);
        return Jwts.builder()
                // 代表这个JWT的主体,即它的所有人
                .setSubject(String.valueOf(userId))
                // 代表这个JWT的签发主体
                .setIssuer("")
                // 是一个时间戳,代表这个JWT的签发时间;
                .setIssuedAt(new Date())
                // 代表这个JWT的接收对象
                .setAudience(clientId)
                .claim("role", role)
                .claim("userId", userId)
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .setExpiration(validity)
                .compact();
    }

    /**
     * 校验token
     */
    public static JwtUser checkToken(String token) {
        if (validateToken(token)) {
            Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
            String audience = claims.getAudience();
            String userId = claims.get("userId", String.class);
            String role = claims.get("role", String.class);
            JwtUser jwtUser = new JwtUser().setUserId(userId).setRole(role).setValid(true);
            log.info("===token有效{},客户端{}", jwtUser, audience);
            return jwtUser;
        }
        log.error("***token无效***");
        return new JwtUser();
    }


    private static boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(authToken);
            return true;
        } catch (Exception e) {
            log.error("无效的token:" + authToken);
        }
        return false;
    }
}

3. 登录

  • 3.1 密码加密

对密码进行md5加密

import org.springframework.util.DigestUtils;

/**
 * 密码加密工具类
 *
 * @author liangQiDing
 */
public class PasswordEncoder {

    /**
     * 密码加密
     * @param rawPassword 登录时传入的密码
     */
    public static String encode(CharSequence rawPassword) {
        return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
    }

    /**
     * 密码对比
     * @param rawPassword 登录时传入的密码
     * @param encodedPassword 数据库保存的加密过的密码
     */
    public static boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
    }
}
  • 3.2 登录接口编写
import com.example.jwt_dome.config.PasswordEncoder;
import com.example.jwt_dome.jwt.AuthStorage;
import com.example.jwt_dome.jwt.JwtUser;
import com.example.jwt_dome.jwt.TokenProvider;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;

/**
 * @author liangQiDing
 */
@RestController
@Api("token测试服务器")
public class TokenController {

    /**
     * 模拟数据库数据 账号 admin  密码 123456
     */
    private final static HashMap<String, String> USER = new HashMap<>() {
        {
            put("admin", "e10adc3949ba59abbe56e057f20f883e");
        }
    };

    @GetMapping("/login")
    @ApiOperation("登陆示例(账号admin,密码123456)")
    public String login(String username, String password) {
        if (PasswordEncoder.matches(password, USER.get(username))) {
            // 模拟一个用户的数据 用户id为1  登录端为网页web  角色是admin
            return TokenProvider.createToken("1", "web", "admin");
        }
        return "error";
    }

    @GetMapping("/token/validate")
    @ApiOperation("token校验")
    public JwtUser tokenValidate(String token) {
        return TokenProvider.checkToken(token);
    }
}
  • 3.3 token获取测试
    在这里插入图片描述

  • 3.4 token校验测试
    在这里插入图片描述

4. 编写拦截器进行token校验

  • 4.1 存储授权信息

用于在我们授权通过后,在请求中获取用户的信息

/**
 * 存储本次请求的授权信息,适用于各种业务场景,包括分布式部署
 *
 * @author lqd
 */
public class AuthStorage {

    @ApiModelProperty("请求头token的下标")
    public static final String TOKEN_KEY = "token";

    /**
     * 模拟session
     */
    private static final HashMap<String, JwtUser> JWT_USER = new HashMap<String, JwtUser>();

    /**
     * 全局获取用户
     */
    public static JwtUser getUser() {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        return JWT_USER.get(request.getHeader(TOKEN_KEY));
    }

    /**
     * 设置用户
     */
    public static void setUser(String token, JwtUser user) {
        JWT_USER.put(token, user);
    }

    /**
     * 清除授权
     */
    public static void clearUser() {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        JWT_USER.remove(request.getHeader(TOKEN_KEY));
    }
}
  • 4.2 配置拦截器

在请求响应前,校验token,校验通过后存储用户信息。

import com.example.jwt_dome.jwt.AuthStorage;
import com.example.jwt_dome.jwt.JwtUser;
import com.example.jwt_dome.jwt.TokenProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 拦截器
 *
 * @author lqd
 */
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader(AuthStorage.TOKEN_KEY);
        if (StringUtils.hasLength(token)) {
            JwtUser jwtUser = TokenProvider.checkToken(token);
            // 是否认证通过
            if (jwtUser.isValid()) {
                // 保存授权信息
                AuthStorage.setUser(token, jwtUser);
                return true;
            }
        }
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().write("请先登录!");
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object      handler, Exception ex) throws Exception {
        // 请求完成清除授权信息
        AuthStorage.clearUser();
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
  • 4.3 配置拦截路径
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置拦截器路径
 *
 * @author lqd
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                // 拦截的路径
                .addPathPatterns("/**")
                // 开放的路径
                .excludePathPatterns("/login/**", "/token/validate");
    }
}
  • 4.4 拦截测试

在controller层添加测试接口

    @GetMapping("/get/Info")
    @ApiOperation("模拟拦截")
    public String getInfo() {
        // 从全局环境中获取用户id
        JwtUser user = AuthStorage.getUser();
        return "用户:"+user.getUserId() + ",请求成功";
    }

普通访问
在这里插入图片描述

请求头添加token后再访问
在这里插入图片描述

5. 源码下载

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注