第一步:创建项目
添加Maven依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!-- 集成Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
在resources/static目录下创建js文件夹,然后在其中添加jquery3.3.1.js文件
编辑application.yml:
server:
port: 80
servlet:
context-path: /jwt
spring:
thymeleaf:
#前缀,也就是模板存放的路径
prefix: classpath:/templates/
#编码格式
encoding: UTF-8
check-template-location: false
#关闭缓存,不然无法看到实时页面
cache: false
#后缀
suffix: .html
#设置不严格的html
mode: HTML
servlet:
content-type: text/html
第二步:创建表示用户的实体类:
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -8390887042859558153L;
private Integer id;
private String username;
private String password;
}
第三步:通用类:
ResultCodeEnum.java
public enum ResultCodeEnum {
/* 成功状态码 */
SUCCESS(200,"操作成功!"),
/* 错误状态码 */
FAIL(-1,"操作失败!"),
/* 用户错误:20001-29999*/
USER_NOT_LOGGED_IN(20001, "用户未登录,请先登录"),
/* 权限错误:70001-79999 */
PERMISSION_TOKEN_EXPIRED(70004, "token已过期"),
PERMISSION_TOKEN_INVALID(70006, "无效token"),
PERMISSION_SIGNATURE_ERROR(70007, "签名失败");
int code; //操作代码
String msg; //提示信息
ResultCodeEnum(int code, String msg) {
this.code = code;
this.msg = 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;
}
}
Result.java
@Getter
@AllArgsConstructor
public class Result<T> {
private int code;
private String msg;
private T data;
public Result<T> setCode(int code) {
this.code = code;
return this;
}
public Result<T> setMsg(String msg) {
this.msg = msg;
return this;
}
public Result<T> setData(T data) {
this.data = data;
return this;
}
public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(ResultCodeEnum resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
}
ResultUtil.java
public class ResultUtil {
// 操作成功,只返回结果码和提示信息
public static Result success(ResultCodeEnum resultCodeEnum) {
return new Result(ResultCodeEnum.SUCCESS);
}
// 操作成功,只返回结果码和具体的数据,但不返回提示信息
public static Result success(int code, String msg) {
return new Result(code, msg);
}
// 操作成功,返回具体的数据、结果码和提示信息
public static Result success(Object data) {
Result<Object> result = new Result(ResultCodeEnum.SUCCESS);
result.setData(data);
return result;
}
// 操作成功,返回具体的数据、结果码和提示信息
public static Result success(Integer code, String msg, Object data) {
return new Result<>(code,msg,data);
}
// 操作成功,只返回结果码和提示信息
public static Result fail(ResultCodeEnum resultCodeEnum) {
return new Result(ResultCodeEnum.FAIL);
}
// 操作失败,只返回指定的结果码和具体的数据,但不返回提示信息
public static Result fail(int code, String msg) {
return new Result(code, msg);
}
// 操作失败,返回具体的数据、结果码和提示信息
public static Result fail(Object data) {
Result<Object> result = new Result(ResultCodeEnum.FAIL);
result.setData(data);
return result;
}
// 操作失败,返回具体的数据、结果码和提示信息
public static Result fail(Integer code, String msg, Object data) {
return new Result(code,msg,data);
}
}
第四步:创建常量类:
public interface JwtConst {
//JWT签发者
String JWT_ID = "098f6bcd4621d373cade4e832627b4f6";
// 密钥, 经过Base64加密, 可自行替换
String JWT_SECRECT = "MDk4ZjZiY2Q0NjIxZD";
// 过期时间,单位毫秒
int JWT_TTL = 60*60*1000;
}
第五步:Jwt工具类:
public class JwtUtil {
private static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 创建JWT
public static String createJWT(String id, String subject, int ttl) {
Calendar calendar = Calendar.getInstance();
JwtBuilder builder = Jwts.builder()
.setId(id) //JWT唯一标识
.setIssuedAt(calendar.getTime()) //签发时间
.setSubject(subject) //JWT的主题,比如JSON类型的User对象
.signWith(key);
calendar.add(Calendar.MINUTE, ttl);
builder.setExpiration(calendar.getTime()); //过期时间
return builder.compact();
}
public static Claims parseJwt(String jwt) {
Claims claims = Jwts.parserBuilder().setSigningKey(key)
.build().parseClaimsJws(jwt).getBody();
return claims;
}
public static Result<Claims> validateJwt(String jwt) {
Result<Claims> result = null;
try {
Claims claims = Jwts.parserBuilder().setSigningKey(key)
.build().parseClaimsJws(jwt).getBody();
result = ResultUtil.success(ResultCodeEnum.SUCCESS)
.setData(claims);
} catch (ExpiredJwtException e) {
result = ResultUtil.fail(ResultCodeEnum.PERMISSION_TOKEN_EXPIRED);
e.printStackTrace();
} catch (UnsupportedJwtException e) {
result = ResultUtil.fail(ResultCodeEnum.PERMISSION_TOKEN_INVALID);
e.printStackTrace();
} catch (MalformedJwtException e) {
result = ResultUtil.fail(ResultCodeEnum.PERMISSION_TOKEN_INVALID);
e.printStackTrace();
} catch (SignatureException e) {
result = ResultUtil.fail(ResultCodeEnum.PERMISSION_SIGNATURE_ERROR);
e.printStackTrace();
}
return result;
}
public static String createSubject(User user) {
return JSON.toJSONString(user);
}
}
第六步:自定义全局异常及处理类
全局异常
@Data
@NoArgsConstructor
public class GlobalException extends RuntimeException {
private int code;
private String msg;
public GlobalException(int code,String msg) {
super(msg);
this.code =code;
this.msg = msg;
}
public GlobalException(ResultCodeEnum resultCode){
super(resultCode.getMsg());
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
}
处理类
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理自定义全局异常
@ExceptionHandler(GlobalException.class)
public Result handleException(GlobalException e) {
// 打印异常信息
log.error("### 异常信息:{} ###", e.getMessage());
return new Result(e.getCode(),e.getMsg());
}
// 处理所有不可知的异常
@ExceptionHandler(Exception.class)
public Result handleOtherException(Exception e) {
//打印异常堆栈信息
e.printStackTrace();
// 打印异常信息
log.error("### 未知的异常:{} ###", e.getMessage());
return new Result(1,"系统未知异常");
}
}
第七步:
@Log4j2
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
log.info("当前所访问的资源不需要登录");
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
// 从 http 请求头中取出 token
String jwt = request.getHeader("Authorization");
//如果header中没有则从Request中获取
if (jwt == null || jwt.length() == 0) {
jwt = request.getParameter("Authorization");
}
//如果没有获取到token
if (jwt == null || jwt.length() == 0) {
log.info("用户未登录,请先登录");
throw new GlobalException(ResultCodeEnum.USER_NOT_LOGGED_IN);
}
String token = JSON.parseObject(jwt).getString("token");
Result<Claims> result = JwtUtil.validateJwt(token);
if (result.getCode() == 200) {
Claims claims = result.getData();
String subject = claims.getSubject();
User user = JSON.parseObject(subject, User.class);
log.info("jwt 解析成功 {}", user);
再次验证,解析出来的用户在数据库/Redis中是否存在,
//User temp = userService.selectByPrimaryKey(Integer.parseInt(userId));
//if (temp == null) {
// log.info("用户不存在,请重新登录");
// throw new RuntimeException("用户不存在,请重新登录");
//}
log.info("用户存在,登录成功");
return true;
} else {
throw new GlobalException(ResultCodeEnum.PERMISSION_SIGNATURE_ERROR);
}
}
}
return false;
}
}
第八步:配置类:
@Configuration //@Configuration会将该类作为一个SpringBean添加到IOC容器中
public class InterceptorConfig implements WebMvcConfigurer {
// 配置跨域和支持restoful接口
@Override
public void addCorsMappings(CorsRegistry registry) {//跨域请求
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addInterceptor()需要一个实现HandlerInterceptor接口的拦截器实例
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");//用于设置拦截器的过滤路径规则
}
@Bean
public JwtInterceptor authenticationInterceptor() {
return new JwtInterceptor();
}
}
第九步:自定义注解:
Logintoken.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
Passtoken.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
第十步:创建Controller:
生成、解析JWT的Controller
@Controller
@RequestMapping("/jwt")
public class JWTController {
@PassToken
@RequestMapping("/login")
@ResponseBody
public Result<String> login(String username, String password) {
if (username == null || password == null) {
return new Result<String>(789, "用户名或密码不正确", null);
}
User user = new User(1001, username, password);
String jwt = JwtUtil.createJWT(JwtConst.JWT_ID, JwtUtil.createSubject(user), JwtConst.JWT_TTL);
System.out.println(jwt);
return new Result<>(200, "请求成功!", jwt);
}
@LoginToken
@RequestMapping("/parseJwt")
@ResponseBody
public User parseJwt(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
String token = JSON.parseObject(jwt).getString("token");
Claims claims = JwtUtil.parseJwt(token);
String subject = claims.getSubject();
User user = JSON.parseObject(subject, User.class);
System.out.println(user);
return user;
}
}
页面分发的Controller:
@Controller
public class DispatchController {
@PassToken
@GetMapping("/login")
public String showLogin() {
return "/login";
}
@PassToken
@GetMapping("/showJwt")
public String showJwt() {
return "showJwt";
}
}
第十一步:前端页面:
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
<script src="js/jquery3.5.1.js" type="text/javascript"></script>
</head>
<body>
<form id="loginForm">
<input type="text" name="username" value="zhangsan"><br>
<input type="password" name="password" value="123456">
</form>
<button id="btn">登录</button>
<script>
$(function () {
$("#btn").click(function () {
$.ajax({
type: "POST",//请求方式
url: "jwt/login", //请求地址
dataType: "JSON", //预期服务器端返回的数据的类型
data: $("#loginForm").serialize(),//数据,json字符串
success: function (result) { //请求成功
console.log(result);
if (result.code == 200) {
//获取令牌并保存到本地
localStorage.setItem("token", '{"token":"' + result.data + '"}');
}
if (result.code == 789) {
window.location = "login";
}
},
error: function (e) { //请求失败,包含具体的错误信息
console.log(e.status);
console.log(e.responseText);
}
});
});
});
</script>
</body>
</html>
注:第一次点登录按钮时,无法将token数据保存在Application中,原因未找到,发现原因的朋友麻烦告诉我一声。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script src="js/jquery3.3.1.js" type="text/javascript"></script>
</head>
<body>
首页启动时请求后台的解析Jwt的的请求
<div id="sh"></div>
<script>
$(function () {
$.ajax({
url: "jwt/parseJwt",
headers: {Authorization: localStorage.getItem("token")},
success:function (res) {
console.info(res);
$("#sh").text(JSON.stringify(res));
}
})
});
</script>
</body>
</html>
第十二步:部署运行查看结果:
- 访问http://localhost/jwt/login,在打开的login.html页面中单击登录按钮:
注:第一次点登录按钮时,无法将token数据保存在Application中,原因未找到。
2. 访问http://localhost/jwt/index,在页面上会看到: