前言

最近正在搭建一个SpringBoot+Vue的一套后台管理系统的模板,对于用户登录的功能使用了JWT来实现的,自己在学习SpringCloud微服务时使用的就是JWT,通过Cookie来传递token,实现用户的登录状态。

以下就是自己在SpringBoot中集成JWT过程。

1、加入maven依赖

<!-- jwt依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

2、实现配置类

配置类中的公钥私钥可以自己网上找

#jwt配置
config:
  jwt:
    # 加密密钥
    secret: iwqjhda8232bjgh432[cicada-smile]
    # token有效时长
    expire: 3600
    # header 名称
    cookieName: token
    pubKeyPath: D:\googleDownload\RSA签名验签工具windows_V1.4\RSA密钥\rsa.pub # 公钥地址
    priKeyPath: D:\googleDownload\RSA签名验签工具windows_V1.4\RSA密钥\rsa.pri # 私钥地址

3、实现工具类

JwtProperties 工具类

@Data
@Component
public class JwtProperties {

    @Value("${config.jwt.secret}")
    private String secret; // 密钥

    @Value("${config.jwt.pubKeyPath}")
    private String pubKeyPath;// 公钥

    @Value("${config.jwt.priKeyPath}")
    private String priKeyPath;// 私钥

    @Value("${config.jwt.cookieName}")
    private String cookieName;  //cookie的名称

    @Value("${config.jwt.expire}")
    private int expire;// token过期时间

    private PublicKey publicKey; // 公钥

    private PrivateKey privateKey; // 私钥

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    /**
     * @PostContruct:在构造方法执行之后执行该方法
     */
    @PostConstruct
    public void init() {
        try {
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);
            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公钥和私钥
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 获取公钥和私钥
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            logger.error("初始化公钥和私钥失败!", e);
            throw new RuntimeException();
        }
    }

}

RsaUtils工具类,用户实现公钥私钥的解密

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * @author chenmo
 * @date 2018/9/30
 */
public class RsaUtils {

