SpringBoot 2整合SpringSecurity权限管理(六)实现基于JWT的访问

前言
1. 什么是JWT?

JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

2. JWT的工作流程
  • 用户进入登录页,输入用户名、密码,进行登录。
  • 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成 JWT Token。
  • 服务器将该 token 以 json 形式返回(不一定要json形式,这里说的是一种常见的做法)。
  • 用户得到 token,存在 localStorage、cookie 或其它数据存储形式中。以后用户请求 /protected 中的 API 时,在请求的 header 中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer。
  • 服务器端对此 token 进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
  • 用户取得结果。

来看一下 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

token 分成了三部分,头部(header),荷载(Payload) 和 签名(Signature),每部分用 . 分隔,其中头部和荷载使用了base64编码,分别解码之后得到两个JSON串:

第一部分-头部:

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

alg字段为加密算法,这是告诉我们 HMAC 采用 HS512 算法对 JWT 进行的签名。

第二部分-荷载:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

荷载的字段及含义:

  • iss: 该JWT的签发者
  • sub: 该JWT所面向的用户
  • aud: 接收该JWT的一方
  • exp(expires): 什么时候过期,这里是一个Unix时间戳
  • iat(issued at): 在什么时候签发的

这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, name 和 iat。在我们这个例子中,分别代表着所面向的用户、用户名、创建时间,当然你可以把任意数据声明在这里。

第三部分-签名:

第三部分签名则不能使用base64解码出来,该部分用于验证头部和荷载数据的完整性。

一、pom中引入JWT的依赖

<!-- jwt -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

同时在配置文件properties中加入:

# 密钥
jwt.secret=mySecret
jwt.header=Authorization
# token 过期时间 2个小时
jwt.expiration=7200000

二、新增工具类JwtTokenUtil,定义关于JWT的一些方法

/**
 * @program sweet-dream
 * @description: JWT工具类
 * @author: zhangchao
 * @date: 2020/03/21 00:29
 * @since: 1.0.0
 */
@Slf4j
@Component
public class JwtTokenUtil implements Serializable {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.header}")
    private String header;

    @Value("${jwt.expiration}")
    private long expiration;


    /**
     * 获取token中的信息
     * @param token 生成的token
     * @return 信息
     */
    public String getInfoFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    /**
     * 获取token的生成时间
     * @param token 生成的token
     * @return token的生成时间
     */
    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    /**
     * 获取token的过期时间
     * @param token 生成的token
     * @return token的过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    /**
     * 判断token是否过期
     * @param token 生成的token
     * @return true:过期,false:失效
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(Date.from(Instant.now()));
    }

    public <T> T getClaimFromToken(String token, Function<Claims,T> claimsResolver){
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    private Boolean ignoreTokenExpiration(String token) {
        // here you specify tokens, for that the expiration is ignored
        return false;
    }

    /**
     * 生成令牌
     * @param userDetails
     * @return
     */
    public String makeToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }

    /**
     * 真正进行创建token的方法
     * @param claims
     * @param subject
     * @return
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = Date.from(Instant.now());
        final Date expirationDate = calculateExpirationDate(createdDate);

        return Jwts.builder()
                .setClaims(claims) /* 自定义属性 */
                .setSubject(subject) /* 该JWT所面向的用户 */
                .setIssuedAt(createdDate) /* 设置发放的时间,类型为: Date*/
                .setExpiration(expirationDate) /* 设置过期时间 类型为:Date */
                .signWith(SignatureAlgorithm.HS512, secret) /* jwt签名算法和密钥 */
                .compact(); /* 返回一个URL安全JWT字符串 */
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        final Date createdDate =  Date.from(Instant.now());
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUser user = (SecurityUser) userDetails;
        final Date created = getIssuedAtDateFromToken(token);
       /* final Date expiration = getExpirationDateFromToken(token);
        如果token存在,且token创建日期 > 最后修改密码的日期 则代表token有效*/
        return (!isTokenExpired(token)
                /*&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())*/
        );
    }

    /**
     * 生成过期时间
     * @param createdDate 当前时间
     * @return 返回到期时间
     */
    private Date calculateExpirationDate(Date createdDate) {
        return Date.from(Instant.ofEpochMilli(createdDate.toInstant().toEpochMilli()+expiration));
    }
}

三、修改以前定义的登录成功处理器LoginSuccessHandler,在登录成功后生成和返回一个token给客户端

 /**
 * @author ZHANGCHAO
 * @date 2020/3/13 9:35
 * @since 1.0.0
 */
@Slf4j
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        log.info("登录成功!");
        /* 默认:会帮我们跳转到上一次请求的页面上 */
        //super.onAuthenticationSuccess(request, response, authentication);

        //生成token
        String token = jwtTokenUtil.makeToken((UserDetails) authentication.getPrincipal());
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter writer = response.getWriter();
        writer.write("{\"status\":\"ok\",\"msg\":\"登录成功\",\"token\":\""+token+"\"}");
        writer.flush();
        writer.close();
    }
}

四、自定义一个JWT的过滤器

/**
 * @program sweet-dream
 * @description: JWT过滤器
 * @author: zhangchao
 * @date: 2020/03/21 00:48
 * @since: 1.0.0
 */
@Slf4j
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    @Value("${jwt.header}")
    private String header;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        final String header = request.getHeader(this.header);

        String authToken = null;
        String username = null;

        if (null != header && header.startsWith("Bearer")){
            authToken = header.replace("Bearer ","");
            log.info("获取token: {}",authToken);
            username = jwtTokenUtil.getInfoFromToken(authToken);
        }

        if (null != username && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.customUserDetailsService.loadUserByUsername(username);
            if (jwtTokenUtil.validateToken(authToken,userDetails)){
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); /* 增加额外数据 */
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request,response);
    }
}

五、将自定义JWT过滤器加入Security配置文件SecurityConfig的过滤器链中

 // 禁用CSRF防护
http.csrf().disable();   http.addFilterBefore(jwtAuthorizationTokenFilter,UsernamePasswordAuthenticationFilter.class);

六、启动项目测试

image-20200321011931037

输入用户名密码和验证码,登录成功后可以看到已经返回token了,以后每次请求都要在header里带上token。比如访问/test接口:

SpringBoot 2整合SpringSecurity权限管理(六)实现基于JWT的访问_第1张图片

以上!

你可能感兴趣的:(SpringBoot 2整合SpringSecurity权限管理(六)实现基于JWT的访问)