JWT(Token令牌)整合 SpringBoot
前言:
JWT(JSON Web Token)可以被称为令牌(token)。JWT是一种在网络应用中广泛使用的令牌格式,用于在用户和服务器之间传递安全可靠的信息。JWT通常包含了用户的身份信息和一些其他的元数据,被用作身份验证和授权。因此,人们经常将JWT简称为令牌(token)。
代码整合:
1. 导入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- javax.xml.bind.DatatypeConverter 在Java9 版本后加 ->--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> <!-- 简化实体类 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.16</version> </dependency> <!-- StringUtils等工具类 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <!--hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.0.0</version> </dependency> </dependencies>
2. 代码
1) pojo层
@Data @AllArgsConstructor @NoArgsConstructor @TableName("user") public class User { @TableId(type = IdType.AUTO) private Integer id; @TableField("username") private String username; @TableField("password") private String password; @TableField("age") private String age; @TableField("nick") private String nick; @TableField("icon") private String icon; }
2) mapper/dao层
@Mapper public interface UserMapper extends BaseMapper<User> { }
3) model层
public class Result<T> implements Serializable { private static final long serialVersionUID = 1L; /** * 成功标志 */ private boolean success = true; /** * 返回处理消息 */ private String message = "操作成功!"; /** * 返回代码 */ private Integer code = 0; /** * 返回数据对象 data */ private T result; public static long getSerialVersionUID() { return serialVersionUID; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public T getResult() { return result; } public void setResult(T result) { this.result = result; } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } /** * 时间戳 */ private long timestamp = System.currentTimeMillis(); public Result() { } public Result<T> success(String message) { this.message = message; this.code = 200; this.success = true; return this; } public static Result<Object> ok() { Result<Object> r = new Result<Object>(); r.setSuccess(true); r.setCode(200); r.setMessage("成功"); return r; } public static Result<Object> ok(String msg) { Result<Object> r = new Result<Object>(); r.setSuccess(true); r.setCode(200); r.setMessage(msg); return r; } public static Result<Object> ok(Object data) { Result<Object> r = new Result<Object>(); r.setSuccess(true); r.setCode(200); r.setResult(data); return r; } public static Result<Object> error(String msg) { return error(500, msg); } public static Result<Object> error(int code, String msg) { Result<Object> r = new Result<Object>(); r.setCode(code); r.setMessage(msg); r.setSuccess(false); return r; } public Result<T> error500(String message) { this.message = message; this.code = 500; this.success = false; return this; } /** * 无权限访问返回结果 */ public static Result<Object> noauth(String msg) { return error(555, msg); } }
4) service/impl层
public interface IUserService { // 注册 Result register(User user); // 登录 Result login(String username, String password); }
@Service @Transactional // 在Service层使用,以确保Service层方法中的数据库操作在一个事务内执行 public class UserServiceImpl implements IUserService { @Resource private UserMapper userMapper; /** * 注册 * @param user * @return // N:java_bei P:bei2024.. */ @Override public Result register(User user) { // 判断用户名格式与是否被注册 String username = user.getUsername(); // 判断用户名是否被注册 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); if (userMapper.selectOne(queryWrapper) != null) { return Result.error("此用户名已被注册."); } // 判断用户名格式 if (!RegexUtil.checkUsername(username)) { return Result.error("用户名格式有误."); } // 判断密码格式 String userPassword = user.getPassword(); if (!RegexUtil.checkPassword(userPassword)) { return Result.error("密码格式有误."); } // 密码加密 user.setPassword(MD5Util.MD5Encode(user.getPassword(),"utf-8")); int insertUser = userMapper.insert(user); return insertUser > 0 ? Result.ok("注册成功.") : Result.error("注册失败."); } /** * 登录 * @param username * @param password * @return */ @Override public Result login(String username, String password) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); queryWrapper.eq("password", MD5Util.MD5Encode(password,"utf-8")); User userMatch = userMapper.selectOne(queryWrapper); // 加上JWT(token) userMatch.setPassword(null); if (userMatch != null) { String jsonString = JSON.toJSONString(userMatch); String token = JwtUtil.createJWT(userMatch.getId().toString(), jsonString, 7 * 24 * 2600 * 1000L); return Result.ok(token); } return Result.error("用户名或密码有误."); } }
5) controller层
@RestController @RequestMapping("/user") public class UserController { @Resource private IUserService iUserService; /** * 注册 * @param user * @return */ @PostMapping("/register") public Result register(User user) { return iUserService.register(user); } /** * 登录 * @param username * @param password * @return */ @PostMapping("/login") public Result login(String username, String password) { return iUserService.login(username, password); } /** * 登录后给予权限跳转主页 * @param username * @param password * @return */ @PostMapping("/home") public Result home( String username,String password){ return Result.ok("首页"); } }
3. 工具类
utils包:
JWT工具类:
/** * 创建和解析JWT,可以在用户登录时生成JWT,并在需要验证用户身份时解析JWT来获取用户信息。 */ public class JwtUtil { // token 有效期 public static final Long JWT_TTL = 3600000L; // 60 * 60 * 1000 一个小时 // Jwt令牌信息 public static final String JWT_KEY = "mysql20240224"; // 创建token方法 public static String createJWT(String id, String subject, Long ttlMillis) { // id, 信息, 过期时间 // 指定算法 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 当前系统时间 long nowMillis = System.currentTimeMillis(); //令牌签发时间 Date now = new Date(nowMillis); // 如果令牌有效期为null,则默认设置有效期1小时 if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; } // 令牌过期时间设置 long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); // 生成秘钥 SecretKey secretKey = generalKey(); // 封装Jwt令牌信息 JwtBuilder builder = Jwts.builder() .setId(id) // 唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("admin") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙 .setExpiration(expDate); // 设置过期时间 // builder.addClaims(map); 可以放一些额外信息; return builder.compact(); } /** * 生成加密 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); // 转为字节码 SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析令牌数据 * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
MD5加密:
/** * @description: MD5加密工具类 */ public class MD5Util { // 将字节数组转换为对应的十六进制字符串。 private static String byteArrayToHexString(byte b[]) { StringBuffer resultSb = new StringBuffer(); for (int i = 0; i < b.length; i++) { resultSb.append(byteToHexString(b[i])); } return resultSb.toString(); } // 将单个字节转换为对应的两个十六进制字符 private static String byteToHexString(byte b) { int n = b; if (n < 0) { n += 256; } int d1 = n / 16; int d2 = n % 16; return hexDigits[d1] + hexDigits[d2]; } // 接收一个字符串和字符集名称作为参数,对输入的字符串进行MD5加密,并返回加密后的结果。 public static String MD5Encode(String origin, String charsetname) { String resultString = null; try { resultString = new String(origin); MessageDigest md = MessageDigest.getInstance("MD5"); if (charsetname == null || "".equals(charsetname)) { resultString = byteArrayToHexString(md.digest(resultString .getBytes())); } else { resultString = byteArrayToHexString(md.digest(resultString .getBytes(charsetname))); } } catch (Exception exception) { } return resultString; } // 这个数组用于将字节转换为十六进制字符。 private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}; public static void main(String[] args) { System.out.println(MD5Util.MD5Encode("123", "UTF-8")); } }
正则表达式:
public class RegexUtil { // 用户名的正则表达式:4至8位,只能为单独的英文或中文(中文的话为2至4位),英文可以加下划线 private static final String USERNAME_REGEX = "^([a-zA-Z_]{4,8}|[\\u4E00-\\u9FA5]{2,4})$"; // 密码的正则表达式:6至12位,包含字母、数字、特殊字符 private static final String PASSWORD_REGEX = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*()_+\\-={}\\[\\]:;\"'<>,.?/\\\\]).{6,12}$"; // 检查用户名是否匹配正则表达式 public static boolean checkUsername(String username) { Pattern pattern = Pattern.compile(USERNAME_REGEX); Matcher matcher = pattern.matcher(username); return matcher.matches(); } // 检查密码是否匹配正则表达式 public static boolean checkPassword(String password) { Pattern pattern = Pattern.compile(PASSWORD_REGEX); Matcher matcher = pattern.matcher(password); return matcher.matches(); } public static void main(String[] args) { // 测试用户名 String[] usernames = {"abcd", "abcd123", "abcdABCD", "abcd中文", "abcd12中文", "java@zhanmusi", "Hello,World", "张三", "二零二四"}; for (String username : usernames) { System.out.println(username + " is valid: " + checkUsername(username)); } // 测试密码 String[] passwords = {"abc123", "abc@123", "abc123ABC", "abcABC@123", "abc"}; for (String password : passwords) { System.out.println(password + " is valid: " + checkPassword(password)); } } }
4. 拦截器配置
config包:
/** * 配置类主要用于配置静态资源的访问路径以及注册拦截器,其中拦截器可以用于实现登录验证、权限控制等功能。 * 在这里,拦截器 loginInterceptor 可能用于检查用户的登录状态,对需要登录的请求进行拦截和处理。 */ @Configuration public class WebConfigurer implements WebMvcConfigurer { private static final List<String> EXCLUDE_PATH = Arrays.asList("/user/login","/user/register", "/login.html","/register.html"); @Autowired LoginInterceptor loginInterceptor; // 这个方法是用来配置静态资源的,比如html,js,css,等等 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**"); } // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**")//拦截的路径 .excludePathPatterns(EXCLUDE_PATH); } }
拦截器:
interceptor包
/** * 拦截器:拦截器主要用于实现对请求的权限控制,检查用户是否已经登录(携带有效的 token),如果没有登录则拒绝访问,并返回相应的提示信息。 */ @Component public class LoginInterceptor implements HandlerInterceptor { // 带 token --> 进; 不带 --> 禁 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if(StringUtils.isNoneBlank(token)){ Claims claims = JwtUtil.parseJWT(token); if(claims!=null){ return true; } } HashMap<String, Object> map = new HashMap<>(); map.put("msg", "禁止访问"); map.put("status", 403); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(map); response.getWriter().println(s); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
4. 页面及配置
application.properties:
#??? server.port=8080 #???? spring.application.name=Java_jwt #??? # server.servlet.context-path=/dispatch-nlp # Redis?????????0? #?????????0 #spring.redis.database=0 #????????????8 #spring.redis.jedis.pool.max-active=600 #?????????????????????? #spring.redis.jedis.pool.max-wait=10000 #?????????????8 #spring.redis.jedis.pool.max-idle=100 #??????(??) #spring.redis.timeout=10000 #???????? server.servlet.encoding.charset=UTF-8 server.servlet.encoding.enabled=true server.servlet.encoding.force=true server.tomcat.uri-encoding=UTF-8 #-----------------------------Mysql???? spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driverClassName=com.mysql.jdbc.Driver #mybatis.config-location=classpath:mybatis-config.xml mybatis-plus.type-aliases-package=com.bei.model mybatis-plus.mapper-locations=classpath:mapper/*.xml #-----------------------------Mysql spring.datasource.url=jdbc:mysql://localhost:3306/mysql20240224?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=ll163.cn
register(注册):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>注册2-3</title> </head> <body> <form action="/user/register" method="post"><br> 账户:<input type="text" name="username"/><br> 密码:<input type="password" name="password"><br> 年龄:<input type="text" name="age"><br> 昵称:<input type="text" name="nick"><br> <input type="submit" value="注册"><br> </form> </body> </html>
login(登录):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录1-1</title> </head> <body> <form action="/user/login" method="post"><br> 账户:<input type="text" name="username"/><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>
浏览器访问登录:
Postman模拟token:
流程:登录时等成JWT(token),返回给前端,前端拿到后放到请求头,前端的每次请求都可以带token到请求头;判断是否带上token,并解析(带-进,不带-禁);每次访问都由拦截器拦截。