【SpringBoot】DEMO:集成JWT实现token验证
之前使用Bootstrap和springBoot开发的时候,都是用cookie和session进行登录验证,在接触分布式之后,cookie和session明显没有token方便,在这里记录一下开发过程
一、了解一下 Token 身份验证
二、项目实现
- 使用 IDEA 和 mysql5.7 进行开发
1. 目录结构介绍
- Annotation:自定义注解
- Configuration:配置类
- Controller:控制层
- Interceptor:拦截器
- Mapping:数据库语句映射
- Service:服务层
2. 关键maven依赖
<!-- JWT token验证 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--自定义注解-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 使用自定义注解,可以很方便的对 Controller 进行监控:是否需要 token 才可以发起请求
3. 配置数据库、Model:模型层
public class User {
String Id;
String username;
String password;
—————— 省略 get 和 set 方法 ——————
}
- 数据库中 id 是 int 型,但是为了使用 jwt 生成 token,在 Model层 中使用 String 型 ,不影响后续操作
4. 创建两个自定义注解
- 跳过拦截器校验:PassToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
- 需要有 token 才可以进行操作: UserLoginToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
-
@Target:注解的作用目标
-
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention:注解的保留位置
-
RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解
5. 使用 JWT 生成 token
@Service("TokenService")
public class TokenService {
public String getToken(User user) {
String token="";
token= JWT.create().withAudience(user.getId())
.sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
}
- 将 id 保存到 token 里面,作标识
- 以 password 作为 token 的签名
6. 编写配置类
- 通过编写配置类,启动拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
- addPathPatterns 方法用于设置拦截器的过滤路径规则
- /** 拦截所有请求,通过判断是否有 @LoginRequired注解 决定是否需要登录
7. 编写拦截器
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserMapping userMapping;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 从 http 请求头中取出 token
String token = httpServletRequest.getHeader("token");
// 若果没有加拦截注解,即通过
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()) {
return true;
}
}
// 检查是否有 UserLoginToken 注解,如果有,进行拦截
if (method.isAnnotationPresent(UserLoginToken.class)) {
/* 获取 user 的 token */
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 没有 token
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id ,查询数据库是否存在 user
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userMapping.findUserById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token ,返回 true
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
- 实现一个拦截器就需要实现 HandlerInterceptor 接口
- boolean preHandle ()
预处理回调方法,实现处理器的预处理
第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行 - void postHandle():
后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用)
通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null - void afterCompletion():
整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行在DispatcherServlet渲染了对应的视图之后执行
用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
- boolean preHandle ()
8. 编写控制器
@RestController
@RequestMapping("api")
public class ApiController {
@Autowired
private UserMapping userMapping;
@Autowired
private TokenService tokenService;
@PostMapping("/login")
@ResponseBody
public Object apiLogin(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password){
User user = new User();
user.setUsername(username);
user.setPassword(password);
JSONObject jsonObject=new JSONObject();
User userForBase = userMapping.findUserUsername(username);
if (userForBase==null){
jsonObject.put("message","登录失败,用户不存在");
return jsonObject;
}else{
if (!userForBase.getPassword().equals(user.getPassword())){
jsonObject.put("message","登录失败,密码错误");
}else{
String token = tokenService.getToken(userForBase);
jsonObject.put("token", token);
jsonObject.put("user", userForBase);
}
return jsonObject;
}
}
@UserLoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通过验证";
}
}
- Get方法:getMessage 加入了 @UserLoginToken 注解,需要拿到 token 才可以访问
9. Mapper映射层
@Mapper
@Repository
public interface UserMapping {
@Select("select * from user where id = #{userId}")
User findUserById(@Param(value = "userId") String userId);
@Select("select * from user where username = #{username}")
User findUserUsername(@Param(value = "username") String username);
}
三、接口测试
- 我们使用 postman 进行接口测试
1. 当 user 没有 token 的时候,无法对 getMessage 进行访问
2. 用户登录,获取 token
3. 把 token 写入 getMessage请求中
- 大功告成!