Spring Security总结(三)

JWT Token验证

一、引入依赖和配置

在pom.xml中引入依赖

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

在application.properties中加入token配置

#token
token.expirationSeconds=3600
token.validTime=7

二、Spring Security引入JWT

1. 生成jwt文件

命令行输入

keytool -genkey -alias jwt -keyalg RSA -keysize 1024 -keystore jwt.jks -validity 365

在上面的命令中,-alias选项为别名,-keypass和-storepass为密码选项,-validity为配置jks文件的过期时间(单位:天)。
生成后放入项目中,我是放到了resources中
image.png

2. JWT工具类

JwtTokenUtil.java

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.Map;

/**
 * @Auther: kiritoghy
 * @Date: 19-7-23 下午7:09
 */
public class JwtTokenUtil {

    @Autowired
    private static RedisUtil redisUtil;

    // 寻找证书文件
    private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.jks"); // 寻找证书文件
    private static PrivateKey privateKey = null;
    private static PublicKey publicKey = null;

    static { // 将证书文件里边的私钥公钥拿出来
        try {
            KeyStore keyStore = KeyStore.getInstance("JKS"); // java key store 固定常量
            keyStore.load(inputStream, "123456".toCharArray());
            privateKey = (PrivateKey) keyStore.getKey("jwt", "123456".toCharArray()); // jwt 为 命令生成整数文件时的别名
            publicKey = keyStore.getCertificate("jwt").getPublicKey();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成token
     * @param subject (主体信息)
     * @param expirationSeconds 过期时间(秒)
     * @param claims 自定义身份信息
     * @return
     */
    public static String generateToken(String subject, int expirationSeconds, Map<String,Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
//                .signWith(SignatureAlgorithm.HS512, salt) // 不使用公钥私钥
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * @author: kiritoghy
     * @date: 2018-10-19 09:10
     * @deprecation: 解析token,获得subject中的信息
     */
    public static String parseToken(String token) {
        String subject = null;
        try {
            subject = getTokenBody(token).getSubject();
        } catch (ExpiredJwtException e) {
            subject = e.getClaims().getSubject();
        }
        return subject;
    }

    //获取token自定义属性
    public static Map<String,Object> getClaims(String token){
        Map<String,Object> claims = null;
        try {
            claims = getTokenBody(token);
        }catch (ExpiredJwtException e) {
            claims = e.getClaims();
        }

        return claims;
    }

    // 是否已过期
    public static boolean isExpiration(String expirationTime){
        /*return getTokenBody(token).getExpiration().before(new Date());*/

        //通过redis中的失效时间进行判断
        String currentTime = DateUtil.getTime();
        if(DateUtil.compareDate(currentTime,expirationTime)){
            //当前时间比过期时间小,失效
            return true;
        }else{
            return false;
        }
    }

    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(publicKey)
                .parseClaimsJws(token)
                .getBody();
    }
}

3. JWT Filters

JwtAuthenticationTokenFilter.java

这个是jwt Filter,用来验证用户携带的token,能够实现登录验证

import com.alibaba.fastjson.JSON;
import com.uestc.labelproject.service.impl.SelfUserServiceImpl;
import com.uestc.labelproject.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @Auther: kiritoghy
 * @Desc:验证token,判断是否登录
 * @Date: 19-7-23 下午8:40
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Value("${token.expirationSeconds}")
    private int expirationSeconds;

    @Value("${token.validTime}")
    private int validTime;

    @Autowired
    SelfUserServiceImpl selfUserService;

    @Autowired
    RedisUtil redisUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = httpServletRequest.getHeader("Authorization");
        //获取请求的ip地址
        String currentIp = AccessAddressUtil.getIpAddress(httpServletRequest);

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String authToken = authHeader.substring("Bearer ".length());
            String username = JwtTokenUtil.parseToken(authToken);
            String ip = (String)JwtTokenUtil.getClaims(authToken).get("ip");

            //进入黑名单验证
            if (redisUtil.isBlackList(authToken)) {
                log.info("用户:{}的token:{}在黑名单之中,拒绝访问",username,authToken);
                httpServletResponse.setContentType("application/json;charset=utf-8");
                httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
                return;
            }

            //判断token是否过期
            /*
             * 过期的话,从redis中读取有效时间(比如七天登录有效),再refreshToken(根据以后业务加入,现在直接refresh)
             * 同时,已过期的token加入黑名单
             */
            if (redisUtil.hasKey(authToken)) {//判断redis是否有保存
                String expirationTime = redisUtil.hget(authToken,"expirationTime").toString();
                if (JwtTokenUtil.isExpiration(expirationTime)) {
                    //获得redis中用户的token刷新时效
                    String tokenValidTime = (String) redisUtil.getTokenValidTimeByToken(authToken);
                    String currentTime = DateUtil.getTime();
                    //这个token已作废,加入黑名单
                    log.info("{}已作废,加入黑名单",authToken);
                    redisUtil.hset("blacklist", authToken, DateUtil.getTime());

                    if (DateUtil.compareDate(currentTime, tokenValidTime)) {
                        //超过有效期,不予刷新
                        log.info("{}已超过有效期,不予刷新",authToken);
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
                        return;
                    } else {//仍在刷新时间内,则刷新token,放入请求头中
                        String usernameByToken = (String) redisUtil.getUsernameByToken(authToken);
                        username = usernameByToken;//更新username

                        ip = (String) redisUtil.getIPByToken(authToken);//更新ip

                        //获取请求的ip地址
                        Map<String, Object> map = new HashMap<>();
                        map.put("ip", ip);
                        String jwtToken = JwtTokenUtil.generateToken(usernameByToken, expirationSeconds, map);


                        //更新redis
                        Integer expire = validTime * 24 * 60 * 60 * 1000;//刷新时间
                        redisUtil.setTokenRefresh(jwtToken,usernameByToken,ip);
                        //删除旧的token保存的redis
                        redisUtil.deleteKey(authToken);
                        //新的token保存到redis中
                        redisUtil.setTokenRefresh(jwtToken,username,ip);

                        log.info("token仍在有效期,redis已删除旧token:{},\n新token:{}已更新redis",authToken,jwtToken);
                        authToken = jwtToken;//更新token,为了后面
                        httpServletResponse.setHeader("Authorization", "Bearer " + jwtToken);
                    }
                }

            }

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                /*
                 * 加入对ip的验证
                 * 如果ip不正确,进入黑名单验证
                 */
                if (!StringUtil.equals(ip, currentIp)) {//地址不正确
                    log.info("用户:{}的ip地址变动,进入黑名单校验",username);
                    //进入黑名单验证
                    if (redisUtil.isBlackList(authToken)) {
                        log.info("用户:{}的token:{}在黑名单之中,拒绝访问",username,authToken);
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        httpServletResponse.getWriter().write(JSON.toJSONString(ResultGenerator.genFailResult("Token失效,请重新登录")));
                        return;
                    }
                    //黑名单没有则继续,如果黑名单存在就退出后面
                }


                UserDetails userDetails = selfUserService.loadUserByUsername(username);
                if (userDetails != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));

                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

参考资料

https://www.cnblogs.com/stulzq/p/9678501.html
这篇博客比较详细的描述了关于token的管理,包括token的作用,token带来的风险,例如在token有效的时间内,可能会被恶意利用。因此为了尽量避免这种情况,对如何避免恶意获取token和被恶意获取后怎么控制失效也进行了描述,其中提到了黑名单,因此我又在原来基础上,添加了redis,用来刷新token和作为黑名单。
当然应该还有其他更好的解决办法,但这作为我的第一个项目,暂时就做到这里了。