当前位置:   article > 正文

【深入浅出 Spring Security(十三)】使用 JWT 进行前后端分离认证(附源码)_基于jwt前后端token认证

基于jwt前后端token认证

一、JWT 的简单介绍

JWT 全称 Json web Token,在此所讲述的是 JWT 用于身份认证,用服务器端生成的JWT去替代原始的Session认证,以提高安全性。

JWT本质是一个Token令牌,是由三部分组成的字符串,分别是头部(header)、载荷(payload)和签名(signature)。头部一般包含该 JWT 的基本信息,例如所使用的加密算法;载荷一般包含所需要传递的信息,如用户名;签名则是通过对头部、载荷和密钥加密生成的,用于验证 JWT 的真实性和完整性(即拿到前端传过来的Token,通过其头部、载荷和密钥去生成一个签名,然后比对是否与传过来的Token签名部分是否一致)。

二、使用 JWT 进行安全认证

后端结合SpringSecurity实现

  1. 导入相关依赖(jwt相关的和Spring Security依赖)
		<!--SpringSecurity-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!-- JWT -->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.2.0</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 将生成 jwt 和 认证 jwt 的实现以方法的形式封装成一个工具类(jwt的认证即前端传过来的token和后端中的进行比对),封装的工具类如下(其实封装的方式很多,不局限于这种):
/**
 * jwt加密和解密的工具类
 */
public class JWTUtil {

    /**
     * 签发JWT;这里创建的jwt
     * @param id
     * @param subject   可以是JSON数据 尽可能少
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        SecretKey secretKey = generalKey();  // 通过操作加密生成key
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setSubject(subject)   // 主题
                .setIssuer("xc")// 签发者:小柴
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate); // 过期时间
        }
        return builder.compact();
    }

    /**
     * 生成jwt token
     *
     * @param username
     * @return
     */
    public static String createJWT(String username) {
        return createJWT(username, username, 60 * 60 * 1000);
    }

    /**
     * 验证JWT
     * 根据验证时抛出的超时异常、签名异常、其他异常进行一定的操作
     *
     * @param jwtStr
     * @return
     */
    public static CheckResult validateJWT(String jwtStr) {
        CheckResult checkResult = new CheckResult();
        // 如果jwtStr为空的话,设置errcode为jwt不存在
        if(StringUtils.isEmpty(jwtStr)){
            checkResult.setSuccess(false);
            checkResult.setErrCode(JWTConstant.JWT_ERRCODE_NULL);
        }
        Claims claims = null;
        try {
            claims = parseJWT(jwtStr);
            checkResult.setSuccess(true);
            checkResult.setClaims(claims);
        } catch (ExpiredJwtException e) {
            checkResult.setErrCode(JWTConstant.JWT_ERRCODE_EXPIRE);
            checkResult.setSuccess(false);
        } catch (SignatureException e) {
            checkResult.setErrCode(JWTConstant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        } catch (Exception e) {
            checkResult.setErrCode(JWTConstant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        }
        return checkResult;
    }

    /**
     * 生成加密Key
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.decode(JWTConstant.JWT_SECRET);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析JWT字符串
     *
     * @param jwt
     * @return 返回 jwt 解析后的 payload
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

在这个工具类中用到了俩个自定义的类,一个封装是验证 jwt 结果集实体类 CheckResult,它内部封装了三个属性:errorCode:错误编码,success:验证是否成功,claims:jwt 中包含的一些信息;使用工具类验证JWT时返回该对象,具体代码如下:

@Data
@NoArgsConstructor
/**
 * JWT 验证信息
 */
public class CheckResult {

    private int errCode;

    private boolean success;

    private Claims claims;
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

另一个是一个在验证/生成 JWT 时所需用到的常量类 JWTConstant,如:验证失败所对应异常的编码(自定义的),JWT 秘钥等等。具体代码如下:

public class JWTConstant {

    /**
     * token
     */
    public static final int JWT_ERRCODE_NULL = 4000;			//Token不存在
    public static final int JWT_ERRCODE_EXPIRE = 4001;			//Token过期
    public static final int JWT_ERRCODE_FAIL = 4002;			//验证不通过

    /**
     * JWT 秘钥 1
     */
    public static final String JWT_SECRET = "bG92ZS14bXE=";
    /**
     * JWT 秘钥 2
     */
    public static final String JWT_SECERT2 = "8677df7fc3a34e26a61c034d5ec8245d";			//密匙
    public static final long JWT_TTL = 24*60 * 60 * 1000;		//token有效时间
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  1. 由于前后端的话你使用了 JWT 进行认证,所以我们得关闭Spring Security 默认的Session认证,即得把 Session 管理关了,至于为什么不使用默认的进行认证(Session 认证)?原因很多,如:当认证的用户多了,Session占有的内存会不断地增大;Session是不安全的,很容易造成 CSRF 等等…即在配置 SecurityFilterChain 的时候填上如下代码:
        // 关闭session
        // 关闭原因:
        // 1. 前后端进行通信,每个请求都是一个独立的事务,开启session管理可能会使得信息无法共享
        // 2. 采用session管理的话,多个用户进行访问服务器端的内存会占用过高,这是因为session的废除机制是超时机制
        // 3. 采用session管理功能,这也是一个安全漏洞
        // 这里使用jwt(Java web token)令牌的方式进行认证,不需要session了
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 当用户登录成功后,是需要将这token传给前端的,然后让前端发送请求的时候携带这个token,请求报文中有了这个token才允许请求通过,否则返回401,无权限(当然这异常处理可以自定义,这里不说明了,还有这个token一般在请求报文中的请求头中,当然这是下面前端该实现的),那如何将token传递给前端呢?即在登录认证成功后,Spring Security会去调用配置的AuthenticationSuccessHandler 中的 onAuthenticationSuccess 方法对登录成功的一些操作(即登录成功后需要返回给前端的数据就可以在这个方法中进行实现),那有了JWT工具类,这方法就简单实现了,下面是实现的具体代码(当然如何配置这个handler这里就不说了,在专栏里有专门的博客解释了):
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        // 设置响应编码格式
        response.setContentType("json/application;charset=utf-8");
        // 获取用户名
        String username = authentication.getName();
        // 生成 jwt
        String jwt = JWTUtil.createJWT(username);
        ServletOutputStream out = response.getOutputStream();
        // 将 jwt 返回给前端
        out.write(objectMapper.writeValueAsString(BackResult.success(jwt)).getBytes());
        out.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 首先得明白认证成功后的数据是放在 SecurityContextgHolder 中的,内部默认使用的是 ThreadLocal 去存放认证信息(内部用了策略模式,默认采用的策略是用ThreadLocal),当一个请求结束后这个Authentication会移除,原本移除会放在Session里一同返回给前端,但咱现在把Session管理给静止了(这在【深入浅出 Spring Security(四)】登录用户数据的获取,超详细的源码分析 中详细说明了)。咱现在用的是JWT认证方式了,前端拿到这个token后,放在请求头中向后端发送请求时,后端得对这个token进行验证,如果验证成功了咱得从这个token中提取一些数据封装成Authentication放入 SecurityContextHolder 中,将 SecurityContextHolder 中的对应 Authentication 中的 authenticated 属性设置为 true,以表示认证成功,即这个请求认证成功了(但不代表授权成功哈,提一嘴
    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/518165
推荐阅读
相关标签