JWT-BOOT
JWT也是单点登录的一种实现,而且JWT本身就能携带很多数据,所以没必要远程验证token,简单,好用,主要有以下几点
- JWT是以一种字符串的特殊结构,这个结构内容以及操作流程都比较简单
- 在进行前后端分离的时候,JWT可以做出类似于Session的失效时间的效果
- JWT本身可以携带很多数据(头信息,附加信息,数字签名)
为了简化SSO的实现以及第三方应用的整合难度,可以直接利用Token数据实现用户认证信息存储,这样每次进行资源访问的时候就直接传入Token并且进行校验即可,并且每一次Token数据小,便于网络的传输
在实际的开发过程中,JWT是实现用户认证处理,所以第三方客户端要想进行统一的登录处理,只需要传入用户名以及密码就能获取到Token令牌,考虑到令牌的安全性以及实用型,在每个JWT中都会包含以下三类数据 (Header头信息、Payload负载信息、Signature数字签名)
【图片来源网络侵权删除】
准备工作
本次使用的是基于 Spring-Boot 编写的 JWT 授权校验,通过将核心的token创建解析操作封装到一个自动装配模块中,结合数据库、拦截器的形式拦截需要访问的目标资源进行权限管控,并通过使用Postman模拟真实的用户请求。
表结构设计
- sm_user (用户表)
-
sm_role(角色表)
-
sm_user_role(用户角色关系表)
-
sm_action(权限表)
- sm_role_action(角色权限表)
技术架构
Gradle+SpringBoot+JWT+MySQL+GIT
项目结构
项目依赖
// 通用模块
project(':sm-common'){
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
}
}
// JWT模块
project(':sm-jwt-starter'){
dependencies {
annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')
implementation(libraries.'servlet-api')
implementation(libraries.'commons-codec')
compile(libraries.'jjwt')
compile(libraries.'jaxb-api')
compile(libraries.'jaxb-impl')
compile(libraries.'jaxb-core')
}
}
// 核心业务模块
project(':sm-manage'){
dependencies {
implementation(project(':sm-common'))
implementation(project(':sm-jwt-starter'))
implementation(libraries.'mybatis-plus')
implementation(libraries.'druid-spring-boot-starter')
implementation(libraries.'MySQL')
}
}
常量类
避免大量的魔法值的出现,特别编写一个常量类保存具体的字符串值
package com.yzborder.data.constants;
/**
* @author ZhenBang-Yi
* @ClassName SMConstants
* @date 2022/7/6 19:05
*/
public class SMConstants {
/**
* @Description: 是
**/
public static String YES = "1";
/**
* @Description: 否
**/
public static String No = "0";
/**
* @Description: 登录状态
**/
public static String LOGIN_STATUS = "status";
/**
* @Description: 用户用户
**/
public static String LOGIN_USER_NAME = "userName";
/**
* @Description: 登录用户ID
**/
public static String LOGIN_USER_ID = "userId";
/**
* @Description: TokenName
**/
public static String TOKEN_NAME = "token";
public static String TOKEN_ID = LOGIN_USER_ID;
}
sm-jwt-starter
1、定义 JwtTokenProperties 实体类,接受配置的token生成所需要的属性
package com.yzborder.jwt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author ZhenBang-Yi
* @ClassName JWTTokenProperties
* @date 2022/6/15 22:02
*/
@Data
@ConfigurationProperties(prefix = "yzborder.config.jwt")
public class JwtTokenProperties {
/**加密秘钥*/
private String secret;
/** 失效时间 */
private long expire;
/** 签发人 */
private String issuer;
/** 签名信息 */
private String sign;
}
2、定义 JwtPassWordProperties 实体类,接受配置的密码加密处理所需的属性
package com.yzborder.jwt.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author ZhenBang-Yi
* @ClassName JwtPassWordProperties
* @date 2022/6/18 20:36
*/
@Data
@ConfigurationProperties(prefix = "yzborder.config.password")
public class JwtPassWordProperties {
private String sale;
private int repeat;
}
3、创建 IJwtTokenService 服务,用于创建、解析、刷新 token 操作。
package com.yzborder.jwt.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import javax.crypto.SecretKey;
import java.util.Map;
public interface IJwtTokenService {
/**
* 创建加密key
*
* @Author: ZhenBang-Yi
* @Date 2022/6/12 22:22
**/
SecretKey createSecretKey();
/**
* 创建Token 要求保存用户ID以及附加数据
*
* @param id 用户ID
* @param subjtct 附加数据
* @return java.lang.String 生成后的token
* @Author: ZhenBang-Yi
* @Date 2022/6/12 22:24
**/
String createToken(String id, Map<String, Object> subjtct);
/**
* 将待解析的token传入,解析token,返回解析数据
*
* @param token 待解析的token
* @return io.jsonwebtoken.Jws<io.jsonwebtoken.Claims> 解析jwt失败的时候触发的异常
* @Author: ZhenBang-Yi
* @Date 2022/6/12 22:25
**/
Jws<Claims> parseToken(String token) throws JwtException;
/**
* 验证token是否合法,成功:true,失败:false
*
* @param token 待验证的token
* @return boolean
* @Author: ZhenBang-Yi
* @Date 2022/6/12 22:26
**/
boolean verifyToken(String token);
/**
* 将待刷新的token传入,重新生成一个新的token
*
* @param token 待刷新token
* @return java.lang.String 新创建的token
* @Author: ZhenBang-Yi
* @Date 2022/6/12 22:27
**/
String refreshToken(String token);
}
4、创建 IPasswordService 对密码进行加密处理
package com.yzborder.jwt.service;
public interface IPasswordService {
/**
* 对密码进行加密处理
* @author: ZhenBang-Yi
* @date 2022/6/18 20:35
* @param password 原始密码
* @return java.lang.String
**/
String encryptPassword(String password);
}
5、实现 token 服务
package com.yzborder.jwt.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yzborder.jwt.config.JwtTokenProperties;
import com.yzborder.jwt.service.IJwtTokenService;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author ZhenBang-Yi
* @ClassName JwtTokenServiceImpl
* @date 2022/6/15 22:05
*/
public class JwtTokenServiceImpl implements IJwtTokenService {
@Autowired
private ObjectMapper objectMapper;
@Value("${spring.application.name}")
private String applicationName;
@Autowired
private JwtTokenProperties jwtConfigProperties;
private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@Override
public SecretKey createSecretKey() {
byte[] bytes = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
return new SecretKeySpec(bytes, 0, bytes.length, "HES");
}
@Override
public String createToken(String id, Map<String, Object> subjtct) {
/**
* 怎么创建token?
* 1、创建头信息以及附加信息
* 2、记录创建时间及失效时间
* 3、通过build进行构建token
* */
Date currentTime = new Date(System.currentTimeMillis());
Date expireDate = new Date(currentTime.getTime() + this.jwtConfigProperties.getExpire() * 1000);
Map<String, Object> claimsMap = new HashMap<>();
claimsMap.put("author", "易振邦");
Map<String, Object> headersMap = new HashMap<>();
headersMap.put("moudule", this.applicationName);
JwtBuilder builder = null;
try {
builder = Jwts.builder()
.setClaims(claimsMap)
.setHeader(headersMap)
.setId(id)
.setSubject(this.objectMapper.writeValueAsString(subjtct))
.setIssuedAt(currentTime)
.setIssuer(this.jwtConfigProperties.getIssuer())
.signWith(this.signatureAlgorithm, this.createSecretKey())
.setExpiration(expireDate)
;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return builder.compact();
}
@Override
public Jws<Claims> parseToken(String token) throws JwtException {
if (this.verifyToken(token)) {
return Jwts.parser().setSigningKey(this.createSecretKey()).parseClaimsJws(token);
}
return null;
}
@Override
public boolean verifyToken(String token) {
try {
Jwts.parser().setSigningKey(this.createSecretKey()).parseClaimsJws(token).getBody();
return true;
} catch (Exception e) {
}
return false;
}
@Override
public String refreshToken(String token) {
if (this.verifyToken(token)) {
Claims body = this.parseToken(token).getBody();
try {
return this.createToken(body.getId(), this.objectMapper.readValue(body.getSubject(), Map.class));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return null;
}
}
需要注意的是,在整个JWT的数据传输中所有的数据都是以密文传输的,既然要密文传输肯定需要加密算法的支持
- AES:对称加密
- RSA:非对称加密
- 单向函数散列算法:不可逆加密(MD5)
6、实现密码家里服务
package com.yzborder.jwt.service.impl;
import com.yzborder.jwt.config.JwtPassWordProperties;
import com.yzborder.jwt.service.IPasswordService;
import org.springframework.beans.factory.annotation.Autowired;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @author ZhenBang-Yi
* @ClassName PasswordServiceImpl
* @date 2022/6/18 20:37
*/
public class PasswordServiceImpl implements IPasswordService {
@Autowired
private JwtPassWordProperties jwtPassWordProperties;
private static MessageDigest MD5_DIGEST = null;
private static Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
static {
try {
MD5_DIGEST = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Override
public String encryptPassword(String password) {
String saltPassword = "【" + this.jwtPassWordProperties.getSale() + "】" + password;
for (int i = 0; i < this.jwtPassWordProperties.getRepeat(); i++) {
saltPassword = BASE64_ENCODER.encodeToString(MD5_DIGEST.digest(saltPassword.getBytes()));
}
return saltPassword;
}
}
7、考虑到后续需要集合前端UI实现访问,Token校验结果通过枚举进行定义
package com.yzborder.jwt.enums;
import javax.servlet.http.HttpServletResponse;
public enum JWTResponseCode {
SUCCESS_CODE(HttpServletResponse.SC_OK, "Token验证成功,验证成功!"),
TOKEN_TIMEOUT_CODE(HttpServletResponse.SC_BAD_REQUEST, "验证失效,Token超时,请重新申请"),
NO_ACCESS_CODE(HttpServletResponse.SC_UNAUTHORIZED,"没有权限访问目标资源,请联系管理员申请权限"),
NO_AUTH_CODE(HttpServletResponse.SC_NOT_FOUND, "没有找到匹配的Token,无法进行访问");
private int code;
private String msg;
JWTResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public String toString() {
return "{\"code\":"+this.code+",\"message\":"+this.msg+"}";
}
}
8、配置 application.yml 文件
spring:
application:
name: sm-jwt-starter
yzborder:
config:
jwt:
expire: 1000 #单位:秒
issuer: ZhenBangYi
secret: per.yzb.com
sign: ZhenBangYi
password:
repeat: 5
sale: 你好
9、编写测试类,测试token的生成、解析、校验、刷新是否有异常、密码加密处理是否有异常
10、因为是一个加载器,所以需要添加以下配置 声明JWTAutoConfiguration
package com.yzborder.jwt.autoconfiguration;
import com.yzborder.jwt.config.JwtPassWordProperties;
import com.yzborder.jwt.config.JwtTokenProperties;
import com.yzborder.jwt.service.IJwtTokenService;
import com.yzborder.jwt.service.IPasswordService;
import com.yzborder.jwt.service.impl.JwtTokenServiceImpl;
import com.yzborder.jwt.service.impl.PasswordServiceImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ZhenBang-Yi
* @ClassName JWTAutoConfiguration
* @date 2022/6/15 22:12
*/
@Configuration
@EnableConfigurationProperties({JwtTokenProperties.class, JwtPassWordProperties.class})
public class JWTAutoConfiguration {
@Bean("tokenService")
public IJwtTokenService getTokenService() {
return new JwtTokenServiceImpl();
}
@Bean("passwordService")
public IPasswordService getPasswordService() {
return new PasswordServiceImpl();
}
}
创建 META-INF/spring.factories 文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.yzborder.jwt.autoconfiguration.JWTAutoConfiguration
致此已经完成了核心的 token 生成解析操作,要想进行授权的访问,还需要定义一个注解,注解的作用在于:使用了该注解的方法不进行 token 校验操作
package com.yzborder.jwt.annotation;
import org.springframework.stereotype.Service;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 被这个注解注释的类不需要验证Token
*
* @author: ZhenBang-Yi
* @date 2022/7/6 20:13
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtNonCheckToken {
/*
* 是否不验证Token
* */
boolean require() default true;
}
最后运行 Gradle 命令就完成自动装配模块的开发啦 gradle build -x test
sm-manage
接下来编写 核心业务模块,这个模块完成的操作有:登录实现、资源拦截、权限校验
1、最终是要与前台进行交互的,所以编写 DTO 类进行账户信息的传输
package com.yzborder.data.dto;
import lombok.Data;
/**
* @author ZhenBang-Yi
* @ClassName LoginUserDto
* @date 2022/7/6 11:18
*/
@Data
public class LoginUserDto {
private String userId;
private String password;
}
2、编写用户登录服务,定义登录操作接口
package com.yzborder.service;
import com.yzborder.data.dto.LoginUserDto;
import java.util.Map;
/**
* 登录 服务
*
* @author: ZhenBang-Yi
* @date 2022/7/6 19:18
**/
public interface ILoginService {
/**
* 用户登录
*
* @param loginUserDto 包含未加密的 账号密码
* key1: status ; value: true||false
* key2: mid ; value: 用户ID
* key3: name ; value: 用户名
* key4:resource ; value : 暂定
* @author: ZhenBang-Yi
* @date 2022/7/6 19:19
**/
Map<String, Object> login(LoginUserDto loginUserDto);
}
3、实现 ILoginService 编写具体的登录代码
package com.yzborder.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yzborder.data.constants.SMConstants;
import com.yzborder.data.dto.LoginUserDto;
import com.yzborder.data.entity.login.SmUser;
import com.yzborder.service.ILoginService;
import com.yzborder.service.SmUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author ZhenBang-Yi
* @ClassName LoginServiceImpl
* @date 2022/7/6 19:22
*/
@Service
public class LoginServiceImpl implements ILoginService {
@Autowired
private SmUserService smUserService;
@Override
public Map<String, Object> login(LoginUserDto loginUserDto) {
Map<String, Object> result = new HashMap();
// 通过用户ID查询是否存在该用户
SmUser user = this.smUserService.getOne(Wrappers.lambdaQuery(SmUser.class).eq(SmUser::getUserId, loginUserDto.getUserId()));
// 不存在用户ID为 userId 的账户 或者 密码为空 或者 密码不同 或者 用户被锁定
if (Objects.isNull(user) || StringUtils.isEmpty(user.getUserPassword()) || !Objects.equals(loginUserDto.getPassword(), user.getUserPassword()) || Objects.equals(SMConstants.No, user.getLocked())) {
result.put(SMConstants.LOGIN_STATUS, false);
return result;
} else {
result.put(SMConstants.LOGIN_STATUS, true);
Map<String, Object> map = new HashMap<>();
map.put(SMConstants.LOGIN_USER_ID, user.getUserId());
map.put(SMConstants.LOGIN_USER_NAME, user.getUserName());
result.put("resources", map);
}
return result;
}
}
4、接口定义好了,那么可以直接编写登录Action提供用户访问路径
package com.yzborder.action;
import com.yzborder.data.constants.SMConstants;
import com.yzborder.data.dto.LoginUserDto;
import com.yzborder.jwt.annotation.JwtNonCheckToken;
import com.yzborder.jwt.service.IJwtTokenService;
import com.yzborder.jwt.service.IPasswordService;
import com.yzborder.service.ILoginService;
import com.yzborder.service.SmUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Objects;
/**
* @author ZhenBang-Yi
* @ClassName LoginAction
* @date 2022/7/6 22:29
*/
@RequestMapping(value = "login/*")
@RestController
public class LoginAction {
@Autowired
private IJwtTokenService iJwtTokenService;
@Autowired
private ILoginService iLoginService;
@Autowired
private IPasswordService iPasswordService;
@Autowired
private SmUserService smUserService;
@JwtNonCheckToken
@PostMapping("login")
public String login(@RequestBody LoginUserDto loginUserDto) {
if (Objects.isNull(loginUserDto)) {
throw new IllegalArgumentException("参数丢失,请重新发送请求操作!!!");
}
loginUserDto.setPassword(this.iPasswordService.encryptPassword(loginUserDto.getPassword()));
Map<String, Object> login = this.iLoginService.login(loginUserDto);
// 登录成功,ID = UserId
if ((boolean) login.get(SMConstants.LOGIN_STATUS)) {
System.out.println(this.iJwtTokenService);
Map<String, Object> map = (Map<String, Object>) login.get("resources");
String token = this.iJwtTokenService.createToken(map.get(SMConstants.TOKEN_ID).toString(), map);
return token;
}
return null;
}
@PostMapping("getlist")
public Object list() {
return this.smUserService.list();
}
}
5、JWT的授权校验操作还需一个拦截器进行资源拦截,权限管控
6、配置拦截器
package com.yzborder.config;
import com.yzborder.config.intercept.JwtAuthorizationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author ZhenBang-Yi
* @ClassName JwtInterceptor
* @date 2022/7/11 19:21
*/
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getJwtInterceptorBean()).addPathPatterns("/**");
}
@Bean
public JwtAuthorizationInterceptor getJwtInterceptorBean() {
return new JwtAuthorizationInterceptor();
}
}
以上就是近期编写的JWT生成校验及授权访问操作。