前言
最近正在搭建一个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,然后获得对应的用户信息,可以执行后续操作。