登录需求分析:
- 登录采取弹出层的形式
- 登录方式:第一种:手机号+验证码、第二种:微信扫码登录
- 无注册页面、第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
- 微信扫描登录成功必须绑定手机号码,即:第一次扫描成功后绑定手机号,以后登录扫描直接成功
- 网关统一判断登录状态,如何需要登录,页面弹出登录层
在后台创建登录服务子模块,在service模块下新建子模块service-user登录模块,
配置好相关依赖pom和配置文件application.properties。
# 服务端口
server.port=8160
# 服务名
spring.application.name=service-user
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/yygh_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/c8a/user/mapper/xmluser
public static String getToken(Map payload){
// 指定token过期时间为7天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7);
JWTCreator.Builder builder = JWT.create();
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 指定过期时间和签名算法
String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET));
return token;
}
public static DecodedJWT decode(String token){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return decodedJWT;
}
}
非对称签名
生成jwt串的时候需要指定私钥,解析jwt串的时候需要指定公钥
private static final String RSA_PRIVATE_KEY = "..."; private static final String RSA_PUBLIC_KEY = "..."; public static String getTokenRsa(Map9.2.5 jjwt 对称签名payload){ // 指定token过期时间为7天 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, 7); JWTCreator.Builder builder = JWT.create(); // 构建payload payload.forEach((k,v) -> builder.withClaim(k,v)); // 利用hutool创建RSA RSA rsa = new RSA(RSA_PRIVATE_KEY, null); // 获取私钥 RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey(); // 签名时传入私钥 String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.RSA256(null, privateKey)); return token; } public static DecodedJWT decodeRsa(String token){ // 利用hutool创建RSA RSA rsa = new RSA(null, RSA_PUBLIC_KEY); // 获取RSA公钥 RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey(); // 验签时传入公钥 JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(publicKey, null)).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT; }
io.jsonwebtoken jjwt 0.9.1
public class JwtUtils {
// token时效:24小时
public static final long EXPIRE = 1000 * 60 * 60 * 24;
// 签名哈希的密钥,对于不同的加密算法来说含义不同
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("baobao-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// HS256算法实际上就是MD5加盐值,此时APP_SECRET就代表盐值
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
public static boolean checkToken(HttpServletRequest request) {
try {
// 从http请求头中获取token字符串
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}
jjwt在0.10版本以后发生了较大变化,pom依赖要引入多个
io.jsonwebtoken jjwt-api 0.11.2 io.jsonwebtoken jjwt-impl 0.11.2 runtime io.jsonwebtoken jjwt-jackson 0.11.2 runtime
标准规范中对各种加密算法的secretKey的长度有如下要求:
HS256:要求至少 256 bits (32 bytes)
HS384:要求至少384 bits (48 bytes)
HS512:要求至少512 bits (64 bytes)
RS256 and PS256:至少2048 bits
RS384 and PS384:至少3072 bits
RS512 and PS512:至少4096 bits
ES256:至少256 bits (32 bytes)
ES384:至少384 bits (48 bytes)
ES512:至少512 bits (64 bytes)
- 新版本的jjwt中,之前的签名和验签方法都是传入密钥的字符串,已经过时。最新的方法需要传入Key对象
public class JwtUtils {
// token时效:24小时
public static final long EXPIRE = 1000 * 60 * 60 * 24;
// 签名哈希的密钥,对于不同的加密算法来说含义不同
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHOsdadasdasfdssfeweee";
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
.setSubject("baobao-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// 传入Key对象
.signWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
return JwtToken;
}
public static Jws decode(String jwtToken) {
// 传入Key对象
Jws claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
return claimsJws;
}
}
非对称签名
生成jwt串的时候需要指定私钥,解析jwt串的时候需要指定公钥
private static final String RSA_PRIVATE_KEY = "...";
private static final String RSA_PUBLIC_KEY = "...";
public static String getJwtTokenRsa(String id, String nickname){
// 利用hutool创建RSA
RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
RSAPrivateKey privateKey = (RSAPrivateKey) rsa.getPrivateKey();
String JwtToken = Jwts.builder()
.setSubject("baobao-user")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.claim("id", id)
.claim("nickname", nickname)
// 签名指定私钥
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
return JwtToken;
}
public static Jws decodeRsa(String jwtToken) {
RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
RSAPublicKey publicKey = (RSAPublicKey) rsa.getPublicKey();
// 验签指定公钥
Jws claimsJws = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(jwtToken);
return claimsJws;
}
实际开发中的应用
在实际的SpringBoot项目中,一般我们可以用如下流程做登录:
- 在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间
- 将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端
- 前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串
- 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String JWT = request.getHeader("Authorization");
try {
// 1.校验JWT字符串
DecodedJWT decodedJWT = JWTUtils.decode(JWT);
// 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
...
return true;
}catch (SignatureVerificationException e){
System.out.println("无效签名");
e.printStackTrace();
}catch (TokenExpiredException e){
System.out.println("token已经过期");
e.printStackTrace();
}catch (AlgorithmMismatchException e){
System.out.println("算法不一致");
e.printStackTrace();
}catch (Exception e){
System.out.println("token无效");
e.printStackTrace();
}
return false;
}
}
9.3 集成JWT在实际开发中需要用下列手段来增加JWT的安全性:
因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全
JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的。因此要保证服务器的安全
JWT可以使用暴力穷举来破解,所以为了应对这种破解方式,可以定期更换服务端的哈希签名密钥(相当于盐值)。这样可以保证等破解结果出来了,你的密钥也已经换了
- 在common-util模块的pom中加入依赖
io.jsonwebtoken jjwt
- 在common-util模块下新建工具类
package com.tjj.common.helper;
//省略import...
public class JwtHelper {
private static long tokenExpiration = 24*60*60*1000;
private static String tokenSignKey = "123456";//这个签名可以是任一的
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("userName", userName)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
public static String getUserName(String token) {
if(StringUtils.isEmpty(token)) return "";
Jws claimsJws
= Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String)claims.get("userName");
}
public static void main(String[] args) {
String token = JwtHelper.createToken(1L, "lucy");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUserName(token));
}
}
- 创建好工具类之后,便可以直接在service层生成token
@Service public class UserInfoServiceImpl extends ServiceImplimplements UserInfoService { //省略... @Override public Map login(LoginVo loginVo) { //省略... //jwt生成token String token = JwtHelper.createToken(userInfo.getId(), name); map.put("token", token); return map; } //省略... }