    /**
     * 从文件中读取公钥
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(String fileName) throws Exception {
        byte[] bytes = readFile(fileName);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取私钥
     * @param fileName
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String fileName) throws Exception{
        byte[] bytes = readFile(fileName);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 生成私钥
     *
     * @param bytes
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 读取文件
     *
     * @param fileName
     * @return
     * @throws IOException
     */
    public static byte[] readFile(String fileName) throws IOException {
        return Files.readAllBytes(new File(fileName).toPath());

    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    /**
     * 向目标路径写入文件
     *
     * @param destPath
     * @param bytes
     * @throws IOException
     */
    public static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

CodecUtils 加密工具类,用户加密密码

/**
 * 密码中生成盐的方法
 */
public class CodecUtils {

    /**
     * 将密码加上盐后再经过md5加密
     *
     * @param data
     * @param salt
     * @return
     */
    public static String md5Hex(String data, String salt) {
        if (StringUtils.isBlank(salt)) {
            salt = data.hashCode() + "";
        }
        return DigestUtils.md5Hex(salt + DigestUtils.md5Hex(data));
    }

    public static String shaHex(String data, String salt) {
        if (StringUtils.isBlank(salt)) {
            salt = data.hashCode() + "";
        }
        return DigestUtils.sha512Hex(salt + DigestUtils.sha512Hex(data));
    }

    /**
     * 生成盐
     *
     * @return
     */
    public static String generateSalt() {
        return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
    }
}

JwtUtils工具类,用于解析JWT中的token

import com.modules.application.system.entity.UserInfo;
import com.modules.application.system.paramVo.UserInfoVO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;

import java.security.PrivateKey;
import java.security.PublicKey;

public class JwtUtils {
    /**
     * 私钥加密token
     *
     * @param userInfo      载荷中的数据
     * @param privateKey    私钥
     * @param expireMinutes 过期时间,单位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfoVO userInfo, PrivateKey privateKey, int expireMinutes) throws Exception {
        return Jwts.builder()
                .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 私钥加密token
     *
     * @param userInfo      载荷中的数据
     * @param privateKey    私钥字节数组
     * @param expireMinutes 过期时间,单位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {
        return Jwts.builder()
                .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥字节数组
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
                .parseClaimsJws(token);
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     * @throws Exception
     */
    public static UserInfoVO getInfoFromToken(String token, PublicKey publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfoVO(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     * @throws Exception
     */
    public static UserInfoVO getInfoFromToken(String token, byte[] publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfoVO(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }
}

JWT配置信息

public abstract class JwtConstans {
    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USER_NAME = "username";
}
/**
 * 从jwt解析得到的数据是Object类型,转换为具体类型可能出现空指针,
 * 这个工具类进行了一些转换
 */
public class ObjectUtils {

    public static String toString(Object obj) {
        if (obj == null) {
            return null;
        }
        return obj.toString();
    }

    public static Long toLong(Object obj) {
        if (obj == null) {
            return 0L;
        }
        if (obj instanceof Double || obj instanceof Float) {
            return Long.valueOf(StringUtils.substringBefore(obj.toString(), "."));
        }
        if (obj instanceof Number) {
            return Long.valueOf(obj.toString());
        }
        if (obj instanceof String) {
            return Long.valueOf(obj.toString());
        } else {
            return 0L;
        }
    }

    public static Integer toInt(Object obj) {
        return toLong(obj).intValue();
    }
}

JWT配置类

获取配置文件中的数据

@ConfigurationProperties(prefix = "config.jwt")
@Component
@Data
public class JwtConfig {
    private JwtProperties jwtProperties;
    /*
     * 根据身份ID标识,生成Token
     */
    public String getToken (String identityId){
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(identityId)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /*
     * 获取 Token 中注册信息
     */
    public Claims getTokenClaim (String token) {
        try {
            return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(pubKeyPath)).parseClaimsJws(token).getBody();
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }
    /*
     * Token 是否过期验证
     */
    public boolean isTokenExpired (Date expirationTime) {
        return expirationTime.before(new Date());
    }
    private String secret;
    private long expire;
    private String pubKeyPath;
    private String header;
}

4、实现用户登录

controller

import com.modules.application.common.oauth.Result;
import com.modules.application.common.oauth.ResultStatusCode;
import com.modules.application.common.properties.JwtProperties;
import com.modules.application.common.utils.CookieUtils;
import com.modules.application.common.utils.StringUtils;
import com.modules.application.common.web.BaseController;
import com.modules.application.system.paramVo.UserInfoVO;
import com.modules.application.system.service.UserInfoService;
import com.modules.application.system.utils.JwtUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/user")
@Api(value = "LoginController", tags = {"LoginController"}, description = "注册/登录")
public class LoginController extends BaseController {

    @Resource
    private UserInfoService userInfoService;
    @Resource
    private JwtProperties jwtProperties;


    /**
     * 根据用户名及密码判断用户是否登录
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "通过用户名和密码登录")
    @ApiResponses({
            @ApiResponse(code = 200, message = "返回数据", response = Result.class)
    })
    public Result login( HttpServletRequest request, HttpServletResponse response,@RequestParam("username")String username, @RequestParam("password")String password){
        String token = this.userInfoService.login(username, password);
        if (StringUtils.isBlank(token)){
            return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD);
        }
        //使用工具类,设置cookie并返回给浏览器,需要cookie名称,cookie的值,过期时间,配置的是分,默认使用的秒,注意乘以60
        CookieUtils.setCookie(request, response, jwtProperties.getCookieName(), token, jwtProperties.getExpire() * 60);
        return new Result(ResultStatusCode.OK,"登录成功!");
    }


    /**
     *解析浏览器中的cookie,获取当前登录用户
     * @param request
     * @param response
     * @return
     */
    @GetMapping("/getUserInfoByToken")
    @ApiOperation(value = "通过token获取用户信息")
    @ApiResponses({
            @ApiResponse(code = 200, message = "返回数据", response = UserInfoVO.class)
    })
    public Result getUserInfoByToken(HttpServletRequest request, HttpServletResponse response){
        try {
            //通过request获取token
            String token= CookieUtils.getCookie(request, jwtProperties.getCookieName());
            //通过jwt获取用户
            UserInfoVO userInfo = JwtUtils.getInfoFromToken(token,jwtProperties.getPublicKey());
            if (userInfo==null){
                return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD);
            }
            //刷新jwt中的有效时间
            token = JwtUtils.generateToken(userInfo,jwtProperties.getPrivateKey(),jwtProperties.getExpire());
            //刷新cookie中的有效时间
            CookieUtils.setCookie(request,response,jwtProperties.getCookieName(),token,jwtProperties.getExpire()*60);
            return new Result(ResultStatusCode.OK,userInfo);
        }catch (Exception e){
            e.printStackTrace();
        }
        return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD);
    }


    
}

service

import com.modules.application.common.properties.JwtProperties;
import com.modules.application.common.service.CrudService;
import com.modules.application.system.dao.UserInfoMapper;
import com.modules.application.system.entity.UserInfo;
import com.modules.application.system.paramVo.UserInfoVO;
import com.modules.application.system.utils.CodecUtils;
import com.modules.application.system.utils.JwtUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * 用户信息Service
 * @author wcf
 * @version 2018-04-17
 */
@Service
@Transactional(readOnly = true)
public class UserInfoService extends CrudService<UserInfoMapper, UserInfo> {
	@Resource
	private JwtProperties jwtProperties;
	@Resource
	private UserInfoMapper userInfoMapper;


	/**
	 * 通过用户名查询用户
	 * @param username
	 * @param password
	 * @return
	 */
	public UserInfo findUserByUserName(String username, String password) {
		//根据用户名查询到用户
		UserInfo user = this.userInfoMapper.findUserByUsername(username);
		//是否存在判断
		if (user == null) {
			return null;
		}
		//使用用户内的盐加请求的密码进行加密处理,再与数据库内的密码进行判断
		String md5Hex = CodecUtils.md5Hex(password, user.getSalt());
		if (!org.apache.commons.lang3.StringUtils.equals(user.getPassword(), md5Hex)) {
			return null;
		}
		return user;
	}

	/**
     * 用户登录
	 * @param username
     * @param password
     * @return
     */
	public String login(String username, String password) {
		//根据用户名查询到用户
		UserInfo user = findUserByUserName(username,password);
		//是否存在判断
		if (user == null) {
			return null;
		}
		try {
			//生成token逻辑
			UserInfoVO userInfo = new UserInfoVO();
			userInfo.setId(user.getId());
			userInfo.setUsername(user.getUsername());
			String token = JwtUtils.generateToken(userInfo, jwtProperties.getPrivateKey(), jwtProperties.getExpire());
			return token;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
}

dao层接口

import com.modules.application.common.persistence.CrudDao;
import com.modules.application.system.entity.UserInfo;

/**
 * 用户信息DAO接口
 * @author wcf
 * @version 2018-04-17
 */
public interface UserInfoMapper extends CrudDao<UserInfo> {

    public UserInfo findUserByUsername(String username);

}

xml文件

<select id="findUserByUsername" resultType="com.modules.application.system.entity.UserInfo">
		SELECT
		<include refid="userInfoColumns"/>
		FROM `user` a
		<include refid="userInfoJoins"/>
		WHERE a.name=#{username}
	</select>

5、返回结果类

/**
 * api接口返回的数据模型
 * @author chunqiu
 *
 */
public class Result {
	private int code;		//返回的代码,0表示成功,其他表示失败
    private String msg;		//成功或失败时返回的错误信息
    private Object data;	//成功时返回的数据信息

	public Result(ResultStatusCode resultStatusCode, Object data){
		this.code = resultStatusCode.getCode();
		this.msg = resultStatusCode.getMsg();
		this.data = data;
	}

    public Result(int code, String msg, Object data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public Result(int code, String msg){
    	this(code, msg, null);
	}
	public Result(ResultStatusCode resultStatusCode){
    	this(resultStatusCode, null);
	}

	public int getCode() {
		return code;
	}
	public void setCode(int code) {
		this.code = code;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}
	public Object getData() {
		return data;
	}
	public void setData(Object data) {
		this.data = data;
	}


}

状态类

/**
 * @param
 * @author chen
 * @description 错误代码
 * @return
 */
public enum ResultStatusCode {
    OK(0, "OK"),
    BAD_REQUEST(400, "参数解析失败"),
    INVALID_TOKEN(401, "无效的授权码"),
    INVALID_CLIENTID(402, "无效的密钥"),
    METHOD_NOT_ALLOWED(405, "不支持当前请求方法"),
    SYSTEM_ERR(500, "服务器运行异常"),
    NOT_EXIST_USER_OR_ERROR_PWD(10000, "该用户不存在或密码错误");


    private int code;
    private String msg;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    private ResultStatusCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

6、实现拦截器

import com.modules.application.common.config.JwtConfig;
import com.modules.application.common.properties.JwtProperties;
import com.modules.application.common.utils.CookieUtils;
import com.modules.application.common.utils.StringUtils;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 请求api服务器时,对请求进行拦截判断,有效则可以访问接口,否则返回错误
 *
 * @author Win7
 */
@Component
public class InterceptorJWT extends HandlerInterceptorAdapter {
    @Resource
    private JwtConfig jwtConfig;
    @Resource
    private JwtProperties jwtProperties;


    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 地址过滤
        String uri = request.getRequestURI();
        if (uri.contains("/login")) {
            return true;
        }
        // Token 验证
        String token = CookieUtils.getCookie(request, jwtProperties.getCookieName());
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter(jwtConfig.getHeader());
        }
        if (StringUtils.isEmpty(token)) {
            throw new Exception(jwtConfig.getHeader() + "不能为空");
        }
        Claims claims = jwtConfig.getTokenClaim(token);
        if (claims == null || jwtConfig.isTokenExpired(claims.getExpiration())) {
            throw new Exception(jwtConfig.getHeader() + "失效,请重新登录");
        }
        //设置 identityId 用户身份ID
        request.setAttribute("identityId", claims.getSubject());
        return true;
    }
}

通过拦截器拦截请求,验证用户是否处于登录状态,成功则放行,否则进行拦截,让用户重新登陆。

总结

这套流程中主要通过JWT机制将特定的信息通过RSA加密方式生成token传递给前端的Cookie,用户每次请求时都会携带该Cookie,然后后端将获得的Cookie进行解密,获得token,然后获得对应的用户信息,可以执行后续操作。